1
0
mirror of https://github.com/hybridgroup/gobot.git synced 2025-04-24 13:48:49 +08:00

introduce generic i2c.Driver with example for digispark

fix missing/wrong entries in README
stabilize test
This commit is contained in:
Thomas Kohler 2021-03-11 19:43:17 +01:00 committed by Thomas Kohler
parent 2c4e12b296
commit ad59d23e2e
11 changed files with 445 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

130
drivers/i2c/i2c_driver.go Normal file
View File

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

View File

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

View File

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

View File

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