From ea63dac2c6fe2e3cec5e000b54954f54f6c3dc51 Mon Sep 17 00:00:00 2001 From: M-A Date: Sun, 20 Aug 2017 14:40:37 -0400 Subject: [PATCH] devices: Add Environmental.SenseContinuous(); implement for bme280 (#150) - devices: Add SenseContinuous() to Environmental. - devices: Add Device to Display and Environmental. - devices: Remove fmt.Stringer from Device. - devices: Add more type assertions to variaous devices and corresponding unit test. - bme280: Unexport the Standby parameter. - bme280: Have SenseContinuous accept time.Duration period instead. - bme280: Do not enable automatic sensing upon initialization. - bme280: Use forced mode by default. - bme280smoketest: improved. --- devices/apa102/apa102.go | 1 + devices/bme280/bme280.go | 412 +++++++++++++----- devices/bme280/bme280_test.go | 333 +++++++++++++- .../bme280/bme280smoketest/bme280smoketest.go | 32 +- devices/devices.go | 13 +- devices/devicestest/display.go | 11 + devices/ds18b20/ds18b20.go | 1 + devices/ds248x/dev.go | 1 + devices/lepton/cci/cci.go | 12 + devices/lepton/cci/cci_test.go | 5 +- devices/lepton/lepton.go | 7 + devices/lepton/lepton_test.go | 3 + devices/lirc/lirc.go | 5 + devices/ssd1306/ssd1306.go | 1 + devices/tm1637/tm1637.go | 1 + experimental/devices/bitbang/i2c.go | 1 + experimental/devices/bitbang/spi.go | 1 + experimental/devices/bmp180/bmp180.go | 7 + 18 files changed, 723 insertions(+), 124 deletions(-) diff --git a/devices/apa102/apa102.go b/devices/apa102/apa102.go index 69f0f0e..aee6e3d 100644 --- a/devices/apa102/apa102.go +++ b/devices/apa102/apa102.go @@ -322,3 +322,4 @@ var errLength = errors.New("apa102: invalid RGB stream length") var _ devices.Display = &Dev{} var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/bme280/bme280.go b/devices/bme280/bme280.go index 87fb11c..e89d2cb 100644 --- a/devices/bme280/bme280.go +++ b/devices/bme280/bme280.go @@ -12,6 +12,9 @@ package bme280 import ( "errors" "fmt" + "log" + "sync" + "time" "periph.io/x/periph/conn" "periph.io/x/periph/conn/i2c" @@ -31,8 +34,12 @@ import ( type Oversampling uint8 // Possible oversampling values. +// +// The higher the more time and power it takes to take a measurement. Even at +// 16x for all 3 sensor, it is less than 100ms albeit increased power +// consumption may increase the temperature reading. const ( - No Oversampling = 0 + Off Oversampling = 0 O1x Oversampling = 1 O2x Oversampling = 2 O4x Oversampling = 3 @@ -40,104 +47,199 @@ const ( O16x Oversampling = 5 ) -// Standby is the time the BME280 waits idle between measurements. This reduces -// power consumption when the host won't read the values as fast as the -// measurements are done. -type Standby uint8 +const oversamplingName = "Off1x2x4x8x16x" -// Possible standby values, these determines the refresh rate. -const ( - S500us Standby = 0 - S10ms Standby = 6 - S20ms Standby = 7 - S62ms Standby = 1 - S125ms Standby = 2 - S250ms Standby = 3 - S500ms Standby = 4 - S1s Standby = 5 -) +var oversamplingIndex = [...]uint8{0, 3, 5, 7, 9, 11, 14} + +func (i Oversampling) String() string { + if i >= Oversampling(len(oversamplingIndex)-1) { + return fmt.Sprintf("Oversampling(%d)", i) + } + return oversamplingName[oversamplingIndex[i]:oversamplingIndex[i+1]] +} + +func (o Oversampling) asValue() int { + switch o { + case O1x: + return 1 + case O2x: + return 2 + case O4x: + return 4 + case O8x: + return 8 + case O16x: + return 16 + default: + return 0 + } +} -// Filter specifies the internal IIR filter to get steady measurements without -// using oversampling. This is mainly used to reduce power consumption. +// Filter specifies the internal IIR filter to get steadier measurements. +// +// Oversampling will get better measurements than filtering but at a larger +// power consumption cost, which may slightly affect temperature measurement. type Filter uint8 // Possible filtering values. +// +// The higher the filter, the slower the value converges but the more stable +// the measurement is. const ( - FOff Filter = 0 - F2 Filter = 1 - F4 Filter = 2 - F8 Filter = 3 - F16 Filter = 4 + NoFilter Filter = 0 + F2 Filter = 1 + F4 Filter = 2 + F8 Filter = 3 + F16 Filter = 4 ) -// Dev is an handle to a bme280. +// Dev is a handle to an initialized bme280. type Dev struct { - d conn.Conn - isSPI bool - c calibration + d conn.Conn + isSPI bool + opts Opts + measDelay time.Duration + c calibration + + mu sync.Mutex + stop chan struct{} } func (d *Dev) String() string { return fmt.Sprintf("BME280{%s}", d.d) } -// Sense returns measurements as °C, kPa and % of relative humidity. +// Sense requests a one time measurement as °C, kPa and % of relative humidity. +// +// The very first measurements may be of poor quality. func (d *Dev) Sense(env *devices.Environment) error { - // All registers must be read in a single pass, as noted at page 21, section - // 4.1. - // Pressure: 0xF7~0xF9 - // Temperature: 0xFA~0xFC - // Humidity: 0xFD~0xFE - buf := [0xFF - 0xF7]byte{} - if err := d.readReg(0xF7, buf[:]); err != nil { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop != nil { + return errors.New("bme280: already sensing continuously") + } + err := d.writeCommands([]byte{ + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(forced), + }) + if err != nil { return err } - // These values are 20 bits as per doc. - pRaw := int32(buf[0])<<12 | int32(buf[1])<<4 | int32(buf[2])>>4 - tRaw := int32(buf[3])<<12 | int32(buf[4])<<4 | int32(buf[5])>>4 - // This value is 16 bits as per doc. - hRaw := int32(buf[6])<<8 | int32(buf[7]) - - t, tFine := d.c.compensateTempInt(tRaw) - env.Temperature = devices.Celsius(t * 10) - - p := d.c.compensatePressureInt64(pRaw, tFine) - env.Pressure = devices.KPascal((int32(p) + 127) / 256) + time.Sleep(d.measDelay) + for idle := false; !idle; { + if idle, err = d.isIdle(); err != nil { + return err + } + } + return d.sense(env) +} - h := d.c.compensateHumidityInt(hRaw, tFine) - env.Humidity = devices.RelativeHumidity((int32(h)*100 + 511) / 1024) - return nil +// SenseContinuous returns measurements as °C, kPa and % of relative humidity +// on a continuous basis. +// +// The application must call Halt() to stop the sensing when done to stop the +// sensor and close the channel. +// +// It's the responsibility of the caller to retrieve the values from the +// channel as fast as possible, otherwise the interval may not be respected. +func (d *Dev) SenseContinuous(interval time.Duration) (<-chan devices.Environment, error) { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop != nil { + // Don't send the stop command to the device. + close(d.stop) + d.stop = nil + } + s := chooseStandby(interval - d.measDelay) + err := d.writeCommands([]byte{ + // config + 0xF5, byte(s)<<5 | byte(d.opts.Filter)<<2, + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(normal), + }) + if err != nil { + return nil, err + } + sensing := make(chan devices.Environment) + d.stop = make(chan struct{}) + go func() { + defer close(sensing) + d.sensingContinuous(interval, sensing, d.stop) + }() + return sensing, nil } -// Halt stops the bme280 from acquiring measurements. +// Halt stops the bme280 from acquiring measurements as initiated by Sense(). // -// It is recommended to call to reduce idle power usage. +// It is recommended to call this function before terminating the process to +// reduce idle power usage. func (d *Dev) Halt() error { + d.mu.Lock() + defer d.mu.Unlock() + if d.stop == nil { + return nil + } + close(d.stop) + d.stop = nil // Page 27 (for register) and 12~13 section 3.3. - return d.writeCommands([]byte{0xF4, byte(sleep)}) + return d.writeCommands([]byte{ + // config + 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + }) } // Opts is optional options to pass to the constructor. // -// Recommended (and default) values are O4x for oversampling, S20ms for standby -// and FOff for filter if planing to call frequently, else use S500ms to get a -// bit more than one reading per second. +// Recommended (and default) values are O4x for oversampling. +// +// Address can only used on creation of an I²C-device. Its default value is +// 0x76. It can be set to 0x77. Both values depend on HW configuration of the +// sensor's SDO pin. +// +// Filter is only used while using SenseContinuous(). +// +// Recommended sensing settings as per the datasheet: +// +// → Weather monitoring: manual sampling once per minute, all sensors O1x. +// Power consumption: 0.16µA, filter NoFilter. RMS noise: 3.3Pa / 30cm, 0.07%RH. +// +// → Humidity sensing: manual sampling once per second, pressure Off, humidity +// and temperature O1X, filter NoFilter. Power consumption: 2.9µA, 0.07%RH. // -// Address is only used on creation of an I²C-device. Its default value is 0x76. -// It can be set to 0x77. Both values depend on HW configuration of the sensor's -// SDO pin. This has no effect with NewSPI() +// → Indoor navigation: continuous sampling at 40ms with filter F16, pressure +// O16x, temperature O2x, humidity O1x, filter F16. Power consumption 633µA. +// RMS noise: 0.2Pa / 1.7cm. // -// BUG(maruel): Remove the Standby flag and replace with a -// WaitForNextSample(time.Duration). Then use the closest value automatically. +// → Gaming: continuous sampling at 40ms with filter F16, pressure O4x, +// temperature O1x, humidity Off, filter F16. Power consumption 581µA. RMS +// noise: 0.3Pa / 2.5cm. +// +// See the datasheet for more details about the trade offs. type Opts struct { Temperature Oversampling Pressure Oversampling Humidity Oversampling - Standby Standby Filter Filter Address uint16 } +func (o *Opts) delayTypical() time.Duration { + // Page 51. + µs := 1000 + if o.Temperature != Off { + µs += 2000 * o.Temperature.asValue() + } + if o.Pressure != Off { + µs += 2000*o.Pressure.asValue() + 500 + } + if o.Humidity != Off { + µs += 2000*o.Humidity.asValue() + 500 + } + return time.Microsecond * time.Duration(µs) +} + // NewI2C returns an object that communicates over I²C to BME280 environmental // sensor. // @@ -165,15 +267,14 @@ func NewI2C(b i2c.Bus, opts *Opts) (*Dev, error) { // NewSPI returns an object that communicates over SPI to BME280 environmental // sensor. // -// Recommended values are O4x for oversampling, S20ms for standby and FOff for -// filter if planing to call frequently, else use S500ms to get a bit more than -// one reading per second. -// // It is recommended to call Halt() when done with the device so it stops // sampling. // // When using SPI, the CS line must be used. func NewSPI(p spi.Port, opts *Opts) (*Dev, error) { + if opts != nil && opts.Address != 0 { + return nil, errors.New("bme280: do not use Address in SPI") + } // It works both in Mode0 and Mode3. c, err := p.Connect(10000000, spi.Mode3, 8) if err != nil { @@ -188,44 +289,15 @@ func NewSPI(p spi.Port, opts *Opts) (*Dev, error) { // -// mode is stored in config -type mode byte - -const ( - sleep mode = 0 // no operation, all registers accessible, lowest power, selected after startup - forced mode = 1 // perform one measurement, store results and return to sleep mode - normal mode = 3 // perpetual cycling of measurements and inactive periods -) - -type status byte - -const ( - measuring status = 8 // set when conversion is running - imUpdate status = 1 // set when NVM data are being copied to image registers -) - -var defaults = Opts{ - Temperature: O4x, - Pressure: O4x, - Humidity: O4x, - Standby: S20ms, - Filter: FOff, -} - func (d *Dev) makeDev(opts *Opts) error { if opts == nil { opts = &defaults } - config := []byte{ - // ctrl_meas; put it to sleep otherwise the config update may be ignored. - 0xF4, byte(opts.Temperature)<<5 | byte(opts.Pressure)<<2 | byte(sleep), - // ctrl_hum - 0xF2, byte(opts.Humidity), - // config - 0xF5, byte(opts.Standby)<<5 | byte(opts.Filter)<<2, - // ctrl_meas - 0xF4, byte(opts.Temperature)<<5 | byte(opts.Pressure)<<2 | byte(normal), + if opts.Temperature == Off { + return errors.New("temperature measurement is required, use at least O1x") } + d.opts = *opts + d.measDelay = d.opts.delayTypical() // The device starts in 2ms as per datasheet. No need to wait for boot to be // finished. @@ -238,6 +310,8 @@ func (d *Dev) makeDev(opts *Opts) error { if chipID[0] != 0x60 { return fmt.Errorf("bme280: unexpected chip id %x; is this a BME280?", chipID[0]) } + + // TODO(maruel): We may want to wait for isIdle(). // Read calibration data t1~3, p1~9, 8bits padding, h1. var tph [0xA2 - 0x88]byte if err := d.readReg(0x88, tph[:]); err != nil { @@ -248,15 +322,91 @@ func (d *Dev) makeDev(opts *Opts) error { if err := d.readReg(0xE1, h[:]); err != nil { return err } + d.c = newCalibration(tph[:], h[:]) + + config := []byte{ + // ctrl_meas; put it to sleep otherwise the config update may be ignored. + // This is really just in case the device was somehow put into normal but + // was not Halt'ed. + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + // ctrl_hum + 0xF2, byte(opts.Humidity), + // config + 0xF5, byte(s1s)<<5 | byte(NoFilter)<<2, + // As per page 25, ctrl_meas must be re-written last. + // ctrl_meas + 0xF4, byte(d.opts.Temperature)<<5 | byte(d.opts.Pressure)<<2 | byte(sleep), + } if err := d.writeCommands(config[:]); err != nil { return err } + return nil +} - d.c = newCalibration(tph[:], h[:]) +// sense reads the device's registers. +// +// It must be called with d.mu lock held. +func (d *Dev) sense(env *devices.Environment) error { + // All registers must be read in a single pass, as noted at page 21, section + // 4.1. + // Pressure: 0xF7~0xF9 + // Temperature: 0xFA~0xFC + // Humidity: 0xFD~0xFE + buf := [0xFF - 0xF7]byte{} + if err := d.readReg(0xF7, buf[:]); err != nil { + return err + } + // These values are 20 bits as per doc. + pRaw := int32(buf[0])<<12 | int32(buf[1])<<4 | int32(buf[2])>>4 + tRaw := int32(buf[3])<<12 | int32(buf[4])<<4 | int32(buf[5])>>4 + // This value is 16 bits as per doc. + hRaw := int32(buf[6])<<8 | int32(buf[7]) + t, tFine := d.c.compensateTempInt(tRaw) + env.Temperature = devices.Celsius(t * 10) + + p := d.c.compensatePressureInt64(pRaw, tFine) + env.Pressure = devices.KPascal((int32(p) + 127) / 256) + + h := d.c.compensateHumidityInt(hRaw, tFine) + env.Humidity = devices.RelativeHumidity((int32(h)*100 + 511) / 1024) return nil } +func (d *Dev) sensingContinuous(interval time.Duration, sensing chan<- devices.Environment, stop <-chan struct{}) { + t := time.NewTicker(interval) + defer t.Stop() + + for { + // Do one initial sensing right away. + var e devices.Environment + d.mu.Lock() + err := d.sense(&e) + d.mu.Unlock() + if err != nil { + log.Printf("bme280: failed to sense: %v", err) + return + } + sensing <- e + + select { + case <-stop: + return + case <-t.C: + } + } +} + +func (d *Dev) isIdle() (bool, error) { + // status + v := [1]byte{} + if err := d.readReg(0xF3, v[:]); err != nil { + return false, err + } + // Make sure bit 3 is cleared. Bit 0 is only important at device boot up. + return v[0]&8 == 0, nil +} + func (d *Dev) readReg(reg uint8, b []byte) error { // Page 32-33 if d.isSPI { @@ -287,6 +437,69 @@ func (d *Dev) writeCommands(b []byte) error { return d.d.Tx(b, nil) } +// + +// mode is the operating mode. +type mode byte + +const ( + sleep mode = 0 // no operation, all registers accessible, lowest power, selected after startup + forced mode = 1 // perform one measurement, store results and return to sleep mode + normal mode = 3 // perpetual cycling of measurements and inactive periods +) + +type status byte + +const ( + measuring status = 8 // set when conversion is running + imUpdate status = 1 // set when NVM data are being copied to image registers +) + +var defaults = Opts{ + Temperature: O4x, + Pressure: O4x, + Humidity: O4x, + Address: 0x76, +} + +// standby is the time the BME280 waits idle between measurements. This reduces +// power consumption when the host won't read the values as fast as the +// measurements are done. +type standby uint8 + +// Possible standby values, these determines the refresh rate. +const ( + s500us standby = 0 + s10ms standby = 6 + s20ms standby = 7 + s62ms standby = 1 + s125ms standby = 2 + s250ms standby = 3 + s500ms standby = 4 + s1s standby = 5 +) + +func chooseStandby(d time.Duration) standby { + switch { + case d < 10*time.Millisecond: + return s500us + case d < 20*time.Millisecond: + return s10ms + case d < 62500*time.Microsecond: + return s20ms + case d < 125*time.Millisecond: + return s62ms + case d < 250*time.Millisecond: + return s125ms + case d < 500*time.Millisecond: + return s250ms + case d < time.Second: + return s500ms + default: + return s1s + } +} + // Register table: // 0x00..0x87 -- // 0x88..0xA1 Calibration data @@ -409,3 +622,4 @@ func (c *calibration) compensateHumidityInt(raw, tFine int32) uint32 { var _ devices.Environmental = &Dev{} var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/bme280/bme280_test.go b/devices/bme280/bme280_test.go index 91f8720..180024f 100644 --- a/devices/bme280/bme280_test.go +++ b/devices/bme280/bme280_test.go @@ -7,8 +7,11 @@ package bme280 import ( "errors" "fmt" + "io/ioutil" "log" + "os" "testing" + "time" "periph.io/x/periph/conn/conntest" "periph.io/x/periph/conn/i2c/i2creg" @@ -59,8 +62,13 @@ func TestSPISense_success(t *testing.T) { W: []byte{0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, R: []byte{0x00, 0x5C, 0x01, 0x00, 0x15, 0x0F, 0x00, 0x1E}, }, - {W: []byte{0x74, 0xB4, 0x72, 0x05, 0x75, 0xA0, 0x74, 0xB7}}, - // R. + // Config. + {W: []byte{0x74, 0xB4, 0x72, 0x05, 0x75, 0xA0, 0x74, 0xB4}}, + // Forced mode. + {W: []byte{0x74, 0xB5}}, + // Check if idle. + {W: []byte{0xF3, 0x00}, R: []byte{0, 0}}, + // Read measurement data. { W: []byte{0xF7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, R: []byte{0x00, 0x51, 0x9F, 0xC0, 0x9E, 0x3A, 0x50, 0x5E, 0x5B}, @@ -72,8 +80,6 @@ func TestSPISense_success(t *testing.T) { Temperature: O16x, Pressure: O16x, Humidity: O16x, - Standby: S1s, - Filter: FOff, } dev, err := NewSPI(&s, &opts) if err != nil { @@ -107,6 +113,9 @@ func TestNewSPI_fail(t *testing.T) { if d, err := NewSPI(&spiFail{}, nil); d != nil || err == nil { t.Fatal("Connect() have failed") } + if d, err := NewSPI(&spiFail{}, &Opts{Address: 1}); d != nil || err == nil { + t.Fatal("Address can't be used with SPI") + } } func TestNewSPI_fail_len(t *testing.T) { @@ -200,7 +209,7 @@ func TestNewI2C_calib1(t *testing.T) { }, DontPanic: true, } - opts := Opts{Address: 0} + opts := Opts{Temperature: O1x, Address: 0} if dev, err := NewI2C(&bus, &opts); dev != nil || err == nil { t.Fatal("2nd calib read failed") } @@ -256,7 +265,6 @@ func TestI2COpts(t *testing.T) { } func TestI2CSense_fail(t *testing.T) { - // This data was generated with "bme280 -r" bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chipd ID detection. @@ -270,9 +278,10 @@ func TestI2CSense_fail(t *testing.T) { // Calibration data. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. - {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xe0, 0xf4, 0x6f}, R: nil}, - // Read. - {Addr: 0x76, W: []byte{0xf7}}, + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF4, 0xB5}}, + // Check if idle fails. }, DontPanic: true, } @@ -291,7 +300,6 @@ func TestI2CSense_fail(t *testing.T) { } func TestI2CSense_success(t *testing.T) { - // This data was generated with "bme280 -r" bus := i2ctest.Playback{ Ops: []i2ctest.IO{ // Chipd ID detection. @@ -305,11 +313,15 @@ func TestI2CSense_success(t *testing.T) { // Calibration data. {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, // Configuration. - {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xe0, 0xf4, 0x6f}, R: nil}, + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF4, 0x6d}}, + // Check if idle; not idle. + {Addr: 0x76, W: []byte{0xF3}, R: []byte{8}}, + // Check if idle. + {Addr: 0x76, W: []byte{0xF3}, R: []byte{0}}, // Read. {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, - // Halt. - {Addr: 0x76, W: []byte{0xf4, 0x0}}, }, } dev, err := NewI2C(&bus, nil) @@ -340,6 +352,254 @@ func TestI2CSense_success(t *testing.T) { } } +func TestI2CSense_idle_fail(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data. + {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF4, 0x6d}}, + // Check if idle fails. + }, + DontPanic: true, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + if s := dev.String(); s != "BME280{playback(118)}" { + t.Fatal(s) + } + env := devices.Environment{} + if dev.Sense(&env) == nil { + t.Fatal("isIdle() should have failed") + } +} + +func TestI2CSense_command_fail(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data. + {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF4, 0x6d}}, + // Check if idle. + {Addr: 0x76, W: []byte{0xF3}, R: []byte{0}}, + // Read fail. + }, + DontPanic: true, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + if s := dev.String(); s != "BME280{playback(118)}" { + t.Fatal(s) + } + env := devices.Environment{} + if dev.Sense(&env) == nil { + t.Fatal("isIdle() should have failed") + } +} + +func TestI2CSenseContinuous_success(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data. + {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Normal mode. + {Addr: 0x76, W: []byte{0xF5, 0xa0, 0xf4, 0x6f}}, + // Read. + {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, + // Normal mode. + {Addr: 0x76, W: []byte{0xF5, 0, 0xf4, 0x6f}}, + // Read. + {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, + // Read. + {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, + // Read. + {Addr: 0x76, W: []byte{0xf7}, R: []byte{0x4a, 0x52, 0xc0, 0x80, 0x96, 0xc0, 0x7a, 0x76}}, + // Forced mode. + {Addr: 0x76, W: []byte{0xF5, 0xa0, 0xf4, 0x6c}}, + }, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + c, err := dev.SenseContinuous(time.Minute) + if err != nil { + t.Fatal(err) + } + env := devices.Environment{} + select { + case env = <-c: + case <-time.After(2 * time.Second): + t.Fatal("failed") + } + if env.Temperature != 23720 { + t.Fatalf("temp %d", env.Temperature) + } + if env.Pressure != 100943 { + t.Fatalf("pressure %d", env.Pressure) + } + if env.Humidity != 6531 { + t.Fatalf("humidity %d", env.Humidity) + } + + // This cancels the previous channel and resets the interval. + c2, err := dev.SenseContinuous(time.Nanosecond) + if err != nil { + t.Fatal(err) + } + + if _, ok := <-c; ok { + t.Fatal("c should be closed") + } + select { + case env = <-c2: + case <-time.After(2 * time.Second): + t.Fatal("failed") + } + select { + case env = <-c2: + case <-time.After(2 * time.Second): + t.Fatal("failed") + } + if env.Temperature != 23720 { + t.Fatalf("temp %d", env.Temperature) + } + if env.Pressure != 100943 { + t.Fatalf("pressure %d", env.Pressure) + } + if env.Humidity != 6531 { + t.Fatalf("humidity %d", env.Humidity) + } + + if dev.Sense(&env) == nil { + t.Fatal("can't Sense() during SenseContinously") + } + + // Inspect to make sure senseContinuous() had the time to do its things. This + // is a bit sad but it is the simplest way to make this test deterministic. + for { + bus.Lock() + count := bus.Count + bus.Unlock() + if count == 10 { + break + } + } + + if err := dev.Halt(); err != nil { + t.Fatal(err) + } + if err := bus.Close(); err != nil { + t.Fatal(err) + } +} + +func TestI2CSenseContinuous_command_fail(t *testing.T) { + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data. + {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Normal mode fails. + }, + DontPanic: true, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + if _, err := dev.SenseContinuous(time.Minute); err == nil { + t.Fatal("send command should have failed") + } +} + +func TestI2CSenseContinuous_sense_fail(t *testing.T) { + if !testing.Verbose() { + log.SetOutput(ioutil.Discard) + defer log.SetOutput(os.Stderr) + } + bus := i2ctest.Playback{ + Ops: []i2ctest.IO{ + // Chipd ID detection. + {Addr: 0x76, W: []byte{0xd0}, R: []byte{0x60}}, + // Calibration data. + { + Addr: 0x76, + W: []byte{0x88}, + R: []byte{0x10, 0x6e, 0x6c, 0x66, 0x32, 0x0, 0x5d, 0x95, 0xb8, 0xd5, 0xd0, 0xb, 0x77, 0x1e, 0x9d, 0xff, 0xf9, 0xff, 0xac, 0x26, 0xa, 0xd8, 0xbd, 0x10, 0x0, 0x4b}, + }, + // Calibration data. + {Addr: 0x76, W: []byte{0xe1}, R: []byte{0x6e, 0x1, 0x0, 0x13, 0x5, 0x0, 0x1e}}, + // Configuration. + {Addr: 0x76, W: []byte{0xf4, 0x6c, 0xf2, 0x3, 0xf5, 0xa0, 0xf4, 0x6c}, R: nil}, + // Normal mode. + {Addr: 0x76, W: []byte{0xF5, 0xa0, 0xf4, 0x6f}}, + // Read fail. + }, + DontPanic: true, + } + dev, err := NewI2C(&bus, nil) + if err != nil { + t.Fatal(err) + } + c, err := dev.SenseContinuous(time.Minute) + if err != nil { + t.Fatal(err) + } + select { + case _, ok := <-c: + if ok { + t.Fatal("expecting channel to be closed") + } + case <-time.After(2 * time.Second): + t.Fatal("failed") + } +} + func TestCalibrationFloat(t *testing.T) { // Real data extracted from measurements from this device. tRaw := int32(524112) @@ -432,6 +692,53 @@ func Example() { fmt.Printf("%8s %10s %9s\n", env.Temperature, env.Pressure, env.Humidity) } +func TestOversampling(t *testing.T) { + data := []struct { + o Oversampling + v int + s string + }{ + {Off, 0, "Off"}, + {O1x, 1, "1x"}, + {O2x, 2, "2x"}, + {O4x, 4, "4x"}, + {O8x, 8, "8x"}, + {O16x, 16, "16x"}, + {Oversampling(100), 0, "Oversampling(100)"}, + } + for i, line := range data { + if v := line.o.asValue(); v != line.v { + t.Fatalf("#%d %d != %d", i, v, line.v) + } + if s := line.o.String(); s != line.s { + t.Fatalf("#%d %s != %s", i, s, line.s) + } + } +} + +func TestStandby(t *testing.T) { + data := []struct { + d time.Duration + s standby + }{ + {0, s500us}, + {time.Millisecond, s500us}, + {10 * time.Millisecond, s10ms}, + {20 * time.Millisecond, s20ms}, + {62500 * time.Microsecond, s62ms}, + {125 * time.Millisecond, s125ms}, + {250 * time.Millisecond, s250ms}, + {500 * time.Millisecond, s500ms}, + {time.Second, s1s}, + {time.Minute, s1s}, + } + for i, line := range data { + if s := chooseStandby(line.d); s != line.s { + t.Fatalf("#%d chooseStandby(%s) = %d != %d", i, line.d, s, line.s) + } + } +} + func TestCalibration_compensatePressureInt64(t *testing.T) { c := calibration{} if x := c.compensatePressureInt64(0, 0); x != 0 { diff --git a/devices/bme280/bme280smoketest/bme280smoketest.go b/devices/bme280/bme280smoketest/bme280smoketest.go index 1018d40..d4a22c9 100644 --- a/devices/bme280/bme280smoketest/bme280smoketest.go +++ b/devices/bme280/bme280smoketest/bme280smoketest.go @@ -124,8 +124,7 @@ func run(i2cBus i2c.Bus, spiPort spi.PortCloser) (err error) { Temperature: bme280.O16x, Pressure: bme280.O16x, Humidity: bme280.O16x, - Standby: bme280.S1s, - Filter: bme280.FOff, + Filter: bme280.NoFilter, } i2cDev, err2 := bme280.NewI2C(i2cBus, opts) @@ -148,25 +147,38 @@ func run(i2cBus i2c.Bus, spiPort spi.PortCloser) (err error) { } }() - // TODO(maruel): Generally the first measurement is way off. i2cEnv := devices.Environment{} spiEnv := devices.Environment{} if err2 := i2cDev.Sense(&i2cEnv); err2 != nil { return err2 } + printEnv(i2cDev, &i2cEnv) if err2 = spiDev.Sense(&spiEnv); err2 != nil { return err2 } + printEnv(spiDev, &spiEnv) + delta := devices.Environment{ + Temperature: i2cEnv.Temperature - spiEnv.Temperature, + Pressure: i2cEnv.Pressure - spiEnv.Pressure, + Humidity: i2cEnv.Humidity - spiEnv.Humidity, + } + printEnv("Delta", &delta) - // TODO(maruel): Determine acceptable threshold. - if d := i2cEnv.Temperature - spiEnv.Temperature; d > 1000 || d < -1000 { - return fmt.Errorf("Temperature delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Temperature, spiEnv.Temperature) + // 1°C + if delta.Temperature > 1000 || delta.Temperature < -1000 { + return fmt.Errorf("Temperature delta higher than expected (%s): I²C got %s; SPI got %s", delta.Temperature, i2cEnv.Temperature, spiEnv.Temperature) } - if d := i2cEnv.Pressure - spiEnv.Pressure; d > 100 || d < -100 { - return fmt.Errorf("Pressure delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Pressure, spiEnv.Pressure) + // 0.1KPa + if delta.Pressure > 100 || delta.Pressure < -100 { + return fmt.Errorf("Pressure delta higher than expected (%s): I²C got %s; SPI got %s", delta.Pressure, i2cEnv.Pressure, spiEnv.Pressure) } - if d := i2cEnv.Humidity - spiEnv.Humidity; d > 100 || d < -100 { - return fmt.Errorf("Humidity delta higher than expected (%s): I²C got %s; SPI got %s", d, i2cEnv.Humidity, spiEnv.Humidity) + // 4%rH + if delta.Humidity > 400 || delta.Humidity < -400 { + return fmt.Errorf("Humidity delta higher than expected (%s): I²C got %s; SPI got %s", delta.Humidity, i2cEnv.Humidity, spiEnv.Humidity) } return nil } + +func printEnv(dev interface{}, env *devices.Environment) { + fmt.Printf("%-18s: %8s %10s %9s\n", dev, env.Temperature, env.Pressure, env.Humidity) +} diff --git a/devices/devices.go b/devices/devices.go index 39695ae..f7c6f84 100644 --- a/devices/devices.go +++ b/devices/devices.go @@ -9,11 +9,13 @@ import ( "image" "image/color" "io" + "time" ) // Device is a basic device. +// +// It is expected to implement fmt.Stringer. type Device interface { - fmt.Stringer // Halt stops the device. // // Unlike a connection, a device cannot be closed, only the port can be @@ -28,6 +30,8 @@ type Device interface { // What Display represents can be as varied as a 1 bit OLED display or a strip // of LED lights. type Display interface { + Device + // Writer can be used when the native display pixel format is known. Each // write must cover exactly the whole screen as a single packed stream of // pixels. @@ -137,7 +141,14 @@ type Environment struct { // Environmental represents an environmental sensor. type Environmental interface { + Device + // Sense returns the value read from the sensor. Unsupported metrics are not // modified. Sense(env *Environment) error + // SenseContinuous initiates a continuous sensing at the specified interval. + // + // It is important to call Halt() once done with the sensing, which will turn + // the device off and will close the channel. + SenseContinuous(interval time.Duration) (<-chan Environment, error) } diff --git a/devices/devicestest/display.go b/devices/devicestest/display.go index dc3d96e..e7cc0fb 100644 --- a/devices/devicestest/display.go +++ b/devices/devicestest/display.go @@ -6,6 +6,7 @@ package devicestest import ( "errors" + "fmt" "image" "image/color" "image/draw" @@ -18,6 +19,14 @@ type Display struct { Img *image.NRGBA } +func (d *Display) String() string { + return "Display" +} + +func (d *Display) Halt() error { + return nil +} + // Write implements devices.Display. func (d *Display) Write(pixels []byte) (int, error) { if len(pixels)%3 != 0 { @@ -43,3 +52,5 @@ func (d *Display) Draw(r image.Rectangle, src image.Image, sp image.Point) { } var _ devices.Display = &Display{} +var _ devices.Device = &Display{} +var _ fmt.Stringer = &Display{} diff --git a/devices/ds18b20/ds18b20.go b/devices/ds18b20/ds18b20.go index 59fda10..42480d9 100644 --- a/devices/ds18b20/ds18b20.go +++ b/devices/ds18b20/ds18b20.go @@ -175,3 +175,4 @@ func (d *Dev) readScratchpad() ([]byte, error) { } var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/ds248x/dev.go b/devices/ds248x/dev.go index b10ebf9..2086703 100644 --- a/devices/ds248x/dev.go +++ b/devices/ds248x/dev.go @@ -198,3 +198,4 @@ func (e busError) Error() string { return string(e) } func (e busError) BusError() bool { return true } var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/lepton/cci/cci.go b/devices/lepton/cci/cci.go index a4016a7..8214ee1 100644 --- a/devices/lepton/cci/cci.go +++ b/devices/lepton/cci/cci.go @@ -172,6 +172,10 @@ func New(i i2c.Bus) (*Dev, error) { } } +func (d *Dev) String() string { + return d.c.String() +} + // Init initializes the FLIR Lepton in raw 14 bits mode, enables telemetry as // header. func (d *Dev) Init() error { @@ -318,6 +322,10 @@ type conn struct { r mmr.Dev16 } +func (c *conn) String() string { + return fmt.Sprintf("%s", &c.r) +} + // waitIdle waits for the busy bit to clear. func (c *conn) waitIdle() (StatusBit, error) { // Do not take the lock. @@ -548,3 +556,7 @@ const ( ) // TODO(maruel): Enable RadXXX commands. + +var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} +var _ fmt.Stringer = &conn{} diff --git a/devices/lepton/cci/cci_test.go b/devices/lepton/cci/cci_test.go index 34db8f2..557926f 100644 --- a/devices/lepton/cci/cci_test.go +++ b/devices/lepton/cci/cci_test.go @@ -38,10 +38,13 @@ func TestNew_WaitIdle(t *testing.T) { {Addr: 42, W: []byte{0x00, 0x02}, R: []byte{0x00, 0x06}}, }, } - _, err := New(&bus) + c, err := New(&bus) if err != nil { t.Fatal(err) } + if s := c.String(); s != "playback(42)" { + t.Fatal(s) + } if err := bus.Close(); err != nil { t.Fatal(err) } diff --git a/devices/lepton/lepton.go b/devices/lepton/lepton.go index b98b65a..d9804c7 100644 --- a/devices/lepton/lepton.go +++ b/devices/lepton/lepton.go @@ -150,6 +150,10 @@ func New(p spi.Port, i i2c.Bus, cs gpio.PinOut) (*Dev, error) { return d, nil } +func (d *Dev) String() string { + return fmt.Sprintf("Lepton(%s/%s/%s)", d.Dev, d.s, d.cs) +} + // ReadImg reads an image. // // It is ok to call other functions concurrently to send commands to the @@ -449,3 +453,6 @@ func verifyCRC(d []byte) bool { tmp[3] = 0 return internal.CRC16(tmp) == internal.Big16.Uint16(d[2:]) } + +var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/lepton/lepton_test.go b/devices/lepton/lepton_test.go index c9ac94d..7077adf 100644 --- a/devices/lepton/lepton_test.go +++ b/devices/lepton/lepton_test.go @@ -39,6 +39,9 @@ func TestNew_cs(t *testing.T) { if err != nil { t.Fatal(err) } + if s := d.String(); s != "Lepton(playback(42)/playback/CS(0))" { + t.Fatal(s) + } if err := d.Halt(); err != nil { t.Fatal(err) } diff --git a/devices/lirc/lirc.go b/devices/lirc/lirc.go index e44750d..0f72962 100644 --- a/devices/lirc/lirc.go +++ b/devices/lirc/lirc.go @@ -44,6 +44,10 @@ func New() (*Conn, error) { return c, nil } +func (c *Conn) String() string { + return "lirc" +} + // Close closes the socket to lirc. It is not a requirement to close before // process termination. func (c *Conn) Close() error { @@ -260,3 +264,4 @@ func getPins() (int, int) { } var _ ir.Conn = &Conn{} +var _ fmt.Stringer = &Conn{} diff --git a/devices/ssd1306/ssd1306.go b/devices/ssd1306/ssd1306.go index 6ff0488..044080b 100644 --- a/devices/ssd1306/ssd1306.go +++ b/devices/ssd1306/ssd1306.go @@ -490,3 +490,4 @@ const ( var _ devices.Display = &Dev{} var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/devices/tm1637/tm1637.go b/devices/tm1637/tm1637.go index 7e94503..111c604 100644 --- a/devices/tm1637/tm1637.go +++ b/devices/tm1637/tm1637.go @@ -201,3 +201,4 @@ func (d *Dev) sleepHalfCycle() { } var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{} diff --git a/experimental/devices/bitbang/i2c.go b/experimental/devices/bitbang/i2c.go index f25ea32..bdc641b 100644 --- a/experimental/devices/bitbang/i2c.go +++ b/experimental/devices/bitbang/i2c.go @@ -255,3 +255,4 @@ func (i *I2C) sleepHalfCycle() { } var _ i2c.Bus = &I2C{} +var _ fmt.Stringer = &I2C{} diff --git a/experimental/devices/bitbang/spi.go b/experimental/devices/bitbang/spi.go index c2e78e7..61b2a1d 100644 --- a/experimental/devices/bitbang/spi.go +++ b/experimental/devices/bitbang/spi.go @@ -195,3 +195,4 @@ func (s *SPI) sleepHalfCycle() { } var _ spi.Conn = &SPI{} +var _ fmt.Stringer = &SPI{} diff --git a/experimental/devices/bmp180/bmp180.go b/experimental/devices/bmp180/bmp180.go index 6a82ee9..9962698 100644 --- a/experimental/devices/bmp180/bmp180.go +++ b/experimental/devices/bmp180/bmp180.go @@ -129,6 +129,12 @@ func (d *Dev) Sense(env *devices.Environment) error { return nil } +// SenseContinuous implements devices.Environmental. +func (d *Dev) SenseContinuous(interval time.Duration) (<-chan devices.Environment, error) { + // TODO(maruel): Manually poll in a loop via time.NewTicker. + return nil, errors.New("not implemented") +} + // Halt is a noop for the BMP180. func (d *Dev) Halt() error { return nil @@ -245,3 +251,4 @@ func (c *calibration) compensatePressure(up, ut int32, os Oversampling) uint32 { var _ devices.Environmental = &Dev{} var _ devices.Device = &Dev{} +var _ fmt.Stringer = &Dev{}