From ad59d23e2e608c4f594e8b51cc77ff1d30b16341 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Thu, 11 Mar 2021 19:43:17 +0100 Subject: [PATCH] introduce generic i2c.Driver with example for digispark fix missing/wrong entries in README stabilize test --- Makefile | 4 +- README.md | 4 + drivers/i2c/README.md | 3 +- drivers/i2c/helpers_test.go | 6 +- drivers/i2c/i2c_config_test.go | 81 ++++++++++ drivers/i2c/{i2c.go => i2c_connection.go} | 0 .../{i2c_test.go => i2c_connection_test.go} | 0 drivers/i2c/i2c_driver.go | 130 ++++++++++++++++ drivers/i2c/i2c_driver_test.go | 139 ++++++++++++++++++ examples/digispark_driver.go | 77 ++++++++++ robot_work_test.go | 9 +- 11 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 drivers/i2c/i2c_config_test.go rename drivers/i2c/{i2c.go => i2c_connection.go} (100%) rename drivers/i2c/{i2c_test.go => i2c_connection_test.go} (100%) create mode 100644 drivers/i2c/i2c_driver.go create mode 100644 drivers/i2c/i2c_driver_test.go create mode 100644 examples/digispark_driver.go diff --git a/Makefile b/Makefile index f835651f..5fc81d37 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,9 @@ EXAMPLES := $(shell grep -L 'joystick' $$(grep -L 'gocv' $(ALL_EXAMPLES))) # opencv platform currently skipped to prevent install of preconditions including_except := $(shell go list ./... | grep -v platforms/opencv) -# Run tests on nearly all directories +# Run tests on nearly all directories without test cache test: - go test -v $(including_except) + go test -count=1 -v $(including_except) # Run tests with race detection test_race: diff --git a/README.md b/README.md index 529e753d..efeb027e 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ Support for devices that use Inter-Integrated Circuit (I2C) have a shared set of drivers provided using the `gobot/drivers/i2c` package: - [I2C](https://en.wikipedia.org/wiki/I%C2%B2C) <=> [Drivers](https://github.com/hybridgroup/gobot/tree/master/drivers/i2c) + - Adafruit 2x16 RGB-LCD with 5 keys - Adafruit Motor Hat - ADS1015 Analog to Digital Converter - ADS1115 Analog to Digital Converter @@ -271,6 +272,7 @@ drivers provided using the `gobot/drivers/i2c` package: - BMP280 Barometric Pressure/Temperature/Altitude Sensor - BMP388 Barometric Pressure/Temperature/Altitude Sensor - DRV2605L Haptic Controller + - Generic driver for read and write values to/from register address - Grove Digital Accelerometer - GrovePi Expansion Board - Grove RGB LCD @@ -285,11 +287,13 @@ drivers provided using the `gobot/drivers/i2c` package: - MPL115A2 Barometer - MPU6050 Accelerometer/Gyroscope - PCA9685 16-channel 12-bit PWM/Servo Driver + - PCF8591 8-bit 4xA/D & 1xD/A converter - SHT2x Temperature/Humidity - SHT3x-D Temperature/Humidity - SSD1306 OLED Display Controller - TSL2561 Digital Luminosity/Lux/Light Sensor - Wii Nunchuck Controller + - YL-40 Brightness/Temperature sensor, Potentiometer, analog input, analog output Driver Support for devices that use Serial Peripheral Interface (SPI) have a shared set of drivers provided using the `gobot/drivers/spi` package: diff --git a/drivers/i2c/README.md b/drivers/i2c/README.md index 010e11a7..7f9f7abb 100644 --- a/drivers/i2c/README.md +++ b/drivers/i2c/README.md @@ -24,6 +24,7 @@ Gobot has a extensible system for connecting to hardware devices. The following - BMP280 Barometric Pressure/Temperature/Altitude Sensor - BMP388 Barometric Pressure/Temperature/Altitude Sensor - DRV2605L Haptic Controller +- Generic driver for read and write values to/from register address - Grove Digital Accelerometer - GrovePi Expansion Board - Grove RGB LCD @@ -44,7 +45,7 @@ Gobot has a extensible system for connecting to hardware devices. The following - SSD1306 OLED Display Controller - TSL2561 Digital Luminosity/Lux/Light Sensor - Wii Nunchuck Controller -- Y-40 Brightness/Temperature sensor, Potentiometer, analog input, analog output Driver +- YL-40 Brightness/Temperature sensor, Potentiometer, analog input, analog output Driver More drivers are coming soon... diff --git a/drivers/i2c/helpers_test.go b/drivers/i2c/helpers_test.go index 61920e4e..4ad39aa0 100644 --- a/drivers/i2c/helpers_test.go +++ b/drivers/i2c/helpers_test.go @@ -22,6 +22,8 @@ var blue = castColor("blue") type i2cTestAdaptor struct { name string + bus int + address int written []byte mtx sync.Mutex i2cConnectErr bool @@ -153,10 +155,12 @@ func (t *i2cTestAdaptor) WriteBlockData(reg uint8, b []byte) error { return t.writeBytes(buf) } -func (t *i2cTestAdaptor) GetConnection( /* address */ int /* bus */, int) (connection Connection, err error) { +func (t *i2cTestAdaptor) GetConnection(address int, bus int) (connection Connection, err error) { if t.i2cConnectErr { return nil, errors.New("Invalid i2c connection") } + t.bus = bus + t.address = address return t, nil } diff --git a/drivers/i2c/i2c_config_test.go b/drivers/i2c/i2c_config_test.go new file mode 100644 index 00000000..fed1fa6e --- /dev/null +++ b/drivers/i2c/i2c_config_test.go @@ -0,0 +1,81 @@ +package i2c + +import ( + "testing" + + "gobot.io/x/gobot/gobottest" +) + +func TestNewConfig(t *testing.T) { + // arrange, act + ci := NewConfig() + // assert + c, ok := ci.(*i2cConfig) + if !ok { + t.Errorf("NewConfig() should have returned a *i2cConfig") + } + gobottest.Assert(t, c.bus, BusNotInitialized) + gobottest.Assert(t, c.address, AddressNotInitialized) +} + +func TestWithBus(t *testing.T) { + // arrange + c := NewConfig() + // act + c.WithBus(0x23) + // assert + gobottest.Assert(t, c.(*i2cConfig).bus, 0x23) +} + +func TestWithAddress(t *testing.T) { + // arrange + c := NewConfig() + // act + c.WithAddress(0x24) + // assert + gobottest.Assert(t, c.(*i2cConfig).address, 0x24) +} + +func TestGetBusOrDefaultWithBusOption(t *testing.T) { + var tests = map[string]struct { + init int + bus int + want int + }{ + "not_initialized": {init: -1, bus: 0x25, want: 0x25}, + "initialized": {init: 0x26, bus: 0x27, want: 0x26}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + c := NewConfig() + // act + WithBus(tc.init)(c) + got := c.GetBusOrDefault(tc.bus) + // assert + gobottest.Assert(t, got, tc.want) + }) + } +} + +func TestGetAddressOrDefaultWithAddressOption(t *testing.T) { + var tests = map[string]struct { + init int + address int + want int + }{ + "not_initialized": {init: -1, address: 0x28, want: 0x28}, + "initialized": {init: 0x29, address: 0x2A, want: 0x29}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + c := NewConfig() + // act + WithAddress(tc.init)(c) + got := c.GetAddressOrDefault(tc.address) + // assert + gobottest.Assert(t, got, tc.want) + }) + } +} diff --git a/drivers/i2c/i2c.go b/drivers/i2c/i2c_connection.go similarity index 100% rename from drivers/i2c/i2c.go rename to drivers/i2c/i2c_connection.go diff --git a/drivers/i2c/i2c_test.go b/drivers/i2c/i2c_connection_test.go similarity index 100% rename from drivers/i2c/i2c_test.go rename to drivers/i2c/i2c_connection_test.go diff --git a/drivers/i2c/i2c_driver.go b/drivers/i2c/i2c_driver.go new file mode 100644 index 00000000..b56ba468 --- /dev/null +++ b/drivers/i2c/i2c_driver.go @@ -0,0 +1,130 @@ +package i2c + +import ( + "fmt" + "strconv" + "sync" + + "gobot.io/x/gobot" +) + +// Driver implements the interface gobot.Driver. +type Driver struct { + name string + defaultAddress int + connector Connector + connection Connection + afterStart func() error + beforeHalt func() error + Config + gobot.Commander + mutex *sync.Mutex // mutex often needed to ensure that write-read sequences are not interrupted +} + +// NewDriver creates a new generic and basic i2c gobot driver. +func NewDriver(c Connector, name string, address int, options ...func(Config)) *Driver { + d := &Driver{ + name: gobot.DefaultName(name), + defaultAddress: address, + connector: c, + afterStart: func() error { return nil }, + beforeHalt: func() error { return nil }, + Config: NewConfig(), + Commander: gobot.NewCommander(), + mutex: &sync.Mutex{}, + } + + for _, option := range options { + option(d) + } + + return d +} + +// Name returns the name of the i2c device. +func (d *Driver) Name() string { + return d.name +} + +// SetName sets the name of the i2c device. +func (d *Driver) SetName(name string) { + d.name = name +} + +// Connection returns the connection of the i2c device. +func (d *Driver) Connection() gobot.Connection { + return d.connector.(gobot.Connection) +} + +// Start initializes the i2c device. +func (d *Driver) Start() error { + d.mutex.Lock() + defer d.mutex.Unlock() + + var err error + bus := d.GetBusOrDefault(d.connector.GetDefaultBus()) + address := d.GetAddressOrDefault(int(d.defaultAddress)) + + if d.connection, err = d.connector.GetConnection(address, bus); err != nil { + return err + } + + return d.afterStart() +} + +// Halt halts the i2c device. +func (d *Driver) Halt() error { + d.mutex.Lock() + defer d.mutex.Unlock() + + if err := d.beforeHalt(); err != nil { + return err + } + + // currently there is nothing to do here for the driver + return nil +} + +// Write implements a simple write mechanism to the given register of an i2c device. +func (d *Driver) Write(pin string, val int) error { + d.mutex.Lock() + defer d.mutex.Unlock() + + register, err := driverParseRegister(pin) + if err != nil { + return err + } + + // TODO: create buffer from size + // currently only one byte value is supported + b := []byte{uint8(val)} + return d.connection.WriteBlockData(uint8(register), b) +} + +// Read implements a simple read mechanism from the given register of an i2c device. +func (d *Driver) Read(pin string) (int, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + register, err := driverParseRegister(pin) + if err != nil { + return 0, err + } + + // TODO: create buffer from size + // currently only one byte value is supported + b := []byte{0} + if err := d.connection.ReadBlockData(register, b); err != nil { + return 0, err + } + + return int(b[0]), nil +} + +func driverParseRegister(pin string) (uint8, error) { + register, err := strconv.ParseUint(pin, 10, 8) + if err != nil { + return 0, fmt.Errorf("Could not parse the register from given pin '%s'", pin) + } + return uint8(register), nil +} diff --git a/drivers/i2c/i2c_driver_test.go b/drivers/i2c/i2c_driver_test.go new file mode 100644 index 00000000..75c4e928 --- /dev/null +++ b/drivers/i2c/i2c_driver_test.go @@ -0,0 +1,139 @@ +package i2c + +import ( + "errors" + "strings" + "testing" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/gobottest" +) + +var _ gobot.Driver = (*Driver)(nil) + +func initDriverWithStubbedAdaptor() (*Driver, *i2cTestAdaptor) { + a := newI2cTestAdaptor() + d := NewDriver(a, "I2C_BASIC", 0x15) + return d, a +} + +func initTestDriver() *Driver { + d, _ := initDriverWithStubbedAdaptor() + return d +} + +func TestNewDriver(t *testing.T) { + // arrange + a := newI2cTestAdaptor() + // act + var di interface{} = NewDriver(a, "I2C_BASIC", 0x15) + // assert + d, ok := di.(*Driver) + if !ok { + t.Errorf("NewDriver() should have returned a *Driver") + } + gobottest.Assert(t, strings.Contains(d.name, "I2C_BASIC"), true) + gobottest.Assert(t, d.defaultAddress, 0x15) + gobottest.Assert(t, d.connector, a) + gobottest.Assert(t, d.connection, nil) + gobottest.Assert(t, d.afterStart(), nil) + gobottest.Assert(t, d.beforeHalt(), nil) + gobottest.Refute(t, d.Config, nil) + gobottest.Refute(t, d.Commander, nil) + gobottest.Refute(t, d.mutex, nil) +} + +func TestSetName(t *testing.T) { + // arrange + d := initTestDriver() + // act + d.SetName("TESTME") + // assert + gobottest.Assert(t, d.Name(), "TESTME") +} + +func TestConnection(t *testing.T) { + // arrange + d := initTestDriver() + // act, assert + gobottest.Refute(t, d.Connection(), nil) +} + +func TestStart(t *testing.T) { + // arrange + d, a := initDriverWithStubbedAdaptor() + // act, assert + gobottest.Assert(t, d.Start(), nil) + gobottest.Assert(t, 0x15, a.address) +} + +func TestStartConnectError(t *testing.T) { + // arrange + d, a := initDriverWithStubbedAdaptor() + a.Testi2cConnectErr(true) + // act, assert + gobottest.Assert(t, d.Start(), errors.New("Invalid i2c connection")) +} + +func TestHalt(t *testing.T) { + // arrange + d := initTestDriver() + // act, assert + gobottest.Assert(t, d.Halt(), nil) +} + +func TestWrite(t *testing.T) { + // arrange + const ( + address = "82" + wantAddress = uint8(0x52) + value = 0x25 + ) + d, a := initDriverWithStubbedAdaptor() + d.Start() + // prepare all writes + numCallsWrite := 0 + a.i2cWriteImpl = func([]byte) (int, error) { + numCallsWrite++ + return 0, nil + } + // act + err := d.Write(address, value) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, numCallsWrite, 1) + gobottest.Assert(t, a.written[0], wantAddress) + gobottest.Assert(t, a.written[1], uint8(value)) +} + +func TestRead(t *testing.T) { + // arrange + const ( + address = "83" + wantAddress = uint8(0x53) + want = uint8(0x44) + ) + d, a := initDriverWithStubbedAdaptor() + d.Start() + // prepare all writes + numCallsWrite := 0 + a.i2cWriteImpl = func(b []byte) (int, error) { + numCallsWrite++ + return 0, nil + } + // prepare all reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[0] = want + return len(b), nil + } + // act + val, err := d.Read(address) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, val, int(want)) + gobottest.Assert(t, numCallsWrite, 1) + gobottest.Assert(t, a.written[0], wantAddress) + gobottest.Assert(t, numCallsRead, 1) +} diff --git a/examples/digispark_driver.go b/examples/digispark_driver.go new file mode 100644 index 00000000..d570bb2a --- /dev/null +++ b/examples/digispark_driver.go @@ -0,0 +1,77 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "strconv" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/platforms/digispark" +) + +// This is an example for using the generic I2C driver to write and read values +// to an i2c device. It is suitable for simple devices, e.g. EEPROM. +// The example was tested with the EEPROM part of PCA9501. +// +// Procedure: +// * write value to register (EEPROM address) +// * read value back from register (EEPROM address) and check for differences +func main() { + const ( + defaultAddress = 0x7F + myAddress = 0x44 // needs to be adjusted for your configuration + ) + board := digispark.NewAdaptor() + drv := i2c.NewDriver(board, "PCA9501-EEPROM", defaultAddress, i2c.WithAddress(myAddress)) + var eepromAddr uint8 = 0x00 + var register string + var valWr uint8 = 0xFF + var valRd int + var err error + + work := func() { + gobot.Every(50*time.Millisecond, func() { + // write a value 0-255 to EEPROM address 255-0 + eepromAddr-- + valWr++ + register = strconv.Itoa(int(eepromAddr)) + err = drv.Write(register, int(valWr)) + if err != nil { + fmt.Println("err write:", err) + } + + // write process needs some time, so wait at least 5ms before read a value + // when decreasing to much, the check below will fail + time.Sleep(5 * time.Millisecond) + + // read value back and check for unexpected differences + valRd, err = drv.Read(register) + if err != nil { + fmt.Println("err read:", err) + } + if int(valWr) != valRd { + fmt.Printf("addr: %d wr: %d differ rd: %d\n", eepromAddr, valWr, valRd) + } + + if eepromAddr%10 == 0 { + fmt.Printf("addr: %d, wr: %d rd: %d\n", eepromAddr, valWr, valRd) + } + }) + } + + robot := gobot.NewRobot("simpleDriverI2c", + []gobot.Connection{board}, + []gobot.Device{drv}, + work, + ) + + err = robot.Start() + if err != nil { + fmt.Println(err) + } +} diff --git a/robot_work_test.go b/robot_work_test.go index 37840971..f7ff706a 100644 --- a/robot_work_test.go +++ b/robot_work_test.go @@ -60,17 +60,18 @@ func TestRobotWorkRegistry(t *testing.T) { func TestRobotAutomationFunctions(t *testing.T) { t.Run("Every with cancel", func(t *testing.T) { robot := NewRobot("testbot") + counter := 0 - rw := robot.Every(context.Background(), time.Millisecond*10, func() { - _ = 1 + 1 // perform mindless computation! + rw := robot.Every(context.Background(), time.Millisecond*100, func() { + counter++ }) - time.Sleep(time.Millisecond * 25) + time.Sleep(time.Millisecond * 225) rw.CallCancelFunc() robot.WorkEveryWaitGroup.Wait() - assert.Equal(t, 2, rw.tickCount) + assert.Equal(t, 2, counter) postDeleteKeys := collectStringKeysFromWorkRegistry(robot.workRegistry) assert.NotContains(t, postDeleteKeys, rw.id.String()) })