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"
"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/physic"
"periph.io/x/periph/conn/spi"
"periph.io/x/periph/devices/lepton/cci"
"periph.io/x/periph/devices/lepton/image14bit"
"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
// as image.Gray16.
// 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 {
*image.Gray16
*image14bit.Gray14
Metadata Metadata // Metadata that is sent along the pixels.
}
// 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
// framerate at 80x60.
//
// Maximum I²C speed is 1Mhz.
//
// MOSI is not used and should be grounded.
//
// 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
}
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(20000000, mode, 8)
s, err := p.Connect(20000000, spi.Mode3, 8)
if err != nil {
return nil, err
}
@ -114,10 +91,9 @@ func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) {
d := &Dev{
Dev: c,
s: s,
cs: cs,
w: w,
h: h,
prevImg: image.NewGray16(image.Rect(0, 0, w, h)),
prevImg: image14bit.NewGray14(image.Rect(0, 0, w, h)),
frameWidth: frameWidth,
frameLines: frameLines,
delay: time.Second,
@ -145,10 +121,9 @@ func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) {
type Dev struct {
*cci.Dev
s spi.Conn
cs gpio.PinOut
w int
h int
prevImg *image.Gray16
prevImg *image14bit.Gray14
frameA, frameB []byte
frameWidth int // in bytes
frameLines int
@ -157,7 +132,7 @@ type Dev struct {
}
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.
@ -188,7 +163,9 @@ func (d *Dev) NextFrame(f *Frame) error {
// the camera has a shutter.
//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
}
// It also happen if the image is 100% static without noise.
@ -197,17 +174,6 @@ func (d *Dev) NextFrame(f *Frame) error {
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.
// stream reads continuously from the SPI connection.
@ -218,10 +184,6 @@ func (d *Dev) stream(done <-chan struct{}, c chan<- []byte) error {
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)
@ -317,7 +279,7 @@ func (d *Dev) readFrame(f *Frame) error {
// 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])})
f.SetIntensity14(x, sync-3, image14bit.Intensity14(internal.Big16.Uint16(l[o:o+2])))
}
}
if sync++; sync == d.frameLines {
@ -484,5 +446,17 @@ func verifyCRC(d []byte) bool {
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 _ fmt.Stringer = &Dev{}

@ -7,20 +7,17 @@ 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/physic"
"periph.io/x/periph/conn/spi"
"periph.io/x/periph/conn/spi/spitest"
"periph.io/x/periph/devices/lepton/image14bit"
"periph.io/x/periph/devices/lepton/internal"
)
@ -35,11 +32,11 @@ func TestNew_cs(t *testing.T) {
}...),
}
s := spitest.Playback{}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"})
d, err := New(&s, &i)
if err != nil {
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)
}
if err := d.Halt(); err != nil {
@ -55,8 +52,8 @@ func TestNew_cs(t *testing.T) {
func TestNew(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}}
_, err := New(&s, &i, nil)
s := spitest.Playback{}
_, err := New(&s, &i)
if err != nil {
t.Fatal(err)
}
@ -72,8 +69,8 @@ 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 {
s := spitest.Playback{}
if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.Init() failed")
}
if err := i.Close(); err != nil {
@ -95,8 +92,8 @@ func TestNew_GetStatus_fail(t *testing.T) {
},
DontPanic: true,
}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}}
if _, err := New(&s, &i, nil); err == nil {
s := spitest.Playback{}
if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.GetStatus() failed")
}
if err := i.Close(); err != nil {
@ -119,8 +116,8 @@ func TestNew_GetStatus_bad(t *testing.T) {
},
DontPanic: true,
}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}}
if _, err := New(&s, &i, nil); err == nil {
s := spitest.Playback{}
if _, err := New(&s, &i); err == nil {
t.Fatal("cci.Dev.GetStatus() failed")
}
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) {
i := i2ctest.Record{}
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")
}
}
@ -158,15 +139,15 @@ func TestNew_Connect(t *testing.T) {
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 {
if _, err := New(&s, &i); err == nil {
t.Fatal("cci.New failed")
}
}
func TestBounds(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{CSPin: &gpiotest.Pin{N: "CS"}}
d, err := New(&s, &i, nil)
s := spitest.Playback{}
d, err := New(&s, &i)
if err != nil {
t.Fatal(err)
}
@ -184,11 +165,11 @@ func TestBounds(t *testing.T) {
func TestNextFrame(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()}
s := spiStream{data: prepareFrame(t)}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"})
d, err := New(&s, &i)
if err != nil {
t.Fatal(err)
}
f := Frame{Gray16: image.NewGray16(d.Bounds())}
f := Frame{Gray14: image14bit.NewGray14(d.Bounds())}
if err := d.NextFrame(&f); err != nil {
t.Fatal(err)
}
@ -197,7 +178,7 @@ func TestNextFrame(t *testing.T) {
}
// Compare the frame with the reference image. It should match.
ref := referenceFrame()
if !bytes.Equal(ref.Pix, f.Pix) {
if !equalUint16(ref.Pix, f.Pix) {
offset := 0
for {
if ref.Pix[offset] != f.Pix[offset] {
@ -205,7 +186,8 @@ func TestNextFrame(t *testing.T) {
}
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 {
t.Fatal(err)
@ -215,11 +197,11 @@ func TestNextFrame(t *testing.T) {
func TestNextFrame_invalid_bounds(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()}
s := spiStream{data: prepareFrame(t)}
d, err := New(&s, &i, &gpiotest.Pin{N: "CS"})
d, err := New(&s, &i)
if err != nil {
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")
}
if err := i.Close(); err != nil {
@ -230,11 +212,11 @@ func TestNextFrame_invalid_bounds(t *testing.T) {
func TestNextFrame_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"})
d, err := New(&s, &i)
if err != nil {
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")
}
if err := i.Close(); err != nil {
@ -245,11 +227,11 @@ func TestNextFrame_fail_Tx(t *testing.T) {
func TestNextFrame_fail_Out(t *testing.T) {
i := i2ctest.Playback{Ops: initSequence()}
s := spitest.Playback{Playback: conntest.Playback{DontPanic: true}}
d, err := New(&s, &i, &failPin{})
d, err := New(&s, &i)
if err != nil {
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")
}
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) {
l := telemetryLine(t)
m := Metadata{}
@ -384,12 +344,12 @@ func appendHeader(t *testing.T, i int, d []byte) []byte {
return out
}
func referenceFrame() *image.Gray16 {
func referenceFrame() *image14bit.Gray14 {
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 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
@ -405,7 +365,7 @@ func prepareFrame(t *testing.T) []byte {
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)
internal.Big16.PutUint16(tmp[x*2:], uint16(img.Intensity14At(x, y)))
}
buf.Write(appendHeader(t, y+3, tmp))
}
@ -468,11 +428,3 @@ func (s *spiStream) Duplex() conn.Duplex {
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…
Cancel
Save