From 0dcec286f8cacc3ae06bebb5850c01714da17451 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Wed, 21 Sep 2022 12:40:10 +0200 Subject: [PATCH] FEATURE: CCS811 use ReadBlockData() --- drivers/i2c/ccs811_driver.go | 133 +++++++--------- drivers/i2c/ccs811_driver_test.go | 245 ++++++++++++++++-------------- 2 files changed, 185 insertions(+), 193 deletions(-) diff --git a/drivers/i2c/ccs811_driver.go b/drivers/i2c/ccs811_driver.go index 85150264..ba501808 100644 --- a/drivers/i2c/ccs811_driver.go +++ b/drivers/i2c/ccs811_driver.go @@ -4,8 +4,6 @@ import ( "fmt" "math" "time" - - "gobot.io/x/gobot" ) // CCS811DriveMode type @@ -22,7 +20,7 @@ const ( const ( - //DefaultAddress is the default I2C address for the ccs811 + //the default I2C address for the ccs811 applies for ADDR to GND, for ADDR to VDD it will be 0x5B ccs811DefaultAddress = 0x5A //Registers, all definitions have been taken from the datasheet @@ -74,7 +72,7 @@ type CCS811Status struct { FwMode byte } -//NewCCS811Status returns a new instance of the package ccs811 status definiton +//NewCCS811Status returns a new instance of the package ccs811 status definition func NewCCS811Status(data uint8) *CCS811Status { return &CCS811Status{ HasError: data & 0x01, @@ -88,16 +86,16 @@ func NewCCS811Status(data uint8) *CCS811Status { //The following definitions were taken from the bit fields of the ccs811RegMeasMode defined in //https://ams.com/documents/20143/36005/CCS811_DS000459_6-00.pdf/c7091525-c7e5-37ac-eedb-b6c6828b0dcf#page=16 type CCS811MeasMode struct { - //If intThresh is 1 a data measurement will only be taken when the sensor value mets the threshold constraint. + //If intThresh is 1 a data measurement will only be taken when the sensor value meets the threshold constraint. //The threshold value is set in the threshold register (0x10) intThresh uint8 - //If intDataRdy is 1, the nINT signal (pin 3 of the device) will be driven low when new data is avaliable. + //If intDataRdy is 1, the nINT signal (pin 3 of the device) will be driven low when new data is available. intDataRdy uint8 //driveMode represents the sampling rate of the sensor. If the value is 0, the measurement process is idle. driveMode CCS811DriveMode } -//NewCCS811MeasMode returns a new instance of the package ccs811 measurement mode configuration. This represents the desired intial +//NewCCS811MeasMode returns a new instance of the package ccs811 measurement mode configuration. This represents the desired initial //state of the measurement mode register. func NewCCS811MeasMode() *CCS811MeasMode { return &CCS811MeasMode{ @@ -115,30 +113,26 @@ func (mm *CCS811MeasMode) GetMeasMode() byte { //CCS811Driver is the Gobot driver for the CCS811 (air quality sensor) Adafruit breakout board type CCS811Driver struct { - name string - connector Connector - connection Connection + *Driver measMode *CCS811MeasMode ntcResistanceValue uint32 - Config } //NewCCS811Driver creates a new driver for the CCS811 (air quality sensor) -func NewCCS811Driver(a Connector, options ...func(Config)) *CCS811Driver { - l := &CCS811Driver{ - name: gobot.DefaultName("CCS811"), - connector: a, - measMode: NewCCS811MeasMode(), +func NewCCS811Driver(c Connector, options ...func(Config)) *CCS811Driver { + d := &CCS811Driver{ + Driver: NewDriver(c, "CCS811", ccs811DefaultAddress), + measMode: NewCCS811MeasMode(), //Recommended resistance value is 100,000 ntcResistanceValue: 100000, - Config: NewConfig(), } + d.afterStart = d.initialize for _, option := range options { - option(l) + option(d) } - return l + return d } //WithCCS811MeasMode sets the sampling rate of the device @@ -158,32 +152,11 @@ func WithCCS811NTCResistance(val uint32) func(Config) { } } -//Start initializes the sensor -func (d *CCS811Driver) Start() (err error) { - bus := d.GetBusOrDefault(d.connector.GetDefaultBus()) - address := d.GetAddressOrDefault(ccs811DefaultAddress) - - if d.connection, err = d.connector.GetConnection(address, bus); err != nil { - return err - } - - return d.initialize() -} - -//Name returns the Name for the Driver -func (d *CCS811Driver) Name() string { return d.name } - -//SetName sets the Name for the Driver -func (d *CCS811Driver) SetName(n string) { d.name = n } - -//Connection returns the connection for the Driver -func (d *CCS811Driver) Connection() gobot.Connection { return d.connector.(gobot.Connection) } - -//Halt returns true if devices is halted successfully -func (d *CCS811Driver) Halt() (err error) { return } - //GetHardwareVersion returns the hardware version of the device in the form of 0x1X func (d *CCS811Driver) GetHardwareVersion() (uint8, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + v, err := d.connection.ReadByteData(ccs811RegHwVersion) if err != nil { return 0, err @@ -194,6 +167,9 @@ func (d *CCS811Driver) GetHardwareVersion() (uint8, error) { //GetFirmwareBootVersion returns the bootloader version func (d *CCS811Driver) GetFirmwareBootVersion() (uint16, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + v, err := d.connection.ReadWordData(ccs811RegFwBootVersion) if err != nil { return 0, err @@ -204,6 +180,9 @@ func (d *CCS811Driver) GetFirmwareBootVersion() (uint16, error) { //GetFirmwareAppVersion returns the app code version func (d *CCS811Driver) GetFirmwareAppVersion() (uint16, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + v, err := d.connection.ReadWordData(ccs811RegFwAppVersion) if err != nil { return 0, err @@ -214,6 +193,9 @@ func (d *CCS811Driver) GetFirmwareAppVersion() (uint16, error) { //GetStatus returns the current status of the device func (d *CCS811Driver) GetStatus() (*CCS811Status, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + s, err := d.connection.ReadByteData(ccs811RegStatus) if err != nil { return nil, err @@ -226,8 +208,11 @@ func (d *CCS811Driver) GetStatus() (*CCS811Status, error) { //GetTemperature returns the device temperature in celcius. //If you do not have an NTC resistor installed, this function should not be called func (d *CCS811Driver) GetTemperature() (float32, error) { + d.mutex.Lock() + defer d.mutex.Unlock() - buf, err := d.read(ccs811RegNtc, 4) + buf := make([]byte, 4) + err := d.connection.ReadBlockData(ccs811RegNtc, buf) if err != nil { return 0, err } @@ -248,8 +233,11 @@ func (d *CCS811Driver) GetTemperature() (float32, error) { //GetGasData returns the data for the gas sensor. //eco2 is returned in ppm and tvoc is returned in ppb func (d *CCS811Driver) GetGasData() (uint16, uint16, error) { + d.mutex.Lock() + defer d.mutex.Unlock() - data, err := d.read(ccs811RegAlgResultData, 4) + data := make([]byte, 4) + err := d.connection.ReadBlockData(ccs811RegAlgResultData, data) if err != nil { return 0, 0, err } @@ -261,7 +249,7 @@ func (d *CCS811Driver) GetGasData() (uint16, uint16, error) { return eco2, tvoC, nil } -//HasData returns true if the device has not errored and temperature/gas data is avaliable +//HasData returns true if the device has not errored and temperature/gas data is available func (d *CCS811Driver) HasData() (bool, error) { s, err := d.GetStatus() if err != nil { @@ -277,35 +265,22 @@ func (d *CCS811Driver) HasData() (bool, error) { //EnableExternalInterrupt enables the external output hardware interrupt pin 3. func (d *CCS811Driver) EnableExternalInterrupt() error { + d.mutex.Lock() + defer d.mutex.Unlock() + d.measMode.intDataRdy = 1 return d.connection.WriteByteData(ccs811RegMeasMode, d.measMode.GetMeasMode()) } //DisableExternalInterrupt disables the external output hardware interrupt pin 3. func (d *CCS811Driver) DisableExternalInterrupt() error { + d.mutex.Lock() + defer d.mutex.Unlock() + d.measMode.intDataRdy = 0 return d.connection.WriteByteData(ccs811RegMeasMode, d.measMode.GetMeasMode()) } -//updateMeasMode writes the current value of measMode to the measurement mode register. -func (d *CCS811Driver) updateMeasMode() error { - return d.connection.WriteByteData(ccs811RegMeasMode, d.measMode.GetMeasMode()) -} - -//ResetDevice does a software reset of the device. After this operation is done, -//the user must start the app code before the sensor can take any measurements -func (d *CCS811Driver) resetDevice() error { - return d.connection.WriteBlockData(ccs811RegSwReset, ccs811SwResetSequence) -} - -//startApp starts the app code in the device. This operation has to be done after a -//software reset to start taking sensor measurements. -func (d *CCS811Driver) startApp() error { - //Write without data is needed to start the app code - _, err := d.connection.Write([]byte{ccs811RegAppStart}) - return err -} - func (d *CCS811Driver) initialize() error { deviceID, err := d.connection.ReadByteData(ccs811RegHwID) if err != nil { @@ -335,15 +310,21 @@ func (d *CCS811Driver) initialize() error { return nil } -// An implementation of the ReadBlockData i2c operation. This code was copied from the BMP280Driver code -func (d *CCS811Driver) read(reg byte, n int) ([]byte, error) { - if _, err := d.connection.Write([]byte{reg}); err != nil { - return nil, err - } - buf := make([]byte, n) - bytesRead, err := d.connection.Read(buf) - if bytesRead != n || err != nil { - return nil, err - } - return buf, nil +//ResetDevice does a software reset of the device. After this operation is done, +//the user must start the app code before the sensor can take any measurements +func (d *CCS811Driver) resetDevice() error { + return d.connection.WriteBlockData(ccs811RegSwReset, ccs811SwResetSequence) +} + +//startApp starts the app code in the device. This operation has to be done after a +//software reset to start taking sensor measurements. +func (d *CCS811Driver) startApp() error { + //Write without data is needed to start the app code + _, err := d.connection.Write([]byte{ccs811RegAppStart}) + return err +} + +//updateMeasMode writes the current value of measMode to the measurement mode register. +func (d *CCS811Driver) updateMeasMode() error { + return d.connection.WriteByteData(ccs811RegMeasMode, d.measMode.GetMeasMode()) } diff --git a/drivers/i2c/ccs811_driver_test.go b/drivers/i2c/ccs811_driver_test.go index 57e912f5..330a5208 100644 --- a/drivers/i2c/ccs811_driver_test.go +++ b/drivers/i2c/ccs811_driver_test.go @@ -2,87 +2,57 @@ package i2c import ( "errors" + "strings" "testing" "gobot.io/x/gobot" "gobot.io/x/gobot/gobottest" ) -// The CCS811 Meets the Driver Definition +// this ensures that the implementation is based on i2c.Driver, which implements the gobot.Driver +// and tests all implementations, so no further tests needed here for gobot.Driver interface var _ gobot.Driver = (*CCS811Driver)(nil) -// --------- HELPERS -func initTestCCS811Driver() (driver *CCS811Driver) { - driver, _ = initTestCCS811DriverWithStubbedAdaptor() - return +func initTestCCS811WithStubbedAdaptor() (*CCS811Driver, *i2cTestAdaptor) { + a := newI2cTestAdaptor() + return NewCCS811Driver(a), a } -func initTestCCS811DriverWithStubbedAdaptor() (*CCS811Driver, *i2cTestAdaptor) { - adaptor := newI2cTestAdaptor() - return NewCCS811Driver(adaptor), adaptor -} - -// --------- BASE TESTS func TestNewCCS811Driver(t *testing.T) { - // Does it return a pointer to an instance of CCS811Driver? - var c interface{} = NewCCS811Driver(newI2cTestAdaptor()) - _, ok := c.(*CCS811Driver) + var di interface{} = NewCCS811Driver(newI2cTestAdaptor()) + d, ok := di.(*CCS811Driver) if !ok { t.Errorf("NewCCS811Driver() should have returned a *CCS811Driver") } + gobottest.Refute(t, d.Driver, nil) + gobottest.Assert(t, strings.HasPrefix(d.Name(), "CCS811"), true) + gobottest.Assert(t, d.defaultAddress, 0x5A) + gobottest.Refute(t, d.measMode, nil) + gobottest.Assert(t, d.ntcResistanceValue, uint32(100000)) } -func TestCCS811DriverSetName(t *testing.T) { - // Does it change the name of the driver - d := initTestCCS811Driver() - d.SetName("TESTME") - gobottest.Assert(t, d.Name(), "TESTME") -} - -func TestCCS811Connection(t *testing.T) { - // Does it create an instance of gobot.Connection - ccs811 := initTestCCS811Driver() - gobottest.Refute(t, ccs811.Connection(), nil) -} - -// // --------- CONFIG OVERIDE TESTS - -func TestCCS811DriverWithBus(t *testing.T) { - // Can it update the bus - d := NewCCS811Driver(newI2cTestAdaptor(), WithBus(2)) +func TestCCS811Options(t *testing.T) { + // This is a general test, that options are applied in constructor by using the common WithBus() option and + // least one of this driver. Further tests for options can also be done by call of "WithOption(val)(d)". + d := NewCCS811Driver(newI2cTestAdaptor(), WithBus(2), WithAddress(0xFF), WithCCS811NTCResistance(0xFF)) gobottest.Assert(t, d.GetBusOrDefault(1), 2) -} - -func TestCCS811DriverWithAddress(t *testing.T) { - // Can it update the address - d := NewCCS811Driver(newI2cTestAdaptor(), WithAddress(0xFF)) gobottest.Assert(t, d.GetAddressOrDefault(0x5a), 0xFF) + gobottest.Assert(t, d.ntcResistanceValue, uint32(0xFF)) } -func TestCCS811DriverWithCCS811MeasMode(t *testing.T) { - // Can it update the measurement mode +func TestCCS811WithCCS811MeasMode(t *testing.T) { d := NewCCS811Driver(newI2cTestAdaptor(), WithCCS811MeasMode(CCS811DriveMode10Sec)) gobottest.Assert(t, d.measMode.driveMode, CCS811DriveMode(CCS811DriveMode10Sec)) } -func TestCCS811DriverWithCCS811NTCResistance(t *testing.T) { - // Can it update the ntc resitor value used for temp calcuations - d := NewCCS811Driver(newI2cTestAdaptor(), WithCCS811NTCResistance(0xFF)) - gobottest.Assert(t, d.ntcResistanceValue, uint32(0xFF)) -} - -// // --------- DRIVER SPECIFIC TESTS - -func TestCCS811DriverGetGasData(t *testing.T) { - - cases := []struct { +func TestCCS811GetGasData(t *testing.T) { + var tests = map[string]struct { readReturn func([]byte) (int, error) eco2 uint16 tvoc uint16 err error }{ - // Can it compute the gas data with ideal values taken from the bus - { + "ideal values taken from the bus": { readReturn: func(b []byte) (int, error) { copy(b, []byte{1, 156, 0, 86}) return 4, nil @@ -91,8 +61,7 @@ func TestCCS811DriverGetGasData(t *testing.T) { tvoc: 86, err: nil, }, - // Can it compute the gas data with the max values possible taken from the bus - { + "max values possible taken from the bus": { readReturn: func(b []byte) (int, error) { copy(b, []byte{255, 255, 255, 255}) return 4, nil @@ -101,8 +70,7 @@ func TestCCS811DriverGetGasData(t *testing.T) { tvoc: 65535, err: nil, }, - // Does it return an error when the i2c operation fails - { + "error when the i2c operation fails": { readReturn: func(b []byte) (int, error) { copy(b, []byte{255, 255, 255, 255}) return 4, errors.New("Error") @@ -112,31 +80,31 @@ func TestCCS811DriverGetGasData(t *testing.T) { err: errors.New("Error"), }, } - - d, adaptor := initTestCCS811DriverWithStubbedAdaptor() - // Create stub function as it is needed by read submethod in driver code - adaptor.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } - - d.Start() - for _, c := range cases { - adaptor.i2cReadImpl = c.readReturn - eco2, tvoc, err := d.GetGasData() - gobottest.Assert(t, eco2, c.eco2) - gobottest.Assert(t, tvoc, c.tvoc) - gobottest.Assert(t, err, c.err) + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestCCS811WithStubbedAdaptor() + // Create stub function as it is needed by read submethod in driver code + a.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } + d.Start() + a.i2cReadImpl = tc.readReturn + // act + eco2, tvoc, err := d.GetGasData() + // assert + gobottest.Assert(t, eco2, tc.eco2) + gobottest.Assert(t, tvoc, tc.tvoc) + gobottest.Assert(t, err, tc.err) + }) } - } -func TestCCS811DriverGetTemperature(t *testing.T) { - - cases := []struct { +func TestCCS811GetTemperature(t *testing.T) { + var tests = map[string]struct { readReturn func([]byte) (int, error) temp float32 err error }{ - // Can it compute the temperature data with ideal values taken from the bus - { + "ideal values taken from the bus": { readReturn: func(b []byte) (int, error) { copy(b, []byte{10, 197, 0, 248}) return 4, nil @@ -144,8 +112,7 @@ func TestCCS811DriverGetTemperature(t *testing.T) { temp: 27.811005, err: nil, }, - // Can it compute the temperature data without bus values overflowing - { + "without bus values overflowing": { readReturn: func(b []byte) (int, error) { copy(b, []byte{129, 197, 10, 248}) return 4, nil @@ -153,8 +120,7 @@ func TestCCS811DriverGetTemperature(t *testing.T) { temp: 29.48822, err: nil, }, - // Can it compute a negative temperature - { + "negative temperature": { readReturn: func(b []byte) (int, error) { copy(b, []byte{255, 255, 255, 255}) return 4, nil @@ -162,8 +128,7 @@ func TestCCS811DriverGetTemperature(t *testing.T) { temp: -25.334152, err: nil, }, - // Does it return an error if the i2c bus errors - { + "error if the i2c bus errors": { readReturn: func(b []byte) (int, error) { copy(b, []byte{129, 197, 0, 248}) return 4, errors.New("Error") @@ -172,30 +137,30 @@ func TestCCS811DriverGetTemperature(t *testing.T) { err: errors.New("Error"), }, } - - d, adaptor := initTestCCS811DriverWithStubbedAdaptor() - // Create stub function as it is needed by read submethod in driver code - adaptor.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } - - d.Start() - for _, c := range cases { - adaptor.i2cReadImpl = c.readReturn - temp, err := d.GetTemperature() - gobottest.Assert(t, temp, c.temp) - gobottest.Assert(t, err, c.err) + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestCCS811WithStubbedAdaptor() + // Create stub function as it is needed by read submethod in driver code + a.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } + d.Start() + a.i2cReadImpl = tc.readReturn + // act + temp, err := d.GetTemperature() + // assert + gobottest.Assert(t, temp, tc.temp) + gobottest.Assert(t, err, tc.err) + }) } - } -func TestCCS811DriverHasData(t *testing.T) { - - cases := []struct { +func TestCCS811HasData(t *testing.T) { + var tests = map[string]struct { readReturn func([]byte) (int, error) result bool err error }{ - // Does it return true for HasError = 0 and DataRdy = 1 - { + "true for HasError=0 and DataRdy=1": { readReturn: func(b []byte) (int, error) { copy(b, []byte{0x08}) return 1, nil @@ -203,8 +168,7 @@ func TestCCS811DriverHasData(t *testing.T) { result: true, err: nil, }, - // Does it return false for HasError = 1 and DataRdy = 1 - { + "false for HasError=1 and DataRdy=1": { readReturn: func(b []byte) (int, error) { copy(b, []byte{0x09}) return 1, nil @@ -212,8 +176,7 @@ func TestCCS811DriverHasData(t *testing.T) { result: false, err: nil, }, - // Does it return false for HasError = 1 and DataRdy = 0 - { + "false for HasError=1 and DataRdy=0": { readReturn: func(b []byte) (int, error) { copy(b, []byte{0x01}) return 1, nil @@ -221,8 +184,7 @@ func TestCCS811DriverHasData(t *testing.T) { result: false, err: nil, }, - // Does it return false for HasError = 0 and DataRdy = 0 - { + "false for HasError=0 and DataRdy=0": { readReturn: func(b []byte) (int, error) { copy(b, []byte{0x00}) return 1, nil @@ -230,8 +192,7 @@ func TestCCS811DriverHasData(t *testing.T) { result: false, err: nil, }, - // Does it return an error when the i2c read operation fails - { + "error when the i2c read operation fails": { readReturn: func(b []byte) (int, error) { copy(b, []byte{0x00}) return 1, errors.New("Error") @@ -240,17 +201,67 @@ func TestCCS811DriverHasData(t *testing.T) { err: errors.New("Error"), }, } - - d, adaptor := initTestCCS811DriverWithStubbedAdaptor() - // Create stub function as it is needed by read submethod in driver code - adaptor.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } - - d.Start() - for _, c := range cases { - adaptor.i2cReadImpl = c.readReturn - result, err := d.HasData() - gobottest.Assert(t, result, c.result) - gobottest.Assert(t, err, c.err) + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + d, a := initTestCCS811WithStubbedAdaptor() + // Create stub function as it is needed by read submethod in driver code + a.i2cWriteImpl = func([]byte) (int, error) { return 0, nil } + d.Start() + a.i2cReadImpl = tc.readReturn + // act + result, err := d.HasData() + // assert + gobottest.Assert(t, result, tc.result) + gobottest.Assert(t, err, tc.err) + }) } - +} + +func TestCCS811_initialize(t *testing.T) { + // sequence for initialization the device on Start() + // * write hardware ID register (0x20) + // * read the ID + // * prepare software reset register content: a sequence of four bytes must + // be written to this register in a single I²C sequence: 0x11, 0xE5, 0x72, 0x8A + // * write software reset register content (0xFF) + // * write application start register (0xF4) + // * prepare measurement mode register content + // * INT_THRESH = 0 (normal mode) + // * INT_DATARDY = 0 (disable interrupt mode) + // * DRIVE_MODE = 0x01 (constant power, value every 1 sec) + // * write measure mode register content (0x01) + // + // arrange + d, a := initTestCCS811WithStubbedAdaptor() + a.written = []byte{} // reset writes of former test + const ( + wantChipIDReg = uint8(0x20) + wantChipIDRegVal = uint8(0x20) + wantResetReg = uint8(0xFF) + wantAppStartReg = uint8(0xF4) + wantMeasReg = uint8(0x01) + wantMeasRegVal = uint8(0x10) + ) + wantResetRegSequence := []byte{0x11, 0xE5, 0x72, 0x8A} + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + // chip ID + b[0] = 0x81 + return len(b), nil + } + // arrange, act - initialize() must be called on Start() + err := d.Start() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, numCallsRead, 1) + gobottest.Assert(t, len(a.written), 9) + gobottest.Assert(t, a.written[0], wantChipIDReg) + gobottest.Assert(t, a.written[1], wantResetReg) + gobottest.Assert(t, a.written[2:6], wantResetRegSequence) + gobottest.Assert(t, a.written[6], wantAppStartReg) + gobottest.Assert(t, a.written[7], wantMeasReg) + gobottest.Assert(t, a.written[8], wantMeasRegVal) }