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/lepton.go

451 lines
14 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.
package lepton
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"image"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
"periph.io/x/devices/v3/lepton/cci"
"periph.io/x/devices/v3/lepton/image14bit"
"periph.io/x/devices/v3/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 physic.Temperature // Temperature inside the camera.
TempHousing physic.Temperature // Camera housing temperature.
RawTemp uint16 //
RawTempHousing uint16 //
FFCSince time.Duration // Time since last internal calibration.
FFCTemp physic.Temperature // Temperature at last internal calibration.
FFCTempHousing physic.Temperature //
FFCState cci.FFCState // Current calibration state.
FFCDesired bool // Asserted at start-up, after period (default 3m) or after temperature change (default 3K). 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 image14bit.Gray14.
//
// Values centered around 8192 accorging to camera body temperature. Effective
// range is 14 bits, so [0, 16383].
//
// Each 1 increment is approximatively 0.025K.
type Frame struct {
*image14bit.Gray14
Metadata Metadata // Metadata that is sent along the pixels.
}
// New returns an initialized connection to the FLIR Lepton.
//
// 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(p spi.Port, i i2c.Bus) (*Dev, error) {
// TODO(maruel): Switch to 16 bits per word, so that big endian 16 bits word
// decoding is done by the SPI driver.
s, err := p.Connect(20*physic.MegaHertz, spi.Mode3, 8)
if 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,
w: w,
h: h,
prevImg: image14bit.NewGray14(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: %#v", status)
}
if err := d.Init(); err != nil {
return nil, err
}
return d, nil
}
// 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
w int
h int
prevImg *image14bit.Gray14
frameWidth int // in bytes
frameLines int
maxTxSize int
delay time.Duration
}
func (d *Dev) String() string {
return fmt.Sprintf("Lepton(%s/%s)", d.Dev, d.s)
}
// Halt implements conn.Resource.
func (d *Dev) Halt() error {
// TODO(maruel): Stop the read loop.
return d.Dev.Halt()
}
// Bounds returns the device frame size.
func (d *Dev) Bounds() image.Rectangle {
return image.Rect(0, 0, d.w, d.h)
}
// NextFrame blocks and returns the next frame from the camera.
//
// It is ok to call other functions concurrently to send commands to the
// camera.
func (d *Dev) NextFrame(f *Frame) error {
if f.Bounds() != d.Bounds() {
return errors.New("lepton: invalid frame size")
}
for {
if err := d.readFrame(f); err != nil {
return err
}
/*
if f.Metadata.FFCDesired {
// TODO(maruel): Automatically trigger FFC when applicable, only do if
// the camera has a shutter.
go d.RunFFC()
}
*/
// Sadly the Lepton will unconditionally send 27fps, even if the effective
// rate is 9fps.
if !equalUint16(d.prevImg.Pix, f.Gray14.Pix) {
break
}
// It also happen if the image is 100% static without noise.
}
copy(d.prevImg.Pix, f.Pix)
return 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
}
}
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.SetIntensity14(x, sync-3, image14bit.Intensity14(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.Duration()
m.FrameCount = rowA.FrameCounter
m.AvgValue = rowA.FrameMean
m.Temp = rowA.FPATemp.Temperature()
m.TempHousing = rowA.HousingTemp.Temperature()
m.RawTemp = rowA.FPATempCounts
m.RawTempHousing = rowA.HousingTempCounts
m.FFCSince = rowA.TimeCounterLastFFC.Duration()
m.FFCTemp = rowA.FPATempLastFFC.Temperature()
m.FFCTempHousing = rowA.HousingTempLastFFC.Temperature()
if rowA.StatusBits&statusMaskNil != 0 {
return fmt.Errorf("lepton: (Status: 0x%08X) & (Mask: 0x%08X) = (Extra: 0x%08X) in 0x%08X", 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:])
}
func equalUint16(a, b []uint16) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
var _ conn.Resource = &Dev{}