diff --git a/devices/lepton/cci/cci.go b/devices/lepton/cci/cci.go new file mode 100644 index 0000000..a4016a7 --- /dev/null +++ b/devices/lepton/cci/cci.go @@ -0,0 +1,550 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// "stringer" can be installed with "go get golang.org/x/tools/cmd/stringer" +//go:generate stringer -output=strings_gen.go -type=CameraStatus,command,FFCShutterMode,FFCState,ShutterPos,ShutterTempLockoutState + +// Package cci declares the Camera Command Interface to interact with a FLIR +// Lepton over I²C. +// +// This protocol controls and queries the camera but is not used to read the +// images. +// +// Datasheet +// +// http://www.flir.com/uploadedFiles/OEM/Products/LWIR-Cameras/Lepton/FLIR-Lepton-Software-Interface-Description-Document.pdf +// +// Found via http://www.flir.com/cores/display/?id=51878 +package cci + +import ( + "encoding/binary" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/devices" + "periph.io/x/periph/devices/lepton/internal" +) + +// StatusBit is the status as returned by the FLIR Lepton. +type StatusBit uint16 + +// Status bitmask. +const ( + StatusBusy StatusBit = 0x1 + StatusBootNormal StatusBit = 0x2 + StatusBooted StatusBit = 0x4 + StatusErrorMask StatusBit = 0xFF00 +) + +func (s StatusBit) String() string { + var o []string + if s&StatusBusy != 0 { + o = append(o, "Busy") + } + if s&StatusBootNormal != 0 { + o = append(o, "BootNormal") + } + if s&StatusBooted != 0 { + o = append(o, "Booted") + } + if v := s & StatusErrorMask; v != 0 { + o = append(o, "0x"+strconv.FormatUint(uint64(v)>>8, 16)) + } + return strings.Join(o, "|") +} + +// CameraStatus returns the status of the FLIR Lepton's camera. +type CameraStatus uint32 + +// Valid values for CameraStatus. +const ( + SystemReady CameraStatus = 0 + SystemInitializing CameraStatus = 1 + SystemInLowPowerMode CameraStatus = 2 + SystemGoingIntoStandby CameraStatus = 3 + SystemFlatFieldInProcess CameraStatus = 4 +) + +// Status returns the camera status as returned by the camera. +type Status struct { + CameraStatus CameraStatus + CommandCount uint16 +} + +// ShutterTempLockoutState is used in FFCMode. +type ShutterTempLockoutState uint32 + +// Valid values for ShutterTempLockoutState. +const ( + ShutterTempLockoutStateInactive ShutterTempLockoutState = 0 + ShutterTempLockoutStateHigh ShutterTempLockoutState = 1 + ShutterTempLockoutStateLow ShutterTempLockoutState = 2 +) + +// FFCShutterMode is used in FFCMode. +type FFCShutterMode uint32 + +// Valid values for FFCShutterMode. +const ( + FFCShutterModeManual FFCShutterMode = 0 + FFCShutterModeAuto FFCShutterMode = 1 + FFCShutterModeExternal FFCShutterMode = 2 +) + +// ShutterPos returns the shutter position, which is used to calibrate the +// camera. +type ShutterPos uint32 + +// Valid values for ShutterPos. +const ( + ShutterPosUnknown ShutterPos = 0xFFFFFFFF // -1 + ShutterPosIdle ShutterPos = 0 + ShutterPosOpen ShutterPos = 1 + ShutterPosClosed ShutterPos = 2 + ShutterPosBrakeOn ShutterPos = 3 +) + +// FFCState describes the Flat-Field Correction state. +type FFCState uint8 + +const ( + // No FFC was requested. + FFCNever FFCState = 0 + // FFC is in progress. It lasts 23 frames (at 27fps) so it lasts less than a second. + FFCInProgress FFCState = 1 + // FFC was completed successfully. + FFCComplete FFCState = 2 +) + +// FFCMode +type FFCMode struct { + FFCShutterMode FFCShutterMode // Default: FFCShutterModeExternal + ShutterTempLockoutState ShutterTempLockoutState // Default: ShutterTempLockoutStateInactive + ElapsedTimeSinceLastFFC time.Duration // Uptime + DesiredFFCPeriod time.Duration // Default: 300s + DesiredFFCTempDelta devices.Celsius // Default: 3°C + ImminentDelay uint16 // Default: 52 + VideoFreezeDuringFFC bool // Default: true + FFCDesired bool // Default: false + ExplicitCommandToOpen bool // Default: false +} + +// + +// Dev is the Lepton specific Command and Control Interface (CCI). +// +// +// Dev can safely accessed concurrently via multiple goroutines. +// +// This interface is accessed via I²C and provides access to view and modify +// the internal state. +// +// Maximum I²C speed is 1Mhz. +type Dev struct { + c conn + serial uint64 +} + +// New returns a driver for the FLIR Lepton CCI protocol. +func New(i i2c.Bus) (*Dev, error) { + d := &Dev{ + c: conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: i, Addr: 0x2A}, Order: internal.Big16}}, + } + // Wait for the device to be booted. + for { + if status, err := d.c.waitIdle(); err != nil { + return nil, err + } else if status == StatusBootNormal|StatusBooted { + return d, nil + } + //log.Printf("lepton not yet booted: 0x%02x", status) + // Polling rocks. + time.Sleep(5 * time.Millisecond) + } +} + +// Init initializes the FLIR Lepton in raw 14 bits mode, enables telemetry as +// header. +func (d *Dev) Init() error { + if err := d.c.set(agcEnable, internal.Disabled); err != nil { + return err + } + // Setup telemetry to always be as the header. There's no reason to make this + // configurable by the user. + if err := d.c.set(sysTelemetryEnable, internal.Enabled); err != nil { + return err + } + if err := d.c.set(sysTelemetryLocation, internal.Header); err != nil { + return err + } + + /* + // Verification code in case the I²C do not work properly. + f := internal.Enabled + if err := d.c.get(agcEnable, &f); err != nil { + return err + } else if f != internal.Disabled { + return fmt.Errorf("lepton-cci: internal verification for AGC failed %v", f) + } + if err := d.c.get(sysTelemetryEnable, &f); err != nil { + return err + } else if f != internal.Enabled { + return fmt.Errorf("lepton-cci: internal verification for telemetry flag failed %v", f) + } + hdr := internal.Footer + if err := d.c.get(sysTelemetryLocation, &hdr); err != nil { + return err + } else if hdr != internal.Header { + return fmt.Errorf("lepton-cci: internal verification for telemetry position failed %s", hdr) + } + */ + return nil +} + +// WaitIdle waits for camera to be ready. +// +// It loops forever and returns the StatusBit. +func (d *Dev) WaitIdle() (StatusBit, error) { + return d.c.waitIdle() +} + +// Halt stops the camera. +func (d *Dev) Halt() error { + // TODO(maruel): Doc says it won't restart. Yo. + return d.c.run(oemPowerDown) +} + +// GetStatus return the status of the camera as known by the camera itself. +func (d *Dev) GetStatus() (*Status, error) { + var v internal.Status + if err := d.c.get(sysStatus, &v); err != nil { + return nil, err + } + return &Status{ + CameraStatus: CameraStatus(v.CameraStatus), + CommandCount: v.CommandCount, + }, nil +} + +// GetSerial returns the FLIR Lepton serial number. +func (d *Dev) GetSerial() (uint64, error) { + if d.serial == 0 { + out := uint64(0) + if err := d.c.get(sysSerialNumber, &out); err != nil { + return out, err + } + d.serial = out + } + return d.serial, nil +} + +// GetUptime returns the uptime. Rolls over after 1193 hours. +func (d *Dev) GetUptime() (time.Duration, error) { + var v internal.DurationMS + if err := d.c.get(sysUptime, &v); err != nil { + return 0, err + } + return v.ToD(), nil +} + +// GetTemp returns the temperature inside the camera. +func (d *Dev) GetTemp() (devices.Celsius, error) { + var v internal.CentiK + if err := d.c.get(sysTemperature, &v); err != nil { + return 0, err + } + return v.ToC(), nil +} + +// GetTempHousing returns the temperature of the camera housing. +func (d *Dev) GetTempHousing() (devices.Celsius, error) { + var v internal.CentiK + if err := d.c.get(sysHousingTemperature, &v); err != nil { + return 0, err + } + return v.ToC(), nil +} + +// GetFFCModeControl returns the internal state with regards to calibration. +func (d *Dev) GetFFCModeControl() (*FFCMode, error) { + v := internal.FFCMode{} + if err := d.c.get(sysFFCMode, &v); err != nil { + return nil, err + } + return &FFCMode{ + FFCShutterMode: FFCShutterMode(v.FFCShutterMode), + ShutterTempLockoutState: ShutterTempLockoutState(v.ShutterTempLockoutState), + ElapsedTimeSinceLastFFC: v.ElapsedTimeSinceLastFFC.ToD(), + DesiredFFCPeriod: v.DesiredFFCPeriod.ToD(), + DesiredFFCTempDelta: devices.Celsius(v.DesiredFFCTempDelta * 10), + ImminentDelay: v.ImminentDelay, + VideoFreezeDuringFFC: v.VideoFreezeDuringFFC == internal.Enabled, + FFCDesired: v.FFCDesired == internal.Enabled, + ExplicitCommandToOpen: v.ExplicitCommandToOpen == internal.Enabled, + }, nil +} + +// GetShutterPos returns the position of the shutter if present. +func (d *Dev) GetShutterPos() (ShutterPos, error) { + out := ShutterPosUnknown + err := d.c.get(sysShutterPosition, &out) + return out, err +} + +// RunFFC forces a Flat-Field Correction to be done by the camera for +// recalibration. It takes 23 frames and the camera runs at 27fps so it lasts +// less than a second. +func (d *Dev) RunFFC() error { + return d.c.run(sysFCCRunNormalization) +} + +// + +// conn is the low level connection. +// +// It implements the low level protocol to run the GET, SET and RUN commands +// via memory mapped registers. +type conn struct { + mu sync.Mutex + r mmr.Dev16 +} + +// waitIdle waits for the busy bit to clear. +func (c *conn) waitIdle() (StatusBit, error) { + // Do not take the lock. + for { + if s, err := c.r.ReadUint16(regStatus); err != nil || StatusBit(s)&StatusBusy == 0 { + return StatusBit(s), err + } + time.Sleep(5 * time.Millisecond) + } +} + +// get returns an attribute by querying the device. +func (c *conn) get(cmd command, data interface{}) error { + if data == nil { + return errors.New("lepton-cci: get() argument must not be nil") + } + if t := reflect.TypeOf(data); t.Kind() != reflect.Ptr && t.Kind() != reflect.Slice { + return fmt.Errorf("lepton-cci: get() argument must be a pointer or a slice, got %T", data) + } + size := binary.Size(data) + if size&1 != 0 { + return errors.New("lepton-cci: get() argument must be 16 bits aligned") + } + nbWords := size / 2 + if nbWords > 1024 { + return errors.New("cci: buffer too large") + } + + c.mu.Lock() + defer c.mu.Unlock() + if _, err := c.waitIdle(); err != nil { + return err + } + if err := c.r.WriteUint16(regDataLength, uint16(nbWords)); err != nil { + return err + } + if err := c.r.WriteUint16(regCommandID, uint16(cmd)); err != nil { + return err + } + s, err := c.waitIdle() + if err != nil { + return err + } + if s&0xff00 != 0 { + return fmt.Errorf("cci: error 0x%x", byte(s>>8)) + } + if nbWords <= 16 { + err = c.r.ReadStruct(regData0, data) + } else { + err = c.r.ReadStruct(regDataBuffer0, data) + } + if err != nil { + return err + } + /* + // Verify CRC: + if crc, err := c.r.ReadUint16(regDataCRC); err != nil { + return err + } else if expected := internal.CRC16(data); expected != crc { + return fmt.Errorf("invalid crc; expected 0x%04X; got 0x%04X", expected, crc) + } + */ + //log.Printf("get(%s) = %v", cmd, data) + return nil +} + +// set returns an attribute on the device. +func (c *conn) set(cmd command, data interface{}) error { + if data == nil { + return errors.New("lepton-cci: set() argument must not be nil") + } + size := binary.Size(data) + if size&1 != 0 { + return errors.New("lepton-cci: set() argument must be 16 bits aligned") + } + nbWords := size / 2 + if nbWords > 1024 { + return errors.New("lepton-cci: buffer too large") + } + + c.mu.Lock() + defer c.mu.Unlock() + if _, err := c.waitIdle(); err != nil { + return err + } + var err error + if nbWords <= 16 { + err = c.r.WriteStruct(regData0, data) + } else { + err = c.r.WriteStruct(regDataBuffer0, data) + } + if err != nil { + return err + } + if err := c.r.WriteUint16(regDataLength, uint16(nbWords)); err != nil { + return err + } + if err := c.r.WriteUint16(regCommandID, uint16(cmd)|1); err != nil { + return err + } + s, err := c.waitIdle() + if err != nil { + return err + } + if s&0xff00 != 0 { + return fmt.Errorf("cci: error 0x%x", s>>8) + } + return nil +} + +// run runs a command on the device that doesn't need any argument. +func (c *conn) run(cmd command) error { + c.mu.Lock() + defer c.mu.Unlock() + if _, err := c.waitIdle(); err != nil { + return err + } + if err := c.r.WriteUint16(regDataLength, 0); err != nil { + return err + } + if err := c.r.WriteUint16(regCommandID, uint16(cmd)|2); err != nil { + return err + } + s, err := c.waitIdle() + if err != nil { + return err + } + if s&0xff00 != 0 { + return fmt.Errorf("cci: error 0x%x", s>>8) + } + return nil +} + +// + +// All the available registers. +const ( + regPower uint16 = 0 + regStatus uint16 = 2 + regCommandID uint16 = 4 + regDataLength uint16 = 6 + regData0 uint16 = 8 + regData1 uint16 = 10 + regData2 uint16 = 12 + regData3 uint16 = 14 + regData4 uint16 = 16 + regData5 uint16 = 18 + regData6 uint16 = 20 + regData7 uint16 = 22 + regData8 uint16 = 24 + regData9 uint16 = 26 + regData10 uint16 = 28 + regData11 uint16 = 30 + regData12 uint16 = 32 + regData13 uint16 = 34 + regData14 uint16 = 36 + regData15 uint16 = 38 + regDataCRC uint16 = 40 + regDataBuffer0 uint16 = 0xF800 + regDataBuffer1 uint16 = 0xFC00 +) + +// command is a command supported by the FLIR Lepton over its CCI interface. +type command uint16 + +// All the available commands. +// +// See page 17 for more details. +// +// Number of words and supported action. +const ( + agcEnable command = 0x0100 // 2 GET/SET + agcRoiSelect command = 0x0108 // 4 GET/SET + agcHistogramStats command = 0x010C // 4 GET + agcHeqDampFactor command = 0x0124 // 1 GET/SET + agcHeqClipLimitHigh command = 0x012C // 1 GET/SET + agcHeqClipLimitLow command = 0x0130 // 1 GET/SET + agcHeqEmptyCounts command = 0x013C // 1 GET/SET + agcHeqOutputScaleFactor command = 0x0144 // 2 GET/SET + agcCalculationEnable command = 0x0148 // 2 GET/SET + oemPowerDown command = 0x4800 // 0 RUN + oemPartNumber command = 0x481C // 16 GET + oemSoftwareRevision command = 0x4820 // 4 GET + oemVideoOutputEnable command = 0x4824 // 2 GET/SET + oemVideoOutputFormat command = 0x4828 // 2 GET/SET + oemVideoOutputSource command = 0x482C // 2 GET/SET + oemCustomerPartNumber command = 0x4838 // 16 GET + oemVideoOutputConst command = 0x483C // 1 GET/SET + oemCameraReboot command = 0x4840 // 0 RUN + oemFCCNormalizationTarget command = 0x4844 // 1 GET/SET/RUN + oemStatus command = 0x4848 // 2 GET + oemFrameMeanIntensity command = 0x484C // 1 GET + oemGPIOModeSelect command = 0x4854 // 2 GET/SET + oemGPIOVSyncPhaseDelay command = 0x4858 // 2 GET/SET + oemUserDefaults command = 0x485C // 2 GET/RUN + oemRestoreUserDefaults command = 0x4860 // 0 RUN + oemShutterProfile command = 0x4064 // 2 GET/SET + oemThermalShutdownEnable command = 0x4868 // 2 GET/SET + oemBadPixel command = 0x486C // 2 GET/SET + oemTemporalFilter command = 0x4870 // 2 GET/SET + oemColumnNoiseFilter command = 0x4874 // 2 GET/SET + oemPixelNoiseFilter command = 0x4878 // 2 GET/SET + sysPing command = 0x0200 // 0 RUN + sysStatus command = 0x0204 // 4 GET + sysSerialNumber command = 0x0208 // 4 GET + sysUptime command = 0x020C // 2 GET + sysHousingTemperature command = 0x0210 // 1 GET + sysTemperature command = 0x0214 // 1 GET + sysTelemetryEnable command = 0x0218 // 2 GET/SET + sysTelemetryLocation command = 0x021C // 2 GET/SET + sysExecuteFrameAverage command = 0x0220 // 0 RUN Undocumented but listed in SDK + sysFlatFieldFrames command = 0x0224 // 2 GET/SET It's an enum, max is 128 + sysCustomSerialNumber command = 0x0228 // 16 GET It's a string + sysRoiSceneStats command = 0x022C // 4 GET + sysRoiSceneSelect command = 0x0230 // 4 GET/SET + sysThermalShutdownCount command = 0x0234 // 1 GET Number of times it exceeded 80C + sysShutterPosition command = 0x0238 // 2 GET/SET + sysFFCMode command = 0x023C // 17 GET/SET Manual control; doc says 20 words but it's 17 in practice. + sysFCCRunNormalization command = 0x0240 // 0 RUN + sysFCCStatus command = 0x0244 // 2 GET + vidColorLookupSelect command = 0x0304 // 2 GET/SET + vidColorLookupTransfer command = 0x0308 // 512 GET/SET + vidFocusCalculationEnable command = 0x030C // 2 GET/SET + vidFocusRoiSelect command = 0x0310 // 4 GET/SET + vidFocusMetricThreshold command = 0x0314 // 2 GET/SET + vidFocusMetricGet command = 0x0318 // 2 GET + vidVideoFreezeEnable command = 0x0324 // 2 GET/SET +) + +// TODO(maruel): Enable RadXXX commands. diff --git a/devices/lepton/cci/cci_test.go b/devices/lepton/cci/cci_test.go new file mode 100644 index 0000000..34db8f2 --- /dev/null +++ b/devices/lepton/cci/cci_test.go @@ -0,0 +1,612 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package cci + +import ( + "testing" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/devices/lepton/internal" +) + +func TestStatusBit(t *testing.T) { + v := ^StatusBit(0) + if s := v.String(); s != "Busy|BootNormal|Booted|0xff" { + t.Fatal(s) + } +} + +func TestNew_WaitIdle_fail(t *testing.T) { + bus := i2ctest.Playback{DontPanic: true} + if d, err := New(&bus); d != nil || err == nil { + t.Fatal("WaitIdle() should have returned an error") + } +} + +func TestNew_WaitIdle(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // WaitIdle loop once. + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x00}}, + // WaitIdle return not booted. + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x01}}, + // WaitIdle return booted. + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + }, + } + _, err := New(&bus) + if err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestInit(t *testing.T) { + // set(agcEnable, internal.Disabled) + ops := setOps([]byte{0x0, 0x4, 0x1, 0x1}, []byte{0, 0, 0, 0}) + // set(sysTelemetryEnable, internal.Enabled) + ops = append(ops, setOps([]byte{0x0, 0x4, 0x2, 0x19}, []byte{0, 1, 0, 0})...) + // set(sysTelemetryLocation, internal.Header) + ops = append(ops, setOps([]byte{0x0, 0x4, 0x2, 0x1d}, []byte{0, 0, 0, 0})...) + bus, d := getDev(ops) + if err := d.Init(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestInit_fail(t *testing.T) { + data := [][]i2ctest.IO{ + // set(agcEnable, internal.Disabled) + setOps([]byte{0x0, 0x4, 0x1, 0x1}, []byte{0, 0, 0, 0}), + // set(sysTelemetryEnable, internal.Enabled) + setOps([]byte{0x0, 0x4, 0x2, 0x19}, []byte{0, 1, 0, 0}), + // set(sysTelemetryLocation, internal.Header) + setOps([]byte{0x0, 0x4, 0x2, 0x1d}, []byte{0, 0, 0, 0}), + } + var ops []i2ctest.IO + { + bus, d := getDev(ops) + bus.DontPanic = true + if d.Init() == nil { + t.Fatal("failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + } + for i := 0; i < len(data)-1; i++ { + ops = append(ops, data[i]...) + bus, d := getDev(ops) + bus.DontPanic = true + if d.Init() == nil { + t.Fatal("failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + } +} + +func TestWaitIdle(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } + bus := i2ctest.Playback{Ops: ops} + d := Dev{c: conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}}} + if _, err := d.WaitIdle(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestHalt(t *testing.T) { + bus, d := getDev(runOps([]byte{0x0, 0x4, 0x48, 0x2})) + if err := d.Halt(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetStatus(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x4}, []byte{0, 0, 0, 0, 0, 0, 0, 0})) + if _, err := d.GetStatus(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetStatus_fail(t *testing.T) { + if _, err := getDevFail().GetStatus(); err == nil { + t.Fatal("failed") + } +} + +func TestGetSerial(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x8}, []byte{0, 0, 0, 0, 0, 0, 0, 0})) + if _, err := d.GetSerial(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetSerial_fail(t *testing.T) { + if _, err := getDevFail().GetSerial(); err == nil { + t.Fatal("failed") + } +} + +func TestGetUptime(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0xc}, []byte{0, 0, 0, 0})) + if _, err := d.GetUptime(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetUptime_fail(t *testing.T) { + if _, err := getDevFail().GetUptime(); err == nil { + t.Fatal("failed") + } +} + +func TestGetTemp(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x14}, []byte{0, 0})) + if _, err := d.GetTemp(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetTemp_fail(t *testing.T) { + if _, err := getDevFail().GetTemp(); err == nil { + t.Fatal("failed") + } +} + +func TestGetTempHousing(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x10}, []byte{0, 0})) + if _, err := d.GetTempHousing(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetTempHousing_fail(t *testing.T) { + if _, err := getDevFail().GetTempHousing(); err == nil { + t.Fatal("failed") + } +} + +func TestGetFFCModeControl(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x3c}, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) + if _, err := d.GetFFCModeControl(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetFFCModeControl_fail(t *testing.T) { + if _, err := getDevFail().GetFFCModeControl(); err == nil { + t.Fatal("failed") + } +} + +func TestGetShutterPos(t *testing.T) { + bus, d := getDev(getOps([]byte{0x0, 0x4, 0x2, 0x38}, []byte{0, 0, 0, 0})) + if _, err := d.GetShutterPos(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestGetShutterPos_fail(t *testing.T) { + if _, err := getDevFail().GetShutterPos(); err == nil { + t.Fatal("failed") + } +} + +func TestRunFFC(t *testing.T) { + bus, d := getDev(runOps([]byte{0x0, 0x4, 0x2, 0x42})) + if err := d.RunFFC(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +// + +func TestConn_get(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x4}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x4}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: []byte{0x00, 0x08}, R: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if err := c.get(sysStatus, &v); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + + // Test error paths. + for len(ops) != 0 { + ops = ops[:len(ops)-1] + bus := i2ctest.Playback{Ops: ops, DontPanic: true} + c = conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if c.get(sysStatus, &v) == nil { + t.Fatal("should have failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + } +} + +func TestConn_get_large(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x4, 0x0}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x4}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: []byte{0xf8, 0}, R: make([]byte, 2048)}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + v := make([]byte, 2048) + if err := c.get(sysStatus, v); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestConn_get_fail_waitidle(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x4}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x4}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x01, 0x00}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if c.get(sysStatus, &v) == nil { + t.Fatal("waitIdle failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestConn_get_fail(t *testing.T) { + bus := i2ctest.Playback{} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + if c.get(sysStatus, nil) == nil { + t.Fatal("nil value") + } + if c.get(sysStatus, 1) == nil { + t.Fatal("not a pointer") + } + v := []byte{0} + if c.get(sysStatus, &v) == nil { + t.Fatal("odd length") + } + v = make([]byte, 2048+2) + if c.get(sysStatus, &v) == nil { + t.Fatal("overflow") + } +} + +func TestConn_set(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: []byte{0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x4}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x5}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if err := c.set(sysStatus, &v); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + + // Test error paths. + for len(ops) != 0 { + ops = ops[:len(ops)-1] + bus := i2ctest.Playback{Ops: ops, DontPanic: true} + c = conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if c.set(sysStatus, &v) == nil { + t.Fatal("should have failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + } +} + +func TestConn_set_large(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: append([]byte{0xf8, 0}, make([]byte, 2048)...)}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x4, 0x0}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x5}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + v := make([]byte, 2048) + if err := c.set(sysStatus, v); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestConn_set_fail_waitidle(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: []byte{0x0, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x4}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x5}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x0f, 0x00}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + var v internal.Status + if c.set(sysStatus, &v) == nil { + t.Fatal("waitIdle failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestConn_set_fail(t *testing.T) { + bus := i2ctest.Playback{} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + if c.set(sysStatus, nil) == nil { + t.Fatal("nil value") + } + v := []byte{0} + if c.set(sysStatus, &v) == nil { + t.Fatal("odd length") + } + v = make([]byte, 2048+2) + if c.set(sysStatus, &v) == nil { + t.Fatal("overflow") + } +} + +func TestConn_run(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x0}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x42}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + if err := c.run(sysFCCRunNormalization); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + + // Test error paths. + for len(ops) != 0 { + ops = ops[:len(ops)-1] + bus := i2ctest.Playback{Ops: ops, DontPanic: true} + c = conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + if c.run(sysFCCRunNormalization) == nil { + t.Fatal("should have failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } + } +} + +func TestConn_run_fail_waitidle(t *testing.T) { + ops := []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x0}}, + // regCommandID + {Addr: 42, W: []byte{0x0, 0x4, 0x2, 0x42}}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x0f, 0x00}}, + } + bus := i2ctest.Playback{Ops: ops} + c := conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: &bus, Addr: 0x2A}, Order: internal.Big16}} + if c.run(sysFCCRunNormalization) == nil { + t.Fatal("waitIdle failed") + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +// + +func TestStrings(t *testing.T) { + if s := SystemReady.String(); s != "SystemReady" { + t.Fatal(s) + } + if s := CameraStatus(30).String(); s != "CameraStatus(30)" { + t.Fatal(s) + } + + if s := agcEnable.String(); s != "agcEnable" { + t.Fatal(s) + } + if s := command(0).String(); s != "command(0)" { + t.Fatal(s) + } + + if s := FFCShutterModeManual.String(); s != "FFCShutterModeManual" { + t.Fatal(s) + } + if s := FFCShutterMode(30).String(); s != "FFCShutterMode(30)" { + t.Fatal(s) + } + + if s := FFCNever.String(); s != "FFCNever" { + t.Fatal(s) + } + if s := FFCState(30).String(); s != "FFCState(30)" { + t.Fatal(s) + } + + if s := ShutterPosIdle.String(); s != "ShutterPosIdle" { + t.Fatal(s) + } + if s := ShutterPos(30).String(); s != "ShutterPos(30)" { + t.Fatal(s) + } + if s := ShutterPosUnknown.String(); s != "ShutterPosUnknown" { + t.Fatal(s) + } + + if s := ShutterTempLockoutStateInactive.String(); s != "ShutterTempLockoutStateInactive" { + t.Fatal(s) + } + if s := ShutterTempLockoutState(30).String(); s != "ShutterTempLockoutState(30)" { + t.Fatal(s) + } +} + +// + +func getDev(ops []i2ctest.IO) (*i2ctest.Playback, *Dev) { + bus := &i2ctest.Playback{Ops: ops} + d := &Dev{c: conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: bus, Addr: 0x2A}, Order: internal.Big16}}} + return bus, d +} + +func getDevFail() *Dev { + bus := &i2ctest.Playback{DontPanic: true} + d := &Dev{c: conn{r: mmr.Dev16{Conn: &i2c.Dev{Bus: bus, Addr: 0x2A}, Order: internal.Big16}}} + return d +} + +func getOps(cmd, data []byte) []i2ctest.IO { + return []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, byte(len(data) / 2)}}, + // regCommandID + {Addr: 42, W: cmd}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: []byte{0x00, 0x08}, R: data}, + } +} + +func setOps(cmd, data []byte) []i2ctest.IO { + return []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regData0 + {Addr: 42, W: append([]byte{0, 8}, data...)}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, byte(len(data) / 2)}}, + // regCommandID + {Addr: 42, W: cmd}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } +} + +func runOps(c []byte) []i2ctest.IO { + return []i2ctest.IO{ + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + // regDataLength + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x0}}, + // regCommandID + {Addr: 42, W: c}, + // waitIdle + {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, + } +} diff --git a/devices/lepton/cci/strings_gen.go b/devices/lepton/cci/strings_gen.go new file mode 100644 index 0000000..35a58b2 --- /dev/null +++ b/devices/lepton/cci/strings_gen.go @@ -0,0 +1,138 @@ +// Code generated by "stringer -output=strings_gen.go -type=CameraStatus,command,FFCShutterMode,FFCState,ShutterPos,ShutterTempLockoutState"; DO NOT EDIT. + +package cci + +import "fmt" + +const _CameraStatus_name = "SystemReadySystemInitializingSystemInLowPowerModeSystemGoingIntoStandbySystemFlatFieldInProcess" + +var _CameraStatus_index = [...]uint8{0, 11, 29, 49, 71, 95} + +func (i CameraStatus) String() string { + if i >= CameraStatus(len(_CameraStatus_index)-1) { + return fmt.Sprintf("CameraStatus(%d)", i) + } + return _CameraStatus_name[_CameraStatus_index[i]:_CameraStatus_index[i+1]] +} + +const _command_name = "agcEnableagcRoiSelectagcHistogramStatsagcHeqDampFactoragcHeqClipLimitHighagcHeqClipLimitLowagcHeqEmptyCountsagcHeqOutputScaleFactoragcCalculationEnablesysPingsysStatussysSerialNumbersysUptimesysHousingTemperaturesysTemperaturesysTelemetryEnablesysTelemetryLocationsysExecuteFrameAveragesysFlatFieldFramessysCustomSerialNumbersysRoiSceneStatssysRoiSceneSelectsysThermalShutdownCountsysShutterPositionsysFFCModesysFCCRunNormalizationsysFCCStatusvidColorLookupSelectvidColorLookupTransfervidFocusCalculationEnablevidFocusRoiSelectvidFocusMetricThresholdvidFocusMetricGetvidVideoFreezeEnableoemShutterProfileoemPowerDownoemPartNumberoemSoftwareRevisionoemVideoOutputEnableoemVideoOutputFormatoemVideoOutputSourceoemCustomerPartNumberoemVideoOutputConstoemCameraRebootoemFCCNormalizationTargetoemStatusoemFrameMeanIntensityoemGPIOModeSelectoemGPIOVSyncPhaseDelayoemUserDefaultsoemRestoreUserDefaultsoemThermalShutdownEnableoemBadPixeloemTemporalFilteroemColumnNoiseFilteroemPixelNoiseFilter" + +var _command_map = map[command]string{ + 256: _command_name[0:9], + 264: _command_name[9:21], + 268: _command_name[21:38], + 292: _command_name[38:54], + 300: _command_name[54:73], + 304: _command_name[73:91], + 316: _command_name[91:108], + 324: _command_name[108:131], + 328: _command_name[131:151], + 512: _command_name[151:158], + 516: _command_name[158:167], + 520: _command_name[167:182], + 524: _command_name[182:191], + 528: _command_name[191:212], + 532: _command_name[212:226], + 536: _command_name[226:244], + 540: _command_name[244:264], + 544: _command_name[264:286], + 548: _command_name[286:304], + 552: _command_name[304:325], + 556: _command_name[325:341], + 560: _command_name[341:358], + 564: _command_name[358:381], + 568: _command_name[381:399], + 572: _command_name[399:409], + 576: _command_name[409:431], + 580: _command_name[431:443], + 772: _command_name[443:463], + 776: _command_name[463:485], + 780: _command_name[485:510], + 784: _command_name[510:527], + 788: _command_name[527:550], + 792: _command_name[550:567], + 804: _command_name[567:587], + 16484: _command_name[587:604], + 18432: _command_name[604:616], + 18460: _command_name[616:629], + 18464: _command_name[629:648], + 18468: _command_name[648:668], + 18472: _command_name[668:688], + 18476: _command_name[688:708], + 18488: _command_name[708:729], + 18492: _command_name[729:748], + 18496: _command_name[748:763], + 18500: _command_name[763:788], + 18504: _command_name[788:797], + 18508: _command_name[797:818], + 18516: _command_name[818:835], + 18520: _command_name[835:857], + 18524: _command_name[857:872], + 18528: _command_name[872:894], + 18536: _command_name[894:918], + 18540: _command_name[918:929], + 18544: _command_name[929:946], + 18548: _command_name[946:966], + 18552: _command_name[966:985], +} + +func (i command) String() string { + if str, ok := _command_map[i]; ok { + return str + } + return fmt.Sprintf("command(%d)", i) +} + +const _FFCShutterMode_name = "FFCShutterModeManualFFCShutterModeAutoFFCShutterModeExternal" + +var _FFCShutterMode_index = [...]uint8{0, 20, 38, 60} + +func (i FFCShutterMode) String() string { + if i >= FFCShutterMode(len(_FFCShutterMode_index)-1) { + return fmt.Sprintf("FFCShutterMode(%d)", i) + } + return _FFCShutterMode_name[_FFCShutterMode_index[i]:_FFCShutterMode_index[i+1]] +} + +const _FFCState_name = "FFCNeverFFCInProgressFFCComplete" + +var _FFCState_index = [...]uint8{0, 8, 21, 32} + +func (i FFCState) String() string { + if i >= FFCState(len(_FFCState_index)-1) { + return fmt.Sprintf("FFCState(%d)", i) + } + return _FFCState_name[_FFCState_index[i]:_FFCState_index[i+1]] +} + +const ( + _ShutterPos_name_0 = "ShutterPosIdleShutterPosOpenShutterPosClosedShutterPosBrakeOn" + _ShutterPos_name_1 = "ShutterPosUnknown" +) + +var ( + _ShutterPos_index_0 = [...]uint8{0, 14, 28, 44, 61} + _ShutterPos_index_1 = [...]uint8{0, 17} +) + +func (i ShutterPos) String() string { + switch { + case 0 <= i && i <= 3: + return _ShutterPos_name_0[_ShutterPos_index_0[i]:_ShutterPos_index_0[i+1]] + case i == 4294967295: + return _ShutterPos_name_1 + default: + return fmt.Sprintf("ShutterPos(%d)", i) + } +} + +const _ShutterTempLockoutState_name = "ShutterTempLockoutStateInactiveShutterTempLockoutStateHighShutterTempLockoutStateLow" + +var _ShutterTempLockoutState_index = [...]uint8{0, 31, 58, 84} + +func (i ShutterTempLockoutState) String() string { + if i >= ShutterTempLockoutState(len(_ShutterTempLockoutState_index)-1) { + return fmt.Sprintf("ShutterTempLockoutState(%d)", i) + } + return _ShutterTempLockoutState_name[_ShutterTempLockoutState_index[i]:_ShutterTempLockoutState_index[i+1]] +} diff --git a/devices/lepton/internal/internal.go b/devices/lepton/internal/internal.go new file mode 100644 index 0000000..f5c8226 --- /dev/null +++ b/devices/lepton/internal/internal.go @@ -0,0 +1,170 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package internal + +import ( + "encoding/binary" + "time" + + "periph.io/x/periph/devices" +) + +// Flag is used in FFCMode. +type Flag uint32 + +// Valid values for Flag. +const ( + Disabled Flag = 0 + Enabled Flag = 1 +) + +// DurationMS is duration in millisecond. +// +// It is an implementation detail of the protocol. +type DurationMS uint32 + +func (d DurationMS) ToD() time.Duration { + return time.Duration(d) * time.Millisecond +} + +// CentiK is temperature in 0.01°K +// +// It is an implementation detail of the protocol. +type CentiK uint16 + +func (c CentiK) ToC() devices.Celsius { + v := (int(c) - 27315) * 10 + return devices.Celsius(v) +} + +// Status returns the camera status as returned by the camera. +type Status struct { + CameraStatus uint32 + CommandCount uint16 + Reserved uint16 +} + +// FFCMode +type FFCMode struct { + FFCShutterMode uint32 // Default: FFCShutterModeExternal + ShutterTempLockoutState uint32 // Default: ShutterTempLockoutStateInactive + VideoFreezeDuringFFC Flag // Default: Enabled + FFCDesired Flag // Default: Disabled + ElapsedTimeSinceLastFFC DurationMS // Uptime in ms. + DesiredFFCPeriod DurationMS // Default: 300000 + ExplicitCommandToOpen Flag // Default: Disabled + DesiredFFCTempDelta uint16 // Default: 300 + ImminentDelay uint16 // Default: 52 + + // These are documented at page 51 but not listed in the structure. + // ClosePeriodInFrames uint16 // Default: 4 + // OpenPeriodInFrames uint16 // Default: 1 +} + +// TelemetryLocation is used with SysTelemetryLocation. +type TelemetryLocation uint32 + +// Valid values for TelemetryLocation. +const ( + Header TelemetryLocation = 0 + Footer TelemetryLocation = 1 +) + +// + +type table [256]uint16 + +const ccittFalse = 0x1021 + +var ccittFalseTable table + +func init() { + makeReversedTable(ccittFalse, &ccittFalseTable) +} + +func makeReversedTable(poly uint16, t *table) { + width := uint16(16) + for i := uint16(0); i < 256; i++ { + crc := i << (width - 8) + for j := 0; j < 8; j++ { + if crc&(1<<(width-1)) != 0 { + crc = (crc << 1) ^ poly + } else { + crc <<= 1 + } + } + t[i] = crc + } +} + +func updateReversed(crc uint16, t *table, p []byte) uint16 { + for _, v := range p { + crc = t[byte(crc>>8)^v] ^ (crc << 8) + } + return crc +} + +// CRC16 calculates the reversed CCITT CRC16 checksum. +func CRC16(d []byte) uint16 { + return updateReversed(0, &ccittFalseTable, d) +} + +// + +// Big16 translates big endian 16bits words but everything larger is in little +// endian. +// +// It implements binary.ByteOrder. +var Big16 big16 + +type big16 struct{} + +func (big16) Uint16(b []byte) uint16 { + _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808 + return uint16(b[1]) | uint16(b[0])<<8 +} + +func (big16) PutUint16(b []byte, v uint16) { + _ = b[1] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 8) + b[1] = byte(v) +} + +func (big16) Uint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[1]) | uint32(b[0])<<8 | uint32(b[3])<<16 | uint32(b[2])<<24 +} + +func (big16) PutUint32(b []byte, v uint32) { + _ = b[3] // early bounds check to guarantee safety of writes below + b[1] = byte(v) + b[0] = byte(v >> 8) + b[3] = byte(v >> 16) + b[2] = byte(v >> 24) +} + +func (big16) Uint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[1]) | uint64(b[0])<<8 | uint64(b[3])<<16 | uint64(b[2])<<24 | + uint64(b[5])<<32 | uint64(b[4])<<40 | uint64(b[7])<<48 | uint64(b[6])<<56 +} + +func (big16) PutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[1] = byte(v) + b[0] = byte(v >> 8) + b[3] = byte(v >> 16) + b[2] = byte(v >> 24) + b[5] = byte(v >> 32) + b[4] = byte(v >> 40) + b[7] = byte(v >> 48) + b[6] = byte(v >> 56) +} + +func (big16) String() string { + return "big16" +} + +var _ binary.ByteOrder = Big16 diff --git a/devices/lepton/internal/internal_test.go b/devices/lepton/internal/internal_test.go new file mode 100644 index 0000000..f070d22 --- /dev/null +++ b/devices/lepton/internal/internal_test.go @@ -0,0 +1,62 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package internal + +import ( + "bytes" + "testing" + "time" +) + +func TestDuration(t *testing.T) { + if d := DurationMS(100).ToD(); d != 100*time.Millisecond { + t.Fatal(d) + } +} + +func TestCentiK(t *testing.T) { + if c := CentiK(100).ToC(); c != -272150 { + t.Fatal(c) + } +} + +func TestCRC16(t *testing.T) { + d := []byte{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39} + if c := CRC16(d); c != 0x31c3 { + t.Fatal(c) + } +} + +func TestBig16(t *testing.T) { + if s := Big16.String(); s != "big16" { + t.Fatal(s) + } + d := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + if v := Big16.Uint16(d); v != 0x0102 { + t.Fatal(v) + } + if v := Big16.Uint32(d); v != 0x03040102 { + t.Fatal(v) + } + if v := Big16.Uint64(d); v != 0x0708050603040102 { + t.Fatal(v) + } + + d = make([]byte, 2) + Big16.PutUint16(d, 0x0102) + if !bytes.Equal(d, []byte{0x01, 0x02}) { + t.Fatal(d) + } + d = make([]byte, 4) + Big16.PutUint32(d, 0x01020304) + if !bytes.Equal(d, []byte{0x03, 0x04, 0x01, 0x02}) { + t.Fatal(d) + } + d = make([]byte, 8) + Big16.PutUint64(d, 0x0102030405060708) + if !bytes.Equal(d, []byte{0x07, 0x08, 0x05, 0x06, 0x03, 0x04, 0x01, 0x02}) { + t.Fatal(d) + } +} diff --git a/devices/lepton/lepton.go b/devices/lepton/lepton.go new file mode 100644 index 0000000..e084c98 --- /dev/null +++ b/devices/lepton/lepton.go @@ -0,0 +1,450 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// Package lepton drivers a FLIR Lepton. +// +// References +// +// Official FLIR reference: +// http://www.flir.com/cvs/cores/view/?id=51878 +// +// Product page: +// http://www.flir.com/cores/content/?id=66257 +// +// Datasheet: +// http://www.flir.com/uploadedFiles/OEM/Products/LWIR-Cameras/Lepton/Lepton%20Engineering%20Datasheet%20-%20with%20Radiometry.pdf +package lepton + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "sync" + "time" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/spi" + "periph.io/x/periph/devices" + "periph.io/x/periph/devices/lepton/cci" + "periph.io/x/periph/devices/lepton/internal" +) + +// Metadata is constructed from telemetry data, which is sent with each frame. +type Metadata struct { + SinceStartup time.Duration // + FrameCount uint32 // Number of frames since the start of the camera, in 27fps (not 9fps). + AvgValue uint16 // Average value of the buffer. + Temp devices.Celsius // Temperature inside the camera. + TempHousing devices.Celsius // Camera housing temperature. + RawTemp uint16 // + RawTempHousing uint16 // + FFCSince time.Duration // Time since last internal calibration. + FFCTemp devices.Celsius // Temperature at last internal calibration. + FFCTempHousing devices.Celsius // + FFCState cci.FFCState // Current calibration state. + FFCDesired bool // Asserted at start-up, after period (default 3m) or after temperature change (default 3°K). Indicates that a calibration should be triggered as soon as possible. + Overtemp bool // true 10s before self-shutdown. +} + +// Frame is a FLIR Lepton frame, containing 14 bits resolution intensity stored +// as image.Gray16. +// +// Values centered around 8192 accorging to camera body temperature. Effective +// range is 14 bits, so [0, 16383]. +// +// Each 1 increment is approximatively 0.025°K. +type Frame struct { + *image.Gray16 + Metadata Metadata // Metadata that is sent along the pixels. +} + +// Dev controls a FLIR Lepton. +// +// It assumes a specific breakout board. Sadly the breakout board doesn't +// expose the PWR_DWN_L and RESET_L lines so it is impossible to shut down the +// Lepton. +type Dev struct { + *cci.Dev + s spi.Conn + cs gpio.PinOut + prevImg *image.Gray16 + frameA, frameB []byte + frameWidth int // in bytes + frameLines int + maxTxSize int + delay time.Duration +} + +// New returns an initialized connection to the FLIR Lepton. +// +// The CS line is manually managed by using mode spi.NoCS when calling +// DevParams(). In this case pass nil for the cs parameter. Some spidev drivers +// refuse spi.NoCS, they do not implement proper support to not trigger the CS +// line so a manual CS (really, any GPIO pin) must be used instead. +// +// Maximum SPI speed is 20Mhz. Minimum usable rate is ~2.2Mhz to sustain a 9hz +// framerate at 80x60. +// +// Maximum I²C speed is 1Mhz. +// +// MOSI is not used and should be grounded. +func New(s spi.Conn, i i2c.Bus, cs gpio.PinOut) (*Dev, error) { + // Sadly the Lepton will inconditionally send 27fps, even if the effective + // rate is 9fps. + mode := spi.Mode3 + if cs == nil { + // Query the CS pin before disabling it. + p, ok := s.(spi.Pins) + if !ok { + return nil, errors.New("lepton: require manual access to the CS pin") + } + cs = p.CS() + if cs == gpio.INVALID { + return nil, errors.New("lepton: require manual access to a valid CS pin") + } + mode |= spi.NoCS + } + // TODO(maruel): Switch to 16 bits per word, so that big endian 16 bits word + // decoding is done by the SPI driver. + if err := s.DevParams(20000000, mode, 8); err != nil { + return nil, err + } + c, err := cci.New(i) + if err != nil { + return nil, err + } + // TODO(maruel): Support Lepton 3 with 160x120. + w := 80 + h := 60 + // telemetry data is a 3 lines header. + frameLines := h + 3 + frameWidth := w*2 + 4 + d := &Dev{ + Dev: c, + s: s, + cs: cs, + prevImg: image.NewGray16(image.Rect(0, 0, w, h)), + frameWidth: frameWidth, + frameLines: frameLines, + delay: time.Second, + } + if l, ok := s.(conn.Limits); ok { + d.maxTxSize = l.MaxTxSize() + } + if status, err := d.GetStatus(); err != nil { + return nil, err + } else if status.CameraStatus != cci.SystemReady { + // The lepton takes < 1 second to boot so it should not happen normally. + return nil, fmt.Errorf("lepton: camera is not ready: %s", status) + } + if err := d.Init(); err != nil { + return nil, err + } + return d, nil +} + +// ReadImg reads an image. +// +// It is ok to call other functions concurrently to send commands to the +// camera. +func (d *Dev) ReadImg() (*Frame, error) { + f := &Frame{Gray16: image.NewGray16(d.prevImg.Bounds())} + for { + if err := d.readFrame(f); err != nil { + return nil, err + } + if f.Metadata.FFCDesired == true { + // TODO(maruel): Automatically trigger FFC when applicable, only do if + // the camera has a shutter. + //go d.RunFFC() + } + if !bytes.Equal(d.prevImg.Pix, f.Gray16.Pix) { + break + } + // It also happen if the image is 100% static without noise. + } + copy(d.prevImg.Pix, f.Pix) + return f, nil +} + +// Private details. + +// stream reads continuously from the SPI connection. +func (d *Dev) stream(done <-chan struct{}, c chan<- []byte) error { + lines := 8 + if d.maxTxSize != 0 { + if l := d.maxTxSize / d.frameWidth; l < lines { + lines = l + } + } + if err := d.cs.Out(gpio.Low); err != nil { + return err + } + defer d.cs.Out(gpio.High) + for { + // TODO(maruel): Use a ring buffer to stop continuously allocating. + buf := make([]byte, d.frameWidth*lines) + if err := d.s.Tx(nil, buf); err != nil { + return err + } + for i := 0; i < len(buf); i += d.frameWidth { + select { + case <-done: + return nil + case c <- buf[i : i+d.frameWidth]: + } + } + } +} + +// readFrame reads one frame. +// +// Each frame is sent as a packet over SPI including telemetry data as an +// header. See page 49-57 for "VoSPI" protocol explanation. +// +// This operation must complete within 32ms. Frames occur every 38.4ms at +// almost 27hz. +// +// Resynchronization is done by deasserting CS and CLK for at least 5 frames +// (>185ms). +// +// When a packet starts, it must be completely clocked out within 3 line +// periods. +// +// One frame of 80x60 at 2 byte per pixel, plus 4 bytes overhead per line plus +// 3 lines of telemetry is (3+60)*(4+160) = 10332. The sysfs-spi driver limits +// each transaction size, the default is 4Kb. To reduce the risks of failure, +// reads 4Kb at a time and figure out the lines from there. The Lepton is very +// cranky if reading is not done quickly enough. +func (d *Dev) readFrame(f *Frame) error { + done := make(chan struct{}, 1) + c := make(chan []byte, 1024) + var err error + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + defer close(c) + err = d.stream(done, c) + }() + defer func() { + done <- struct{}{} + }() + + timeout := time.After(d.delay) + w := f.Bounds().Dx() + sync := 0 + discard := 0 + for { + select { + case <-timeout: + return fmt.Errorf("failed to synchronize after %s", d.delay) + case l, ok := <-c: + if !ok { + wg.Wait() + return err + } + h := internal.Big16.Uint16(l) + if h&packetHeaderDiscard == packetHeaderDiscard { + discard++ + sync = 0 + continue + } + headerID := h & packetHeaderMask + if discard != 0 { + //log.Printf("discarded %d", discard) + discard = 0 + sync = 0 + } + if int(headerID) == 0 && sync == 0 && !verifyCRC(l) { + //log.Printf("no crc") + sync = 0 + continue + } + if int(headerID) != sync { + //log.Printf("%d != %d", headerID, sync) + sync = 0 + continue + } + if sync == 0 { + // Parse the first row of telemetry data. + if err2 := f.Metadata.parseTelemetry(l[4:]); err2 != nil { + //log.Printf("Failed to parse telemetry line: %v", err2) + continue + } + } else if sync >= 3 { + // Image. + for x := 0; x < w; x++ { + o := 4 + x*2 + f.SetGray16(x, sync-3, color.Gray16{internal.Big16.Uint16(l[o : o+2])}) + } + } + if sync++; sync == d.frameLines { + // Last line, done. + return nil + } + } + } +} + +func (m *Metadata) parseTelemetry(data []byte) error { + // Telemetry line. + var rowA telemetryRowA + if err := binary.Read(bytes.NewBuffer(data), internal.Big16, &rowA); err != nil { + return err + } + m.SinceStartup = rowA.TimeCounter.ToD() + m.FrameCount = rowA.FrameCounter + m.AvgValue = rowA.FrameMean + m.Temp = rowA.FPATemp.ToC() + m.TempHousing = rowA.HousingTemp.ToC() + m.RawTemp = rowA.FPATempCounts + m.RawTempHousing = rowA.HousingTempCounts + m.FFCSince = rowA.TimeCounterLastFFC.ToD() + m.FFCTemp = rowA.FPATempLastFFC.ToC() + m.FFCTempHousing = rowA.HousingTempLastFFC.ToC() + if rowA.StatusBits&statusMaskNil != 0 { + return fmt.Errorf("\n(Status: 0x%08X) & (Mask: 0x%08X) = (Extra: 0x%08X) in 0x%08X\n", rowA.StatusBits, statusMask, rowA.StatusBits&statusMaskNil, statusMaskNil) + } + m.FFCDesired = rowA.StatusBits&statusFFCDesired != 0 + m.Overtemp = rowA.StatusBits&statusOvertemp != 0 + fccstate := rowA.StatusBits & statusFFCStateMask >> statusFFCStateShift + if rowA.TelemetryRevision == 8 { + switch fccstate { + case 0: + m.FFCState = cci.FFCNever + case 1: + m.FFCState = cci.FFCInProgress + case 2: + m.FFCState = cci.FFCComplete + default: + return fmt.Errorf("unexpected fccstate %d; %v", fccstate, data) + } + } else { + switch fccstate { + case 0: + m.FFCState = cci.FFCNever + case 2: + m.FFCState = cci.FFCInProgress + case 3: + m.FFCState = cci.FFCComplete + default: + return fmt.Errorf("unexpected fccstate %d; %v", fccstate, data) + } + } + return nil +} + +// As documented as page.21 +const ( + packetHeaderDiscard = 0x0F00 + packetHeaderMask = 0x0FFF // ID field is 12 bits. Leading 4 bits are reserved. + // Observed status: + // 0x00000808 + // 0x00007A01 + // 0x00022200 + // 0x01AD0000 + // 0x02BF0000 + // 0x1FFF0000 + // 0x3FFF0001 + // 0xDCD0FFFF + // 0xFFDCFFFF + statusFFCDesired uint32 = 1 << 3 // 0x00000008 + statusFFCStateMask uint32 = 3 << 4 // 0x00000030 + statusFFCStateShift uint32 = 4 // + statusReserved uint32 = 1 << 11 // 0x00000800 + statusAGCState uint32 = 1 << 12 // 0x00001000 + statusOvertemp uint32 = 1 << 20 // 0x00100000 + statusMask = statusFFCDesired | statusFFCStateMask | statusAGCState | statusOvertemp | statusReserved // 0x00101838 + statusMaskNil = ^statusMask // 0xFFEFE7C7 +) + +// telemetryRowA is the data structure returned after the frame as documented +// at p.19-20. +// +// '*' means the value observed in practice make sense. +// Value after '-' is observed value. +type telemetryRowA struct { + TelemetryRevision uint16 // 0 * + TimeCounter internal.DurationMS // 1 * + StatusBits uint32 // 3 * Bit field (mostly make sense) + ModuleSerial [16]uint8 // 5 - Is empty (!) + SoftwareRevision uint64 // 13 Junk. + Reserved17 uint16 // 17 - 1101 + Reserved18 uint16 // 18 + Reserved19 uint16 // 19 + FrameCounter uint32 // 20 * + FrameMean uint16 // 22 * The average value from the whole frame. + FPATempCounts uint16 // 23 + FPATemp internal.CentiK // 24 * + HousingTempCounts uint16 // 25 + HousingTemp internal.CentiK // 27 * + Reserved27 uint16 // 27 + Reserved28 uint16 // 28 + FPATempLastFFC internal.CentiK // 29 * + TimeCounterLastFFC internal.DurationMS // 30 * + HousingTempLastFFC internal.CentiK // 32 * + Reserved33 uint16 // 33 + AGCROILeft uint16 // 35 * - 0 (Likely inversed, haven't confirmed) + AGCROITop uint16 // 34 * - 0 + AGCROIRight uint16 // 36 * - 79 - SDK was wrong! + AGCROIBottom uint16 // 37 * - 59 - SDK was wrong! + AGCClipLimitHigh uint16 // 38 * + AGCClipLimitLow uint16 // 39 * + Reserved40 uint16 // 40 - 1 + Reserved41 uint16 // 41 - 128 + Reserved42 uint16 // 42 - 64 + Reserved43 uint16 // 43 + Reserved44 uint16 // 44 + Reserved45 uint16 // 45 + Reserved46 uint16 // 46 + Reserved47 uint16 // 47 - 1 + Reserved48 uint16 // 48 - 128 + Reserved49 uint16 // 49 - 1 + Reserved50 uint16 // 50 + Reserved51 uint16 // 51 + Reserved52 uint16 // 52 + Reserved53 uint16 // 53 + Reserved54 uint16 // 54 + Reserved55 uint16 // 55 + Reserved56 uint16 // 56 - 30 + Reserved57 uint16 // 57 + Reserved58 uint16 // 58 - 1 + Reserved59 uint16 // 59 - 1 + Reserved60 uint16 // 60 - 78 + Reserved61 uint16 // 61 - 58 + Reserved62 uint16 // 62 - 7 + Reserved63 uint16 // 63 - 90 + Reserved64 uint16 // 64 - 40 + Reserved65 uint16 // 65 - 210 + Reserved66 uint16 // 66 - 255 + Reserved67 uint16 // 67 - 255 + Reserved68 uint16 // 68 - 23 + Reserved69 uint16 // 69 - 6 + Reserved70 uint16 // 70 + Reserved71 uint16 // 71 + Reserved72 uint16 // 72 - 7 + Reserved73 uint16 // 73 + Log2FFCFrames uint16 // 74 Found 3, should be 27? + Reserved75 uint16 // 75 + Reserved76 uint16 // 76 + Reserved77 uint16 // 77 + Reserved78 uint16 // 78 + Reserved79 uint16 // 79 +} + +// verifyCRC test the equation x^16 + x^12 + x^5 + x^0 +func verifyCRC(d []byte) bool { + tmp := make([]byte, len(d)) + copy(tmp, d) + tmp[0] &^= 0x0F + tmp[2] = 0 + tmp[3] = 0 + return internal.CRC16(tmp) == internal.Big16.Uint16(d[2:]) +} diff --git a/devices/lepton/lepton_test.go b/devices/lepton/lepton_test.go new file mode 100644 index 0000000..9759aa8 --- /dev/null +++ b/devices/lepton/lepton_test.go @@ -0,0 +1,416 @@ +// Copyright 2017 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package lepton + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "image" + "image/color" + "testing" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/conntest" + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpiotest" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/spi" + "periph.io/x/periph/conn/spi/spitest" + "periph.io/x/periph/devices" + "periph.io/x/periph/devices/lepton/internal" +) + +func TestNew_cs(t *testing.T) { + i := i2ctest.Playback{ + Ops: append(initSequence(), + []i2ctest.IO{ + {Addr: 42, W: []byte{0x0, 0x2}, R: []byte{0x0, 0x6}}, // waitIdle + {Addr: 42, W: []byte{0x0, 0x6, 0x0, 0x0}}, + {Addr: 42, W: []byte{0x0, 0x4, 0x48, 0x2}}, + {Addr: 42, W: []byte{0x0, 0x2}, R: []byte{0x0, 0x6}}, // waitIdle + }...), + } + s := spitest.Playback{} + d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) + if err != nil { + t.Fatal(err) + } + if err := d.Halt(); err != nil { + t.Fatal(err) + } + if err := i.Close(); err != nil { + t.Fatal(err) + } + if err := s.Close(); err != nil { + t.Fatal(err) + } +} + +func TestNew(t *testing.T) { + i := i2ctest.Playback{Ops: initSequence()} + s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} + _, err := New(&s, &i, nil) + if err != nil { + t.Fatal(err) + } + if err := i.Close(); err != nil { + t.Fatal(err) + } + if err := s.Close(); err != nil { + t.Fatal(err) + } +} + +func TestNew_Init_fail(t *testing.T) { + // Strip off last command. + ops := initSequence() + i := i2ctest.Playback{Ops: ops[:len(ops)-1], DontPanic: true} + s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} + if _, err := New(&s, &i, nil); err == nil { + t.Fatal("cci.Dev.Init() failed") + } + if err := i.Close(); err != nil { + t.Fatal(err) + } + if err := s.Close(); err != nil { + t.Fatal(err) + } +} + +func TestNew_GetStatus_fail(t *testing.T) { + i := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 6, 0, 4}}, // GetStatus() + {Addr: 42, W: []byte{0, 4, 2, 4}}, // + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + }, + DontPanic: true, + } + s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} + if _, err := New(&s, &i, nil); err == nil { + t.Fatal("cci.Dev.GetStatus() failed") + } + if err := i.Close(); err != nil { + t.Fatal(err) + } + if err := s.Close(); err != nil { + t.Fatal(err) + } +} + +func TestNew_GetStatus_bad(t *testing.T) { + i := i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 6, 0, 4}}, // GetStatus() + {Addr: 42, W: []byte{0, 4, 2, 4}}, // + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 8}, R: []byte{1, 0, 0, 0, 0, 0, 0, 0}}, // GetStatus() result + }, + DontPanic: true, + } + s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} + if _, err := New(&s, &i, nil); err == nil { + t.Fatal("cci.Dev.GetStatus() failed") + } + if err := i.Close(); err != nil { + t.Fatal(err) + } + if err := s.Close(); err != nil { + t.Fatal(err) + } +} + +func TestNew_fail_invalid(t *testing.T) { + i := i2ctest.Record{} + s := spitest.Record{} + if _, err := New(&s, &i, nil); err == nil { + t.Fatal("spi.Pins.CS() returns INVALID") + } +} + +func TestNew_fail_no_Pins(t *testing.T) { + i := i2ctest.Record{} + s := spiStream{} + if _, err := New(&s, &i, nil); err == nil { + t.Fatal("no CS and no spi.Pins") + } +} + +func TestNew_DevParams(t *testing.T) { + i := i2ctest.Record{} + s := spiStream{err: errors.New("injected")} + if _, err := New(&s, &i, &gpiotest.Pin{N: "CS"}); err == nil { + t.Fatal("DevParams failed") + } +} + +func TestNew_cci_New_fail(t *testing.T) { + i := i2ctest.Playback{DontPanic: true} + s := spitest.Record{} + if _, err := New(&s, &i, &gpiotest.Pin{N: "CS"}); err == nil { + t.Fatal("cci.New failed") + } +} + +func TestReadImg(t *testing.T) { + i := i2ctest.Playback{Ops: initSequence()} + s := spiStream{data: prepareFrame(t)} + d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) + if err != nil { + t.Fatal(err) + } + f, err := d.ReadImg() + if err != nil { + t.Fatal(err) + } + if f.Metadata.TempHousing != devices.Celsius(2000) { + t.Fatal(f.Metadata.TempHousing) + } + // Compare the frame with the reference image. It should match. + ref := referenceFrame() + if !bytes.Equal(ref.Pix, f.Pix) { + offset := 0 + for { + if ref.Pix[offset] != f.Pix[offset] { + break + } + offset++ + } + t.Fatalf("different pixels at offset %d:\n%s\n%s", offset, hex.EncodeToString(ref.Pix[offset:]), hex.EncodeToString(f.Pix[offset:])) + } + if err := i.Close(); err != nil { + t.Fatal(err) + } +} + +func TestReadImg_fail_Tx(t *testing.T) { + i := i2ctest.Playback{Ops: initSequence()} + s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}} + d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) + if err != nil { + t.Fatal(err) + } + if _, err := d.ReadImg(); err == nil { + t.Fatal("spi bus Tx failed") + } + if err := i.Close(); err != nil { + t.Fatal(err) + } +} + +func TestReadImg_fail_OUt(t *testing.T) { + i := i2ctest.Playback{Ops: initSequence()} + s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}} + d, err := New(&s, &i, &failPin{}) + if err != nil { + t.Fatal(err) + } + if _, err := d.ReadImg(); err == nil { + t.Fatal("spi bus Tx failed") + } + if err := i.Close(); err != nil { + t.Fatal(err) + } +} + +func TestParseTelemetry_fail(t *testing.T) { + l := telemetryLine(t) + m := Metadata{} + if m.parseTelemetry(l[:len(l)-1]) == nil { + t.Fatal("buffer too short") + } + buf := bytes.Buffer{} + rowA := telemetryRowA{StatusBits: statusMaskNil} + if err := binary.Write(&buf, internal.Big16, &rowA); err != nil { + t.Fatal(err) + } + if m.parseTelemetry(buf.Bytes()) == nil { + t.Fatal("bad status") + } +} + +func TestParseTelemetry(t *testing.T) { + m := Metadata{} + if err := m.parseTelemetry(telemetryLine(t)); err != nil { + t.Fatal(err) + } + + data := []struct { + rowA telemetryRowA + success bool + }{ + {telemetryRowA{TelemetryRevision: 8, StatusBits: 0 << statusFFCStateShift}, true}, + {telemetryRowA{TelemetryRevision: 8, StatusBits: 1 << statusFFCStateShift}, true}, + {telemetryRowA{TelemetryRevision: 8, StatusBits: 2 << statusFFCStateShift}, true}, + {telemetryRowA{TelemetryRevision: 8, StatusBits: 3 << statusFFCStateShift}, false}, + {telemetryRowA{StatusBits: 0 << statusFFCStateShift}, true}, + {telemetryRowA{StatusBits: 1 << statusFFCStateShift}, false}, + {telemetryRowA{StatusBits: 2 << statusFFCStateShift}, true}, + {telemetryRowA{StatusBits: 3 << statusFFCStateShift}, true}, + } + for _, line := range data { + buf := bytes.Buffer{} + if err := binary.Write(&buf, internal.Big16, &line.rowA); err != nil { + t.Fatal(err) + } + err := m.parseTelemetry(buf.Bytes()) + if line.success { + if err != nil { + t.Fatal(err) + } + } else { + if err == nil { + t.Fatal("expected failure") + } + } + } +} + +// + +func initSequence() []i2ctest.IO { + return []i2ctest.IO{ + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 6, 0, 4}}, // GetStatus() + {Addr: 42, W: []byte{0, 4, 2, 4}}, // + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 8}, R: []byte{0, 0, 0, 0, 0, 0, 0, 0}}, // GetStatus() result + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 8, 0, 0, 0, 0}}, // Init() + {Addr: 42, W: []byte{0, 6, 0, 0x2}}, // + {Addr: 42, W: []byte{0, 4, 1, 0x1}}, // + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 8, 0, 1, 0, 0}}, // + {Addr: 42, W: []byte{0, 6, 0, 2}}, // + {Addr: 42, W: []byte{0, 4, 2, 0x19}}, // + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + {Addr: 42, W: []byte{0, 8, 0, 0, 0, 0}}, // + {Addr: 42, W: []byte{0, 6, 0, 2}}, // + {Addr: 42, W: []byte{0, 4, 2, 0x1d}}, // Init() end + {Addr: 42, W: []byte{0, 2}, R: []byte{0, 6}}, // waitIdle + } +} + +func telemetryLine(t *testing.T) []byte { + b := bytes.Buffer{} + rowA := telemetryRowA{ + TelemetryRevision: 8, + StatusBits: statusFFCDesired, + HousingTemp: internal.CentiK(27515), // 2°C + } + if err := binary.Write(&b, internal.Big16, &rowA); err != nil { + t.Fatal(err) + } + return b.Bytes() +} + +func appendHeader(t *testing.T, i int, d []byte) []byte { + if len(d) != 160 { + t.Fatalf("currently hardcoded for 80x60: %d", len(d)) + } + out := make([]byte, 164) + internal.Big16.PutUint16(out, uint16(i)) + copy(out[4:], d) + calcCRC(out) + return out +} + +func referenceFrame() *image.Gray16 { + r := image.Rect(0, 0, 80, 60) + img := image.NewGray16(r) + for y := r.Min.Y; y < r.Max.Y; y++ { + for x := r.Min.X; x < r.Max.X; x++ { + img.SetGray16(x, y, color.Gray16{uint16(8192 - 80 + (x * 2))}) + } + } + return img +} + +func prepareFrame(t *testing.T) []byte { + buf := bytes.Buffer{} + tmp := make([]byte, 160) + buf.Write(appendHeader(t, 0, telemetryLine(t))) + buf.Write(appendHeader(t, 1, tmp)) + buf.Write(appendHeader(t, 2, tmp)) + img := referenceFrame() + r := img.Bounds() + for y := 0; y < r.Max.Y; y++ { + for x := 0; x < r.Max.X; x++ { + internal.Big16.PutUint16(tmp[x*2:], img.Gray16At(x, y).Y) + } + buf.Write(appendHeader(t, y+3, tmp)) + } + return buf.Bytes() +} + +func calcCRC(d []byte) { + tmp := make([]byte, len(d)) + copy(tmp, d) + tmp[0] &^= 0x0F + tmp[2] = 0 + tmp[3] = 0 + internal.Big16.PutUint16(d[2:], internal.CRC16(tmp)) +} + +type spiStream struct { + t *testing.T + data []byte + offset int + err error +} + +func (s *spiStream) DevParams(maxHz int64, mode spi.Mode, bits int) error { + if maxHz != 20000000 { + s.t.Fatal(maxHz) + } + if mode != spi.Mode3 { + s.t.Fatal(mode) + } + if bits != 8 { + s.t.Fatal(bits) + } + return s.err +} + +func (s *spiStream) Tx(w, r []byte) error { + if w != nil { + s.t.Fatal("write is not implemented") + } + if s.offset < len(s.data) { + copy(r, s.data[s.offset:]) + s.offset += len(r) + } + return s.err +} + +func (s *spiStream) TxPackets(p []spi.Packet) error { + s.t.Fatal("TxPackets is not implemented") + return nil +} + +func (s *spiStream) Duplex() conn.Duplex { + return conn.DuplexUnknown +} + +func (s *spiStream) MaxTxSize() int { + return 7 * 164 +} + +type failPin struct { + gpiotest.Pin +} + +func (f *failPin) Out(l gpio.Level) error { + return errors.New("injected") +}