mirror of https://github.com/periph/devices
lepton: device driver for the FLIR Lepton
Include CLI tool to query the camera, grab a single frame and trigger a calibration.pull/1/head
parent
2853fb49f6
commit
debd177aa5
@ -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.
|
||||
@ -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}},
|
||||
}
|
||||
}
|
||||
@ -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]]
|
||||
}
|
||||
@ -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
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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:])
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
Loading…
Reference in New Issue