package i2c import ( "fmt" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gobot.io/x/gobot/v2" ) // 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 = (*TH02Driver)(nil) func initTestTH02DriverWithStubbedAdaptor() (*TH02Driver, *i2cTestAdaptor) { adaptor := newI2cTestAdaptor() driver := NewTH02Driver(adaptor) if err := driver.Start(); err != nil { panic(err) } return driver, adaptor } func TestNewTH02Driver(t *testing.T) { var di interface{} = NewTH02Driver(newI2cTestAdaptor()) d, ok := di.(*TH02Driver) if !ok { t.Errorf("NewTH02Driver() should have returned a *NewTH02Driver") } assert.NotNil(t, d.Driver) assert.True(t, strings.HasPrefix(d.Name(), "TH02")) assert.Equal(t, 0x40, d.defaultAddress) } func TestTH02Options(t *testing.T) { // This is a general test, that options are applied in constructor by using the common options. // Further tests for options can also be done by call of "WithOption(val)(d)". d := NewTH02Driver(newI2cTestAdaptor(), WithBus(2), WithAddress(0x42)) assert.Equal(t, 2, d.GetBusOrDefault(1)) assert.Equal(t, 0x42, d.GetAddressOrDefault(0x33)) } func TestTH02SetAccuracy(t *testing.T) { b := NewTH02Driver(newI2cTestAdaptor()) if b.SetAccuracy(0x42); b.Accuracy() != TH02HighAccuracy { t.Error("Setting an invalid accuracy should resolve to TH02HighAccuracy") } if b.SetAccuracy(TH02LowAccuracy); b.Accuracy() != TH02LowAccuracy { t.Error("Expected setting low accuracy to actually set to low accuracy") } if acc := b.Accuracy(); acc != TH02LowAccuracy { t.Errorf("Accuracy() didn't return what was expected") } } func TestTH02WithFastMode(t *testing.T) { tests := map[string]struct { value int want bool }{ "fast_on_for >0": {value: 1, want: true}, "fast_off_for =0": {value: 0, want: false}, "fast_off_for <0": {value: -1, want: false}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange d := NewTH02Driver(newI2cTestAdaptor()) // act WithTH02FastMode(tc.value)(d) // assert assert.Equal(t, tc.want, d.fastMode) }) } } func TestTH02FastMode(t *testing.T) { // sequence to read the fast mode status // * write config register address (0x03) // * read register content // * if sixth bit (D5) is set, the fast mode is configured on, otherwise off tests := map[string]struct { read uint8 want bool }{ "fast on": {read: 0x20, want: true}, "fast off": {read: ^uint8(0x20), want: false}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange d, a := initTestTH02DriverWithStubbedAdaptor() a.i2cReadImpl = func(b []byte) (int, error) { b[0] = tc.read return len(b), nil } // act got, err := d.FastMode() // assert require.NoError(t, err) assert.Len(t, a.written, 1) assert.Equal(t, uint8(0x03), a.written[0]) assert.Equal(t, tc.want, got) }) } } func TestTH02SetHeater(t *testing.T) { // sequence to set the heater status // * set the local heater state // * write config register address (0x03) // * prepare config value by set/reset the heater bit (0x02, D1) // * write the config value tests := map[string]struct { heater bool want uint8 }{ "heater on": {heater: true, want: 0x02}, "heater off": {heater: false, want: 0x00}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange d, a := initTestTH02DriverWithStubbedAdaptor() // act err := d.SetHeater(tc.heater) // assert require.NoError(t, err) assert.Equal(t, tc.heater, d.heating) assert.Len(t, a.written, 2) assert.Equal(t, uint8(0x03), a.written[0]) assert.Equal(t, tc.want, a.written[1]) }) } } func TestTH02Heater(t *testing.T) { // sequence to read the heater status // * write config register address (0x03) // * read register content // * if second bit (D1) is set, the heater is configured on, otherwise off tests := map[string]struct { read uint8 want bool }{ "heater on": {read: 0x02, want: true}, "heater off": {read: ^uint8(0x02), want: false}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange d, a := initTestTH02DriverWithStubbedAdaptor() a.i2cReadImpl = func(b []byte) (int, error) { b[0] = tc.read return len(b), nil } // act got, err := d.Heater() // assert require.NoError(t, err) assert.Len(t, a.written, 1) assert.Equal(t, uint8(0x03), a.written[0]) assert.Equal(t, tc.want, got) }) } } func TestTH02SerialNumber(t *testing.T) { // sequence to read SN // * write identification register address (0x11) // * read register content // * use the higher nibble of byte // arrange d, a := initTestTH02DriverWithStubbedAdaptor() a.i2cReadImpl = func(b []byte) (int, error) { b[0] = 0x4F return len(b), nil } want := uint8(0x04) // act sn, err := d.SerialNumber() // assert require.NoError(t, err) assert.Len(t, a.written, 1) assert.Equal(t, uint8(0x11), a.written[0]) assert.Equal(t, want, sn) } func TestTH02Sample(t *testing.T) { // sequence to read values // * write config register address (0x03) // * prepare config bits (START, HEAT, TEMP, FAST) // * write config register with config // * write status register address (0x00) // * read until value is "0" (means ready) // * write data register MSB address (0x01) // * read 2 bytes little-endian (MSB, LSB) // * shift and scale // RH: 4 bits shift right, RH[%]=RH/16-24 // T: 2 bits shift right, T[°C]=T/32-50 // test table according to data sheet page 15, 17 // operating range of the temperature sensor is -40..85 °C (F-grade 0..70 °C) tests := map[string]struct { hData uint16 tData uint16 wantRH float32 wantT float32 }{ "RH 0, T -40": { hData: 0x0180, wantRH: 0.0, tData: 0x0140, wantT: -40.0, }, "RH 10, T -20": { hData: 0x0220, wantRH: 10.0, tData: 0x03C0, wantT: -20.0, }, "RH 20, T -10": { hData: 0x02C0, wantRH: 20.0, tData: 0x0500, wantT: -10.0, }, "RH 30, T 0": { hData: 0x0360, wantRH: 30.0, tData: 0x0640, wantT: 0.0, }, "RH 40, T 10": { hData: 0x0400, wantRH: 40.0, tData: 0x0780, wantT: 10.0, }, "RH 50, T 20": { hData: 0x04A0, wantRH: 50.0, tData: 0x08C0, wantT: 20.0, }, "RH 60, T 30": { hData: 0x0540, wantRH: 60.0, tData: 0x0A00, wantT: 30.0, }, "RH 70, T 40": { hData: 0x05E0, wantRH: 70.0, tData: 0x0B40, wantT: 40.0, }, "RH 80, T 50": { hData: 0x0680, wantRH: 80.0, tData: 0x0C80, wantT: 50.0, }, "RH 90, T 60": { hData: 0x0720, wantRH: 90.0, tData: 0x0DC0, wantT: 60.0, }, "RH 100, T 70": { hData: 0x07C0, wantRH: 100.0, tData: 0x0F00, wantT: 70.0, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange d, a := initTestTH02DriverWithStubbedAdaptor() var reg uint8 var regVal uint8 a.i2cWriteImpl = func(b []byte) (int, error) { reg = b[0] if len(b) == 2 { regVal = b[1] } return len(b), nil } a.i2cReadImpl = func(b []byte) (int, error) { switch reg { case 0x00: // status b[0] = 0 case 0x01: // data register MSB var data uint16 if (regVal & 0x10) == 0x10 { // temperature data = tc.tData << 2 // data sheet values are after shift 2 bits } else { // humidity data = tc.hData << 4 // data sheet values are after shift 4 bits } b[0] = byte(data >> 8) // first read MSB from register 0x01 b[1] = byte(data & 0xFF) // second read LSB from register 0x02 default: assert.Equal(t, "only register 0 and 1 expected", fmt.Sprintf("unexpected register %d", reg)) return 0, nil } return len(b), nil } // act temp, rh, err := d.Sample() // assert require.NoError(t, err) assert.InDelta(t, tc.wantRH, rh, 0.0) assert.InDelta(t, tc.wantT, temp, 0.0) }) } } func TestTH02_readData(t *testing.T) { d, a := initTestTH02DriverWithStubbedAdaptor() var callCounter int tests := map[string]struct { rd func([]byte) (int, error) wr func([]byte) (int, error) rtn uint16 wantErr error }{ "example RH": { rd: func(b []byte) (int, error) { callCounter++ if callCounter == 1 { // read for ready b[0] = 0x00 } else { copy(b, []byte{0x07, 0xC0}) } return len(b), nil }, rtn: 1984, }, "example T": { rd: func(b []byte) (int, error) { callCounter++ if callCounter == 1 { // read for ready b[0] = 0x00 } else { copy(b, []byte{0x12, 0xC0}) } return len(b), nil }, rtn: 4800, }, "timeout - no wait for ready": { rd: func(b []byte) (int, error) { time.Sleep(200 * time.Millisecond) // simulate not ready b[0] = 0x01 return len(b), nil }, wantErr: fmt.Errorf("timeout on \\RDY"), rtn: 0, }, "unable to write status register": { rd: func(b []byte) (int, error) { callCounter++ if callCounter == 1 { // read for ready b[0] = 0x00 } return len(b), nil }, wr: func(b []byte) (int, error) { return len(b), fmt.Errorf("an write error") }, wantErr: fmt.Errorf("timeout on \\RDY"), rtn: 0, }, "unable to write data register": { rd: func(b []byte) (int, error) { callCounter++ if callCounter == 1 { // read for ready b[0] = 0x00 } return len(b), nil }, wr: func(b []byte) (int, error) { if len(b) == 1 && b[0] == 0x00 { // register of ready check return len(b), nil } // data register return len(b), fmt.Errorf("Nope") }, wantErr: fmt.Errorf("Nope"), rtn: 0, }, "unable to read doesn't provide enough data": { rd: func(b []byte) (int, error) { callCounter++ if callCounter == 1 { // read for ready b[0] = 0x00 } else { b = []byte{0x01} } return len(b), nil }, wantErr: fmt.Errorf("Read 1 bytes from device by i2c helpers, expected 2"), rtn: 0, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // arrange a.i2cReadImpl = tc.rd if tc.wr != nil { oldwr := a.i2cWriteImpl a.i2cWriteImpl = tc.wr defer func() { a.i2cWriteImpl = oldwr }() } callCounter = 0 // act got, err := d.waitAndReadData() // assert assert.Equal(t, tc.wantErr, err) assert.Equal(t, tc.rtn, got) }) } } func TestTH02_waitForReadyFailOnTimeout(t *testing.T) { d, a := initTestTH02DriverWithStubbedAdaptor() a.i2cReadImpl = func(b []byte) (int, error) { time.Sleep(50 * time.Millisecond) b[0] = 0x01 return len(b), nil } timeout := 10 * time.Microsecond if err := d.waitForReady(&timeout); err == nil { t.Error("Expected a timeout error") } } func TestTH02_waitForReadyFailOnReadError(t *testing.T) { d, a := initTestTH02DriverWithStubbedAdaptor() a.i2cReadImpl = func(b []byte) (int, error) { time.Sleep(50 * time.Millisecond) b[0] = 0x00 wrongLength := 2 return wrongLength, nil } timeout := 10 * time.Microsecond if err := d.waitForReady(&timeout); err == nil { t.Error("Expected a timeout error") } } func TestTH02_createConfig(t *testing.T) { d := &TH02Driver{} tests := map[string]struct { meas bool fast bool readTemp bool heating bool want byte }{ "meas, no fast, RH, no heating": {meas: true, fast: false, readTemp: false, heating: false, want: 0x01}, "meas, no fast, RH, heating": {meas: true, fast: false, readTemp: false, heating: true, want: 0x03}, "meas, no fast, TE, no heating": {meas: true, fast: false, readTemp: true, heating: false, want: 0x11}, "meas, no fast, TE, heating": {meas: true, fast: false, readTemp: true, heating: true, want: 0x13}, "meas, fast, RH, no heating": {meas: true, fast: true, readTemp: false, heating: false, want: 0x21}, "meas, fast, RH, heating": {meas: true, fast: true, readTemp: false, heating: true, want: 0x23}, "meas, fast, TE, no heating": {meas: true, fast: true, readTemp: true, heating: false, want: 0x31}, "meas, fast, TE, heating": {meas: true, fast: true, readTemp: true, heating: true, want: 0x33}, "no meas, no fast, RH, no heating": {meas: false, fast: false, readTemp: false, heating: false, want: 0x00}, "no meas, no fast, RH, heating": {meas: false, fast: false, readTemp: false, heating: true, want: 0x02}, "no meas, no fast, TE, no heating": {meas: false, fast: false, readTemp: true, heating: false, want: 0x00}, "no meas, no fast, TE, heating": {meas: false, fast: false, readTemp: true, heating: true, want: 0x02}, "no meas, fast, RH, no heating": {meas: false, fast: true, readTemp: false, heating: false, want: 0x00}, "no meas, fast, RH, heating": {meas: false, fast: true, readTemp: false, heating: true, want: 0x02}, "no meas, fast, TE, no heating": {meas: false, fast: true, readTemp: true, heating: false, want: 0x00}, "no meas, fast, TE, heating": {meas: false, fast: true, readTemp: true, heating: true, want: 0x02}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { d.fastMode = tc.fast d.heating = tc.heating got := d.createConfig(tc.meas, tc.readTemp) assert.Equal(t, tc.want, got) }) } }