lepton: complete the lepton driver overhaul

- New(): Remove cs argument.
- Frame uses image14bit.Gray14 instead of image.Gray16.
- ReadImg(): Removed in favor to NextFrame().
pull/1/head
Marc-Antoine Ruel 8 years ago
parent 3e5b1a8345
commit 458e322fcf

@ -22,16 +22,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"image/color"
"sync" "sync"
"time" "time"
"periph.io/x/periph/conn" "periph.io/x/periph/conn"
"periph.io/x/periph/conn/gpio"
"periph.io/x/periph/conn/i2c" "periph.io/x/periph/conn/i2c"
"periph.io/x/periph/conn/physic" "periph.io/x/periph/conn/physic"
"periph.io/x/periph/conn/spi" "periph.io/x/periph/conn/spi"
"periph.io/x/periph/devices/lepton/cci" "periph.io/x/periph/devices/lepton/cci"
"periph.io/x/periph/devices/lepton/image14bit"
"periph.io/x/periph/devices/lepton/internal" "periph.io/x/periph/devices/lepton/internal"
) )
@ -53,51 +52,29 @@ type Metadata struct {
} }
// Frame is a FLIR Lepton frame, containing 14 bits resolution intensity stored // Frame is a FLIR Lepton frame, containing 14 bits resolution intensity stored
// as image.Gray16. // as image14bit.Gray14.
// //
// Values centered around 8192 accorging to camera body temperature. Effective // Values centered around 8192 accorging to camera body temperature. Effective
// range is 14 bits, so [0, 16383]. // range is 14 bits, so [0, 16383].
// //
// Each 1 increment is approximatively 0.025K. // Each 1 increment is approximatively 0.025K.
type Frame struct { type Frame struct {
*image.Gray16 *image14bit.Gray14
Metadata Metadata // Metadata that is sent along the pixels. Metadata Metadata // Metadata that is sent along the pixels.
} }
// New returns an initialized connection to the FLIR Lepton. // New returns an initialized connection to the FLIR Lepton.
// //
// The CS line is manually managed by using mode spi.NoCS when calling
// Connect(). 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 // Maximum SPI speed is 20Mhz. Minimum usable rate is ~2.2Mhz to sustain a 9hz
// framerate at 80x60. // framerate at 80x60.
// //
// Maximum I²C speed is 1Mhz. // Maximum I²C speed is 1Mhz.
// //
// MOSI is not used and should be grounded. // MOSI is not used and should be grounded.
// func New(p spi.Port, i i2c.Bus) (*Dev, error) {
// Deprecated: Argument cs will be removed in v3.
func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) {
// Sadly the Lepton will unconditionally send 27fps, even if the effective
// rate is 9fps.
mode := spi.Mode3
if cs == nil {
// Query the CS pin before disabling it.
pins, ok := p.(spi.Pins)
if !ok {
return nil, errors.New("lepton: require manual access to the CS pin")
}
cs = pins.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 // TODO(maruel): Switch to 16 bits per word, so that big endian 16 bits word
// decoding is done by the SPI driver. // decoding is done by the SPI driver.
s, err := p.Connect(20000000, mode, 8) s, err := p.Connect(20000000, spi.Mode3, 8)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -114,10 +91,9 @@ func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) {
d := &Dev{ d := &Dev{
Dev: c, Dev: c,
s: s, s: s,
cs: cs,
w: w, w: w,
h: h, h: h,
prevImg: image.NewGray16(image.Rect(0, 0, w, h)), prevImg: image14bit.NewGray14(image.Rect(0, 0, w, h)),
frameWidth: frameWidth, frameWidth: frameWidth,
frameLines: frameLines, frameLines: frameLines,
delay: time.Second, delay: time.Second,
@ -145,10 +121,9 @@ func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) {
type Dev struct { type Dev struct {
*cci.Dev *cci.Dev
s spi.Conn s spi.Conn
cs gpio.PinOut
w int w int
h int h int
prevImg *image.Gray16 prevImg *image14bit.Gray14
frameA, frameB []byte frameA, frameB []byte
frameWidth int // in bytes frameWidth int // in bytes
frameLines int frameLines int
@ -157,7 +132,7 @@ type Dev struct {
} }
func (d *Dev) String() string { func (d *Dev) String() string {
return fmt.Sprintf("Lepton(%s/%s/%s)", d.Dev, d.s, d.cs) return fmt.Sprintf("Lepton(%s/%s)", d.Dev, d.s)
} }
// Halt implements conn.Resource. // Halt implements conn.Resource.
@ -188,7 +163,9 @@ func (d *Dev) NextFrame(f *Frame) error {
// the camera has a shutter. // the camera has a shutter.
//go d.RunFFC() //go d.RunFFC()
} }
if !bytes.Equal(d.prevImg.Pix, f.Gray16.Pix) { // Sadly the Lepton will unconditionally send 27fps, even if the effective
// rate is 9fps.
if !equalUint16(d.prevImg.Pix, f.Gray14.Pix) {
break break
} }
// It also happen if the image is 100% static without noise. // It also happen if the image is 100% static without noise.
@ -197,17 +174,6 @@ func (d *Dev) NextFrame(f *Frame) error {
return nil return nil
} }
// ReadImg reads an image.
//
// Deprecated: Use NextFrame() instead.
func (d *Dev) ReadImg() (*Frame, error) {
f := &Frame{Gray16: image.NewGray16(d.prevImg.Bounds())}
if err := d.NextFrame(f); err != nil {
return nil, err
}
return f, nil
}
// Private details. // Private details.
// stream reads continuously from the SPI connection. // stream reads continuously from the SPI connection.
@ -218,10 +184,6 @@ func (d *Dev) stream(done <-chan struct{}, c chan<- []byte) error {
lines = l lines = l
} }
} }
if err := d.cs.Out(gpio.Low); err != nil {
return err
}
defer d.cs.Out(gpio.High)
for { for {
// TODO(maruel): Use a ring buffer to stop continuously allocating. // TODO(maruel): Use a ring buffer to stop continuously allocating.
buf := make([]byte, d.frameWidth*lines) buf := make([]byte, d.frameWidth*lines)
@ -317,7 +279,7 @@ func (d *Dev) readFrame(f *Frame) error {
// Image. // Image.
for x := 0; x < w; x++ { for x := 0; x < w; x++ {
o := 4 + x*2 o := 4 + x*2
f.SetGray16(x, sync-3, color.Gray16{internal.Big16.Uint16(l[o : o+2])}) f.SetIntensity14(x, sync-3, image14bit.Intensity14(internal.Big16.Uint16(l[o:o+2])))
} }
} }
if sync++; sync == d.frameLines { if sync++; sync == d.frameLines {
@ -484,5 +446,17 @@ func verifyCRC(d []byte) bool {
return internal.CRC16(tmp) == internal.Big16.Uint16(d[2:]) 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{} var _ conn.Resource = &Dev{}
var _ fmt.Stringer = &Dev{} var _ fmt.Stringer = &Dev{}

@ -7,20 +7,17 @@ package lepton
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/hex"
"errors" "errors"
"image" "image"
"image/color"
"testing" "testing"
"periph.io/x/periph/conn" "periph.io/x/periph/conn"
"periph.io/x/periph/conn/conntest" "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/i2c/i2ctest"
"periph.io/x/periph/conn/physic" "periph.io/x/periph/conn/physic"
"periph.io/x/periph/conn/spi" "periph.io/x/periph/conn/spi"
"periph.io/x/periph/conn/spi/spitest" "periph.io/x/periph/conn/spi/spitest"
"periph.io/x/periph/devices/lepton/image14bit"
"periph.io/x/periph/devices/lepton/internal" "periph.io/x/periph/devices/lepton/internal"
) )
@ -35,11 +32,11 @@ func TestNew_cs(t *testing.T) {
}...), }...),
} }
s := spitest.Playback{} s := spitest.Playback{}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if s := d.String(); s != "Lepton(playback(42)/playback/CS(0))" { if s := d.String(); s != "Lepton(playback(42)/playback)" {
t.Fatal(s) t.Fatal(s)
} }
if err := d.Halt(); err != nil { if err := d.Halt(); err != nil {
@ -55,8 +52,8 @@ func TestNew_cs(t *testing.T) {
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} s := spitest.Playback{}
_, err := New(&s, &i, nil) _, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -72,8 +69,8 @@ func TestNew_Init_fail(t *testing.T) {
// Strip off last command. // Strip off last command.
ops := initSequence() ops := initSequence()
i := i2ctest.Playback{Ops: ops[:len(ops)-1], DontPanic: true} i := i2ctest.Playback{Ops: ops[:len(ops)-1], DontPanic: true}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} s := spitest.Playback{}
if _, err := New(&s, &i, nil); err == nil { if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.Init() failed") t.Fatal("cci.Dev.Init() failed")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -95,8 +92,8 @@ func TestNew_GetStatus_fail(t *testing.T) {
}, },
DontPanic: true, DontPanic: true,
} }
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} s := spitest.Playback{}
if _, err := New(&s, &i, nil); err == nil { if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.GetStatus() failed") t.Fatal("cci.Dev.GetStatus() failed")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -119,8 +116,8 @@ func TestNew_GetStatus_bad(t *testing.T) {
}, },
DontPanic: true, DontPanic: true,
} }
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} s := spitest.Playback{}
if _, err := New(&s, &i, nil); err == nil { if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.GetStatus() failed") t.Fatal("cci.Dev.GetStatus() failed")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -131,26 +128,10 @@ func TestNew_GetStatus_bad(t *testing.T) {
} }
} }
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_Connect(t *testing.T) { func TestNew_Connect(t *testing.T) {
i := i2ctest.Record{} i := i2ctest.Record{}
s := spiStream{err: errors.New("injected")} s := spiStream{err: errors.New("injected")}
if _, err := New(&s, &i, &gpiotest.Pin{N: "CS"}); err == nil { if _, err := New(&s, &i); err == nil {
t.Fatal("Connect failed") t.Fatal("Connect failed")
} }
} }
@ -158,15 +139,15 @@ func TestNew_Connect(t *testing.T) {
func TestNew_cci_New_fail(t *testing.T) { func TestNew_cci_New_fail(t *testing.T) {
i := i2ctest.Playback{DontPanic: true} i := i2ctest.Playback{DontPanic: true}
s := spitest.Record{} s := spitest.Record{}
if _, err := New(&s, &i, &gpiotest.Pin{N: "CS"}); err == nil { if _, err := New(&s, &i); err == nil {
t.Fatal("cci.New failed") t.Fatal("cci.New failed")
} }
} }
func TestBounds(t *testing.T) { func TestBounds(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}} s := spitest.Playback{}
d, err := New(&s, &i, nil) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -184,11 +165,11 @@ func TestBounds(t *testing.T) {
func TestNextFrame(t *testing.T) { func TestNextFrame(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spiStream{data: prepareFrame(t)} s := spiStream{data: prepareFrame(t)}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
f := Frame{Gray16: image.NewGray16(d.Bounds())} f := Frame{Gray14: image14bit.NewGray14(d.Bounds())}
if err := d.NextFrame(&f); err != nil { if err := d.NextFrame(&f); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -197,7 +178,7 @@ func TestNextFrame(t *testing.T) {
} }
// Compare the frame with the reference image. It should match. // Compare the frame with the reference image. It should match.
ref := referenceFrame() ref := referenceFrame()
if !bytes.Equal(ref.Pix, f.Pix) { if !equalUint16(ref.Pix, f.Pix) {
offset := 0 offset := 0
for { for {
if ref.Pix[offset] != f.Pix[offset] { if ref.Pix[offset] != f.Pix[offset] {
@ -205,7 +186,8 @@ func TestNextFrame(t *testing.T) {
} }
offset++ offset++
} }
t.Fatalf("different pixels at offset %d:\n%s\n%s", offset, hex.EncodeToString(ref.Pix[offset:]), hex.EncodeToString(f.Pix[offset:])) t.Fatalf("different pixels at offset %d", 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 { if err := i.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
@ -215,11 +197,11 @@ func TestNextFrame(t *testing.T) {
func TestNextFrame_invalid_bounds(t *testing.T) { func TestNextFrame_invalid_bounds(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spiStream{data: prepareFrame(t)} s := spiStream{data: prepareFrame(t)}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := d.NextFrame(&Frame{Gray16: image.NewGray16(image.Rect(0, 0, 1, 1))}); err == nil { if err := d.NextFrame(&Frame{Gray14: image14bit.NewGray14(image.Rect(0, 0, 1, 1))}); err == nil {
t.Fatal("invalid bounds") t.Fatal("invalid bounds")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -230,11 +212,11 @@ func TestNextFrame_invalid_bounds(t *testing.T) {
func TestNextFrame_fail_Tx(t *testing.T) { func TestNextFrame_fail_Tx(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}} s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"}) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := d.NextFrame(&Frame{Gray16: image.NewGray16(d.Bounds())}); err == nil { if err := d.NextFrame(&Frame{Gray14: image14bit.NewGray14(d.Bounds())}); err == nil {
t.Fatal("spi port Tx failed") t.Fatal("spi port Tx failed")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -245,11 +227,11 @@ func TestNextFrame_fail_Tx(t *testing.T) {
func TestNextFrame_fail_Out(t *testing.T) { func TestNextFrame_fail_Out(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()} i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}} s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}}
d, err := New(&s, &i, &failPin{}) d, err := New(&s, &i)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := d.NextFrame(&Frame{Gray16: image.NewGray16(d.Bounds())}); err == nil { if err := d.NextFrame(&Frame{Gray14: image14bit.NewGray14(d.Bounds())}); err == nil {
t.Fatal("spi port Tx failed") t.Fatal("spi port Tx failed")
} }
if err := i.Close(); err != nil { if err := i.Close(); err != nil {
@ -257,28 +239,6 @@ func TestNextFrame_fail_Out(t *testing.T) {
} }
} }
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 != 2*physic.Celsius+physic.ZeroCelsius {
t.Fatal(f.Metadata.TempHousing)
}
if err := i.Close(); err != nil {
t.Fatal(err)
}
if _, err = d.ReadImg(); err == nil {
t.Fatal(err)
}
}
func TestParseTelemetry_fail(t *testing.T) { func TestParseTelemetry_fail(t *testing.T) {
l := telemetryLine(t) l := telemetryLine(t)
m := Metadata{} m := Metadata{}
@ -384,12 +344,12 @@ func appendHeader(t *testing.T, i int, d []byte) []byte {
return out return out
} }
func referenceFrame() *image.Gray16 { func referenceFrame() *image14bit.Gray14 {
r := image.Rect(0, 0, 80, 60) r := image.Rect(0, 0, 80, 60)
img := image.NewGray16(r) img := image14bit.NewGray14(r)
for y := r.Min.Y; y < r.Max.Y; y++ { for y := r.Min.Y; y < r.Max.Y; y++ {
for x := r.Min.X; x < r.Max.X; x++ { for x := r.Min.X; x < r.Max.X; x++ {
img.SetGray16(x, y, color.Gray16{uint16(8192 - 80 + (x * 2))}) img.SetIntensity14(x, y, image14bit.Intensity14(uint16(8192-80+(x*2))))
} }
} }
return img return img
@ -405,7 +365,7 @@ func prepareFrame(t *testing.T) []byte {
r := img.Bounds() r := img.Bounds()
for y := 0; y < r.Max.Y; y++ { for y := 0; y < r.Max.Y; y++ {
for x := 0; x < r.Max.X; x++ { for x := 0; x < r.Max.X; x++ {
internal.Big16.PutUint16(tmp[x*2:], img.Gray16At(x, y).Y) internal.Big16.PutUint16(tmp[x*2:], uint16(img.Intensity14At(x, y)))
} }
buf.Write(appendHeader(t, y+3, tmp)) buf.Write(appendHeader(t, y+3, tmp))
} }
@ -468,11 +428,3 @@ func (s *spiStream) Duplex() conn.Duplex {
func (s *spiStream) MaxTxSize() int { func (s *spiStream) MaxTxSize() int {
return 7 * 164 return 7 * 164
} }
type failPin struct {
gpiotest.Pin
}
func (f *failPin) Out(l gpio.Level) error {
return errors.New("injected")
}

Loading…
Cancel
Save