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.
pull/1/head
M-A 9 years ago committed by GitHub
parent 24ef228ca1
commit ea63dac2c6

@ -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{}

@ -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{}

@ -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 {

@ -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)
}

@ -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)
}

@ -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{}

@ -175,3 +175,4 @@ func (d *Dev) readScratchpad() ([]byte, error) {
}
var _ devices.Device = &Dev{}
var _ fmt.Stringer = &Dev{}

@ -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{}

@ -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{}

@ -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)
}

@ -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{}

@ -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)
}

@ -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{}

@ -490,3 +490,4 @@ const (
var _ devices.Display = &Dev{}
var _ devices.Device = &Dev{}
var _ fmt.Stringer = &Dev{}

@ -201,3 +201,4 @@ func (d *Dev) sleepHalfCycle() {
}
var _ devices.Device = &Dev{}
var _ fmt.Stringer = &Dev{}

@ -255,3 +255,4 @@ func (i *I2C) sleepHalfCycle() {
}
var _ i2c.Bus = &I2C{}
var _ fmt.Stringer = &I2C{}

@ -195,3 +195,4 @@ func (s *SPI) sleepHalfCycle() {
}
var _ spi.Conn = &SPI{}
var _ fmt.Stringer = &SPI{}

@ -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{}

Loading…
Cancel
Save