You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/lepton/cci/cci.go

573 lines
17 KiB
Go

// 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.
//go:generate -command stringer go run golang.org/x/tools/cmd/stringer@latest
//go:generate stringer -output=strings_gen.go -type=CameraStatus,command,FFCShutterMode,FFCState,ShutterPos,ShutterTempLockoutState
package cci
import (
"encoding/binary"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/mmr"
"periph.io/x/conn/v3/physic"
"periph.io/x/devices/v3/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 (
// FFCNever means no FFC was requested.
FFCNever FFCState = 0
// FFCInProgress means a FFC is in progress. It lasts 23 frames (at 27fps) so it lasts less than a second.
FFCInProgress FFCState = 1
// FFCComplete means FFC was completed successfully.
FFCComplete FFCState = 2
)
// FFCMode describes the various self-calibration settings and state.
type FFCMode struct {
FFCShutterMode FFCShutterMode // Default: FFCShutterModeExternal
ShutterTempLockoutState ShutterTempLockoutState // Default: ShutterTempLockoutStateInactive
ElapsedTimeSinceLastFFC time.Duration // Uptime
DesiredFFCPeriod time.Duration // Default: 300s
DesiredFFCTempDelta physic.Temperature // Default: 3K
ImminentDelay uint16 // Default: 52
VideoFreezeDuringFFC bool // Default: true
FFCDesired bool // Default: false
ExplicitCommandToOpen bool // Default: false
}
// New returns a driver for the FLIR Lepton CCI protocol.
func New(i i2c.Bus) (*Dev, error) {
d := &Dev{
c: cciConn{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.
sleep(5 * time.Millisecond)
}
}
// 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 cciConn
serial uint64
}
// String implements conn.Resource.
func (d *Dev) String() string {
return d.c.String()
}
// Halt implements conn.Resource.
//
// Halt stops the camera.
func (d *Dev) Halt() error {
// TODO(maruel): Doc says it won't restart. Yo.
return d.c.run(oemPowerDown)
}
// 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()
}
// 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.Duration(), nil
}
// GetTemp returns the temperature inside the camera.
func (d *Dev) GetTemp() (physic.Temperature, error) {
var v internal.CentiK
if err := d.c.get(sysTemperature, &v); err != nil {
return 0, err
}
return v.Temperature(), nil
}
// GetTempHousing returns the temperature of the camera housing.
func (d *Dev) GetTempHousing() (physic.Temperature, error) {
var v internal.CentiK
if err := d.c.get(sysHousingTemperature, &v); err != nil {
return 0, err
}
return v.Temperature(), nil
}
// Sense implements physic.SenseEnv. It returns the housing temperature.
func (d *Dev) Sense(e *physic.Env) error {
var err error
e.Temperature, err = d.GetTempHousing()
return err
}
// SenseContinuous implements physic.SenseEnv.
func (d *Dev) SenseContinuous(time.Duration) (<-chan physic.Env, error) {
// TODO(maruel): Manually poll in a loop via time.NewTicker, or better
// leverage the frames being read.
return nil, errors.New("cci: not implemented")
}
// Precision implements physic.SenseEnv.
func (d *Dev) Precision(e *physic.Env) {
e.Temperature = 10 * physic.MilliKelvin
}
// 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.Duration(),
DesiredFFCPeriod: v.DesiredFFCPeriod.Duration(),
DesiredFFCTempDelta: v.DesiredFFCTempDelta.Temperature(),
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)
}
//
// cciConn is the low level connection.
//
// It implements the low level protocol to run the GET, SET and RUN commands
// via memory mapped registers.
type cciConn struct {
mu sync.Mutex
r mmr.Dev16
}
func (c *cciConn) String() string {
return c.r.String()
}
// waitIdle waits for the busy bit to clear.
func (c *cciConn) 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
}
sleep(5 * time.Millisecond)
}
}
// get returns an attribute by querying the device.
func (c *cciConn) 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 *cciConn) 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 *cciConn) 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.
var sleep = time.Sleep
var _ conn.Resource = &Dev{}
var _ physic.SenseEnv = &Dev{}