From 611ba4eabc607a31fe67fa15e3cb5225675bad3b Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Thu, 31 Oct 2024 11:22:06 +0100 Subject: [PATCH] onewire(ds18b20): introduce 1-wire device access by sysfs and temp driver --- Makefile | 4 +- README.md | 6 + adaptor.go | 32 ++ driver.go | 4 +- drivers/i2c/helpers_test.go | 8 + drivers/onewire/README.md | 15 + drivers/onewire/ds18b20_driver.go | 227 ++++++++++++ drivers/onewire/ds18b20_driver_test.go | 344 +++++++++++++++++++ drivers/onewire/helpers_test.go | 118 +++++++ drivers/onewire/onewire_connection.go | 68 ++++ drivers/onewire/onewire_connection_test.go | 196 +++++++++++ drivers/onewire/onewire_driver.go | 131 +++++++ drivers/onewire/onewire_driver_test.go | 79 +++++ examples/tinkerboard_ds18b20.go | 80 +++++ platforms/adaptors/onewirebusadaptor.go | 77 +++++ platforms/adaptors/onewirebusadaptor_test.go | 76 ++++ platforms/tinkerboard/README.md | 38 +- platforms/tinkerboard/adaptor.go | 10 + platforms/tinkerboard/adaptor_test.go | 1 + system/GPIO.md | 2 +- system/ONEWIRE.md | 71 ++++ system/fs_mock.go | 5 +- system/onewiredevice_sysfs.go | 61 ++++ system/system.go | 10 + 24 files changed, 1640 insertions(+), 23 deletions(-) create mode 100644 drivers/onewire/README.md create mode 100644 drivers/onewire/ds18b20_driver.go create mode 100644 drivers/onewire/ds18b20_driver_test.go create mode 100644 drivers/onewire/helpers_test.go create mode 100644 drivers/onewire/onewire_connection.go create mode 100644 drivers/onewire/onewire_connection_test.go create mode 100644 drivers/onewire/onewire_driver.go create mode 100644 drivers/onewire/onewire_driver_test.go create mode 100644 examples/tinkerboard_ds18b20.go create mode 100644 platforms/adaptors/onewirebusadaptor.go create mode 100644 platforms/adaptors/onewirebusadaptor_test.go create mode 100644 system/ONEWIRE.md create mode 100644 system/onewiredevice_sysfs.go diff --git a/Makefile b/Makefile index faf3056f5..95298d41f 100644 --- a/Makefile +++ b/Makefile @@ -70,10 +70,10 @@ examples_fmt_fix: $(EXAMPLES): ifeq ($(CHECK),ON) - go vet ./$@ + go vet -tags libusb ./$@ else ifeq ($(CHECK),FMT) gofumpt -l -w ./$@ golangci-lint run ./$@ --fix --build-tags example,libusb --disable forcetypeassert --disable noctx else - go build -o /tmp/gobot_examples/$@ ./$@ + go build -tags libusb -o /tmp/gobot_examples/$@ ./$@ endif diff --git a/README.md b/README.md index dd3e18b34..59587c8f6 100644 --- a/README.md +++ b/README.md @@ -408,6 +408,12 @@ a shared set of drivers provided using the `gobot/drivers/spi` package: - MFRC522 RFID Card Reader - SSD1306 OLED Display Controller +Support for devices that use 1-wire bus with Linux Kernel support (w1-gpio) have +a shared set of drivers provided using the `gobot/drivers/onewire` package: + +- [1-wire](https://en.wikipedia.org/wiki/1-Wire) <=> [Drivers](https://github.com/hybridgroup/gobot/blob/release/drivers/onewire) + - DS18B20 Temperature Sensor + ## API Gobot includes a RESTful API to query the status of any robot running within a group, including the connection and diff --git a/adaptor.go b/adaptor.go index cb66b8315..dbb4c2b04 100644 --- a/adaptor.go +++ b/adaptor.go @@ -176,6 +176,22 @@ type SpiSystemDevicer interface { Close() error } +// OneWireSystemDevicer is the interface to a 1-wire device at system level. +type OneWireSystemDevicer interface { + // ID returns the device id in the form "family code"-"serial number". + ID() string + // ReadData reads byte data from the device + ReadData(command string, data []byte) error + // WriteData writes byte data to the device + WriteData(command string, data []byte) error + // ReadInteger reads an integer value from the device + ReadInteger(command string) (int, error) + // WriteInteger writes an integer value to the device + WriteInteger(command string, val int) error + // Close the 1-wire connection. + Close() error +} + // BusOperations are functions provided by a bus device, e.g. SPI, i2c. type BusOperations interface { // ReadByteData reads a byte from the given register of bus device. @@ -213,6 +229,22 @@ type SpiOperations interface { Close() error } +// OneWireOperations are the wrappers around the actual functions used by the 1-wire device interface +type OneWireOperations interface { + // ID returns the device id in the form "family code"-"serial number". + ID() string + // ReadData reads from the device + ReadData(command string, data []byte) error + // WriteData writes to the device + WriteData(command string, data []byte) error + // ReadInteger reads an integer value from the device + ReadInteger(command string) (int, error) + // WriteInteger writes an integer value to the device + WriteInteger(command string, val int) error + // Close the connection. + Close() error +} + // Adaptor is the interface that describes an adaptor in gobot type Adaptor interface { // Name returns the label for the Adaptor diff --git a/driver.go b/driver.go index 650de09b3..c7ba8f2b1 100644 --- a/driver.go +++ b/driver.go @@ -4,8 +4,8 @@ package gobot type Driver interface { // Name returns the label for the Driver Name() string - // SetName sets the label for the Driver. - // Please use options [aio.WithName, ble.WithName, gpio.WithName or serial.WithName] instead. + // SetName sets the label for the Driver (deprecated, use WithName() instead). + // Please use options [aio.WithName, ble.WithName, gpio.WithName, onewire.WithName or serial.WithName] instead. SetName(s string) // Start initiates the Driver Start() error diff --git a/drivers/i2c/helpers_test.go b/drivers/i2c/helpers_test.go index 87bc55e1b..fbdd499d5 100644 --- a/drivers/i2c/helpers_test.go +++ b/drivers/i2c/helpers_test.go @@ -66,6 +66,7 @@ func (t *i2cTestAdaptor) ReadByte() (byte, error) { return 0, err } val := bytes[0] + return val, nil } @@ -80,6 +81,7 @@ func (t *i2cTestAdaptor) ReadByteData(reg uint8) (uint8, error) { return 0, err } val := bytes[0] + return val, nil } @@ -98,6 +100,7 @@ func (t *i2cTestAdaptor) ReadWordData(reg uint8) (uint16, error) { return 0, fmt.Errorf("Buffer underrun") } low, high := bytes[0], bytes[1] + return (uint16(high) << 8) | uint16(low), nil } @@ -107,6 +110,7 @@ func (t *i2cTestAdaptor) ReadBlockData(reg uint8, b []byte) error { if err := t.writeBytes([]byte{reg}); err != nil { return err } + return t.readBytes(b) } @@ -153,6 +157,7 @@ func (t *i2cTestAdaptor) WriteBytes(b []byte) error { if len(b) > 32 { b = b[:32] } + return t.writeBytes(b) } @@ -162,6 +167,7 @@ func (t *i2cTestAdaptor) GetI2cConnection(address int, bus int) (Connection, err } t.bus = bus t.address = address + return t, nil } @@ -194,6 +200,7 @@ func (t *i2cTestAdaptor) readBytes(b []byte) error { if n != len(b) { return fmt.Errorf("Read %v bytes from device by i2c helpers, expected %v", n, len(b)) } + return nil } @@ -204,5 +211,6 @@ func (t *i2cTestAdaptor) writeBytes(b []byte) error { if err != nil { return err } + return nil } diff --git a/drivers/onewire/README.md b/drivers/onewire/README.md new file mode 100644 index 000000000..6ad8289e5 --- /dev/null +++ b/drivers/onewire/README.md @@ -0,0 +1,15 @@ +# 1-wire + +This package provides drivers for [1-wire](https://en.wikipedia.org/wiki/1-Wire) devices supported by Linux Kernel w1-gpio +drivers. It must be used along with an adaptor such as [Tinker Board](https://gobot.io/documentation/platforms/tinkerboard/) +that supports the needed interfaces for 1-wire devices. + +## Getting Started + +Please refer to the main [README.md](https://github.com/hybridgroup/gobot/blob/release/README.md) + +## Hardware Support + +Gobot has a extensible system for connecting to hardware devices. The following 1-wire devices are currently supported: + +- DS18B20 Temperature Sensor diff --git a/drivers/onewire/ds18b20_driver.go b/drivers/onewire/ds18b20_driver.go new file mode 100644 index 000000000..86176387b --- /dev/null +++ b/drivers/onewire/ds18b20_driver.go @@ -0,0 +1,227 @@ +package onewire + +import ( + "fmt" + "math" + "time" +) + +const ( + ds18b20DefaultResolution = 12 + ds18b20DefaultConversionTime = 750 + + temperatureCommand = "temperature" + extPowerCommand = "ext_power" + resolutionCommand = "resolution" + convTimeCommand = "conv_time" +) + +// ds18b20OptionApplier needs to be implemented by each configurable option type +type ds18b20OptionApplier interface { + apply(cfg *ds18b20Configuration) +} + +// ds18b20Configuration contains all changeable attributes of the driver. +type ds18b20Configuration struct { + scaleUnit func(int) float32 + resolution uint8 + conversionTime uint16 +} + +// ds18b20UnitscalerOption is the type for applying another unit scaler to the configuration +type ds18b20UnitscalerOption struct { + unitscaler func(int) float32 +} + +type ds18b20ResolutionOption uint8 + +type ds18b20ConversionTimeOption uint16 + +// DS18B20Driver is a driver for the DS18B20 1-wire temperature sensor. +type DS18B20Driver struct { + *driver + ds18b20Cfg *ds18b20Configuration +} + +// NewDS18B20Driver creates a new Gobot Driver for DS18B20 one wire temperature sensor. +// +// Params: +// +// a *Adaptor - the Adaptor to use with this Driver. +// serial number int - the serial number of the device, without the family code +// +// Optional params: +// +// onewire.WithFahrenheit() +// onewire.WithResolution(byte) +// onewire.WithConversionTime(uint16) +func NewDS18B20Driver(a connector, serialNumber uint64, opts ...interface{}) *DS18B20Driver { + d := &DS18B20Driver{ + driver: newDriver(a, "DS18B20", 0x28, serialNumber), + ds18b20Cfg: &ds18b20Configuration{ + scaleUnit: func(input int) float32 { return float32(input) / 1000 }, // 1000:1 in °C + resolution: ds18b20DefaultResolution, + conversionTime: ds18b20DefaultConversionTime, + }, + } + d.afterStart = d.initialize + d.beforeHalt = d.shutdown + for _, opt := range opts { + switch o := opt.(type) { + case optionApplier: + o.apply(d.driverCfg) + case ds18b20OptionApplier: + o.apply(d.ds18b20Cfg) + default: + panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name)) + } + } + return d +} + +// WithFahrenheit substitute the default °C scaler by a scaler for °F +func WithFahrenheit() ds18b20OptionApplier { + // (1°C × 9/5) + 32 = 33,8°F + unitscaler := func(input int) float32 { return float32(input)/1000*9.0/5.0 + 32.0 } + return ds18b20UnitscalerOption{unitscaler: unitscaler} +} + +// WithResolution substitute the default 12 bit resolution by the given one (9, 10, 11). The device will adjust +// the conversion time automatically. Each smaller resolution will decrease the conversion time by a factor of 2. +// Note: some devices are fixed in 12 bit mode only and do not support this feature (I/O error or just ignore it). +// WithConversionTime() is most likely supported. +func WithResolution(resolution uint8) ds18b20OptionApplier { + return ds18b20ResolutionOption(resolution) +} + +// WithConversionTime substitute the default 750 ms by the given one (93, 187, 375, 750). +// Note: Devices will not adjust the resolution automatically. Some devices accept conversion time values different +// from common specification. E.g. 10...1000, which leads to real conversion time of conversionTime+50ms. This needs +// to be tested for your device and measured for your needs, e.g. by DebugConversionTime(0, 500, 5, true). +func WithConversionTime(conversionTime uint16) ds18b20OptionApplier { + return ds18b20ConversionTimeOption(conversionTime) +} + +// Temperature returns the current temperature, in celsius degrees, if the default unit scaler is used. +func (d *DS18B20Driver) Temperature() (float32, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + val, err := d.connection.ReadInteger(temperatureCommand) + if err != nil { + return 0, err + } + + return d.ds18b20Cfg.scaleUnit(val), nil +} + +// Resolution returns the current resolution in bits (9, 10, 11, 12) +func (d *DS18B20Driver) Resolution() (uint8, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + val, err := d.connection.ReadInteger(resolutionCommand) + if err != nil { + return 0, err + } + + if val < 9 || val > 12 { + return 0, fmt.Errorf("the read value '%d' is out of range (9, 10, 11, 12)", val) + } + + return uint8(val), nil +} + +// IsExternalPowered returns whether the device is external or parasitic powered +func (d *DS18B20Driver) IsExternalPowered() (bool, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + val, err := d.connection.ReadInteger(extPowerCommand) + if err != nil { + return false, err + } + + return val > 0, nil +} + +// ConversionTime returns the conversion time in ms +func (d *DS18B20Driver) ConversionTime() (uint16, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + val, err := d.connection.ReadInteger(convTimeCommand) + if err != nil { + return 0, err + } + + if val < 0 || val > math.MaxUint16 { + return 0, fmt.Errorf("the read value '%d' is out of range (uint16)", val) + } + + return uint16(val), nil +} + +// DebugConversionTime try to set the conversion time and compare with real time to read temperature. +func (d *DS18B20Driver) DebugConversionTime(start, end uint16, stepwide uint16, skipInvalid bool) { + r, _ := d.Resolution() + fmt.Printf("\n---- Conversion time check for '%s'@%dbit %d..%d +%d ----\n", + d.connection.ID(), r, start, end, stepwide) + fmt.Println("|r1(err)\t|w(err)\t\t|r2(err)\t|T(err)\t\t|real\t\t|diff\t\t|") + fmt.Println("--------------------------------------------------------------------------------") + for ct := start; ct < end; ct += stepwide { + r1, e1 := d.ConversionTime() + ew := d.connection.WriteInteger(convTimeCommand, int(ct)) + r2, e2 := d.ConversionTime() + time.Sleep(100 * time.Millisecond) // relax the system + start := time.Now() + temp, err := d.Temperature() + dur := time.Since(start) + valid := ct == r2 + if valid || !skipInvalid { + diff := dur - time.Duration(r2)*time.Millisecond + fmt.Printf("|%d(%t)\t|%d(%t)\t|%d(%t)\t|%v(%t)\t|%s\t|%s\t|\n", + r1, e1 != nil, ct, ew != nil, r2, e2 != nil, temp, err != nil, dur, diff) + } + } +} + +func (d *DS18B20Driver) initialize() error { + if d.ds18b20Cfg.resolution != ds18b20DefaultResolution { + if err := d.connection.WriteInteger(resolutionCommand, int(d.ds18b20Cfg.resolution)); err != nil { + return err + } + } + + if d.ds18b20Cfg.conversionTime != ds18b20DefaultConversionTime { + return d.connection.WriteInteger(convTimeCommand, int(d.ds18b20Cfg.conversionTime)) + } + + return nil +} + +func (d *DS18B20Driver) shutdown() error { + if d.ds18b20Cfg.resolution != ds18b20DefaultResolution { + if err := d.connection.WriteInteger(resolutionCommand, ds18b20DefaultResolution); err != nil { + return err + } + } + + if d.ds18b20Cfg.conversionTime != ds18b20DefaultConversionTime { + return d.connection.WriteInteger(convTimeCommand, int(ds18b20DefaultConversionTime)) + } + + return nil +} + +func (o ds18b20UnitscalerOption) apply(cfg *ds18b20Configuration) { + cfg.scaleUnit = o.unitscaler +} + +func (o ds18b20ResolutionOption) apply(cfg *ds18b20Configuration) { + cfg.resolution = uint8(o) +} + +func (o ds18b20ConversionTimeOption) apply(cfg *ds18b20Configuration) { + cfg.conversionTime = uint16(o) +} diff --git a/drivers/onewire/ds18b20_driver_test.go b/drivers/onewire/ds18b20_driver_test.go new file mode 100644 index 000000000..490f65fe5 --- /dev/null +++ b/drivers/onewire/ds18b20_driver_test.go @@ -0,0 +1,344 @@ +package onewire + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewDS18B20Driver(t *testing.T) { + // arrange & act + a := newOneWireTestAdaptor() + d := NewDS18B20Driver(a, 2345) + // assert + assert.IsType(t, &DS18B20Driver{}, d) + assert.NotNil(t, d.driver) + assert.NotNil(t, d.ds18b20Cfg) + assert.Equal(t, uint8(12), d.ds18b20Cfg.resolution) + assert.Equal(t, uint16(750), d.ds18b20Cfg.conversionTime) + assert.InDelta(t, float32(1), d.ds18b20Cfg.scaleUnit(1000), 0.0) +} + +func TestDS18B20Start(t *testing.T) { + tests := map[string]struct { + cfgResolution uint8 + cfgConvTime uint16 + simulateErr bool + wantCommands []string + wantErr string + }{ + "start_ok": { + cfgResolution: 12, + cfgConvTime: 750, + }, + "start_change_resolution": { + cfgResolution: 9, + cfgConvTime: 750, + wantCommands: []string{"resolution"}, + }, + "start_change_conversiontime": { + cfgResolution: 12, + cfgConvTime: 250, + wantCommands: []string{"conv_time"}, + }, + "start_change_all": { + cfgResolution: 8, + cfgConvTime: 150, + wantCommands: []string{"resolution", "conv_time"}, + }, + "error_start": { + simulateErr: true, + wantErr: "GetOneWireConnection error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + d := NewDS18B20Driver(a, 987654321) + d.ds18b20Cfg.resolution = tc.cfgResolution + d.ds18b20Cfg.conversionTime = tc.cfgConvTime + a.retErr = tc.simulateErr + // act + err := d.Start() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.wantCommands, a.sendCommands) + }) + } +} + +func TestDS18B20Halt(t *testing.T) { + tests := map[string]struct { + cfgResolution uint8 + cfgConvTime uint16 + simulateErr bool + wantCommands []string + wantErr string + }{ + "start_ok": { + cfgResolution: 12, + cfgConvTime: 750, + }, + "start_change_resolution": { + cfgResolution: 9, + cfgConvTime: 750, + wantCommands: []string{"resolution"}, + }, + "start_change_conversiontime": { + cfgResolution: 12, + cfgConvTime: 250, + wantCommands: []string{"conv_time"}, + }, + "start_change_all": { + cfgResolution: 8, + cfgConvTime: 150, + wantCommands: []string{"resolution", "conv_time"}, + }, + "error_halt": { + cfgResolution: 8, // to force writing + simulateErr: true, + wantCommands: []string{"resolution"}, + wantErr: "WriteInteger error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + d := NewDS18B20Driver(a, 987654321) + require.NoError(t, d.Start()) + d.ds18b20Cfg.resolution = tc.cfgResolution + d.ds18b20Cfg.conversionTime = tc.cfgConvTime + a.retErr = tc.simulateErr + // act + err := d.Halt() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, tc.wantCommands, a.sendCommands) + }) + } +} + +func TestDS18B20Temperature(t *testing.T) { + const readValue = 24500 + tests := map[string]struct { + simulateErr bool + wantVal float32 + wantErr string + }{ + "read_ok": { + wantVal: 24.5, + }, + "error_read": { + simulateErr: true, + wantErr: "ReadInteger error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + a.lastValue = readValue + d := NewDS18B20Driver(a, 987654321) + require.NoError(t, d.Start()) + a.retErr = tc.simulateErr + // act + got, err := d.Temperature() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, []string{"temperature"}, a.sendCommands) + assert.InDelta(t, tc.wantVal, got, 0.0) + }) + } +} + +func TestDS18B20Resolution(t *testing.T) { + tests := map[string]struct { + readValue int + simulateErr bool + wantVal float32 + wantErr string + }{ + "read_ok": { + readValue: 9, + wantVal: 9, + }, + "error_below_min": { + readValue: 8, + wantErr: "the read value '8' is out of range (9, 10, 11, 12)", + }, + "error_above_max": { + readValue: 13, + wantErr: "the read value '13' is out of range (9, 10, 11, 12)", + }, + "error_read": { + simulateErr: true, + wantErr: "ReadInteger error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + a.lastValue = tc.readValue + d := NewDS18B20Driver(a, 987654321) + require.NoError(t, d.Start()) + a.retErr = tc.simulateErr + // act + got, err := d.Resolution() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, []string{"resolution"}, a.sendCommands) + assert.InDelta(t, tc.wantVal, got, 0.0) + }) + } +} + +func TestDS18B20IsExternalPowered(t *testing.T) { + tests := map[string]struct { + readValue int + simulateErr bool + wantVal bool + wantErr string + }{ + "read_true": { + readValue: 1, + wantVal: true, + }, + "read_false": { + readValue: 0, + wantVal: false, + }, + "error_read": { + simulateErr: true, + wantErr: "ReadInteger error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + a.lastValue = tc.readValue + d := NewDS18B20Driver(a, 987654321) + require.NoError(t, d.Start()) + a.retErr = tc.simulateErr + // act + got, err := d.IsExternalPowered() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, []string{"ext_power"}, a.sendCommands) + assert.Equal(t, tc.wantVal, got) + }) + } +} + +func TestDS18B20ConversionTime(t *testing.T) { + tests := map[string]struct { + readValue int + simulateErr bool + wantVal float32 + wantErr string + }{ + "read_ok": { + readValue: 20, + wantVal: 20, + }, + "error_below_min": { + readValue: -1, + wantErr: "the read value '-1' is out of range (uint16)", + }, + "error_above_max": { + readValue: 65536, + wantErr: "the read value '65536' is out of range (uint16)", + }, + "error_read": { + simulateErr: true, + wantErr: "ReadInteger error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + a := newOneWireTestAdaptor() + a.lastValue = tc.readValue + d := NewDS18B20Driver(a, 987654321) + require.NoError(t, d.Start()) + a.retErr = tc.simulateErr + // act + got, err := d.ConversionTime() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, []string{"conv_time"}, a.sendCommands) + assert.InDelta(t, tc.wantVal, got, 0.0) + }) + } +} + +func TestDS18B20WithName(t *testing.T) { + // This is a general test, that parent options are applied by using the WithName() option. + // All other configuration options can also be tested by With..(val).apply(cfg). + // arrange + const newName = "new name" + a := newOneWireTestAdaptor() + // act + d := NewDS18B20Driver(a, 2345, WithName(newName)) + // assert + assert.Equal(t, newName, d.Name()) +} + +func TestDS18B20WithResolution(t *testing.T) { + // This is a general test, that options are applied by using the WithResolution() option. + // All other configuration options can also be tested by With..(val).apply(cfg). + // arrange + const newValue = uint8(9) + a := newOneWireTestAdaptor() + // act + d := NewDS18B20Driver(a, 2345, WithResolution(newValue)) + // assert + assert.Equal(t, newValue, d.ds18b20Cfg.resolution) +} + +func TestDS18B20WithConversionTime(t *testing.T) { + // arrange + const newValue = uint16(93) + cfg := ds18b20Configuration{conversionTime: 15} + // act + WithConversionTime(newValue).apply(&cfg) + // assert + assert.Equal(t, newValue, cfg.conversionTime) +} + +func TestDS18B20WithFahrenheit(t *testing.T) { + // arrange + cfg := ds18b20Configuration{} + // act + WithFahrenheit().apply(&cfg) + // assert + assert.InDelta(t, 33.8, cfg.scaleUnit(1000), 0.01) +} diff --git a/drivers/onewire/helpers_test.go b/drivers/onewire/helpers_test.go new file mode 100644 index 000000000..df7ab2c84 --- /dev/null +++ b/drivers/onewire/helpers_test.go @@ -0,0 +1,118 @@ +package onewire + +import ( + "errors" + "sync" +) + +type oneWireAdaptorMock struct { + mtx sync.Mutex + familyCode byte + serialNumber uint64 + sendCommands []string + lastValue int + retErr bool +} + +func newOneWireTestAdaptor() *oneWireAdaptorMock { + return &oneWireAdaptorMock{} +} + +func (am *oneWireAdaptorMock) GetOneWireConnection(familyCode byte, serialNumber uint64) (Connection, error) { + am.mtx.Lock() + defer am.mtx.Unlock() + + if am.retErr { + return nil, errors.New("GetOneWireConnection error") + } + am.familyCode = familyCode + am.serialNumber = serialNumber + + return am, nil +} + +// implementations of gobot.OneWireOperations +func (am *oneWireAdaptorMock) ID() string { return "" } + +func (am *oneWireAdaptorMock) ReadData(command string, data []byte) error { + am.sendCommands = append(am.sendCommands, command) + + return nil +} + +func (am *oneWireAdaptorMock) WriteData(command string, data []byte) error { + am.sendCommands = append(am.sendCommands, command) + + return nil +} + +func (am *oneWireAdaptorMock) ReadInteger(command string) (int, error) { + am.sendCommands = append(am.sendCommands, command) + if am.retErr { + return 0, errors.New("ReadInteger error") + } + + return am.lastValue, nil +} + +func (am *oneWireAdaptorMock) WriteInteger(command string, val int) error { + am.sendCommands = append(am.sendCommands, command) + if am.retErr { + return errors.New("WriteInteger error") + } + + return nil +} + +func (am *oneWireAdaptorMock) Close() error { return nil } + +// implementations of gobot.Connection, respectively gobot.Adaptor +func (am *oneWireAdaptorMock) Name() string { return "" } +func (am *oneWireAdaptorMock) SetName(name string) {} +func (am *oneWireAdaptorMock) Connect() error { return nil } +func (am *oneWireAdaptorMock) Finalize() error { return nil } + +type oneWireSystemDeviceMock struct { + id string + lastValue int + lastData []byte + retErr error + lastCommand string +} + +//nolint:unparam // ok here +func newOneWireTestSystemDevice(id string) *oneWireSystemDeviceMock { + return &oneWireSystemDeviceMock{id: id} +} + +func (dm *oneWireSystemDeviceMock) ID() string { return dm.id } + +func (dm *oneWireSystemDeviceMock) ReadData(command string, data []byte) error { + dm.lastCommand = command + copy(data, dm.lastData) + + return dm.retErr +} + +func (dm *oneWireSystemDeviceMock) WriteData(command string, data []byte) error { + dm.lastCommand = command + dm.lastData = make([]byte, len(data)) + copy(dm.lastData, data) + + return dm.retErr +} + +func (dm *oneWireSystemDeviceMock) ReadInteger(command string) (int, error) { + dm.lastCommand = command + + return dm.lastValue, dm.retErr +} + +func (dm *oneWireSystemDeviceMock) WriteInteger(command string, val int) error { + dm.lastCommand = command + dm.lastValue = val + + return dm.retErr +} + +func (dm *oneWireSystemDeviceMock) Close() error { return dm.retErr } diff --git a/drivers/onewire/onewire_connection.go b/drivers/onewire/onewire_connection.go new file mode 100644 index 000000000..037717306 --- /dev/null +++ b/drivers/onewire/onewire_connection.go @@ -0,0 +1,68 @@ +package onewire + +import ( + "sync" + + "gobot.io/x/gobot/v2" +) + +// onewireConnection is the common implementation of the 1-wire bus interface. +type onewireConnection struct { + onewireSystem gobot.OneWireSystemDevicer + mutex *sync.Mutex +} + +// NewConnection uses the given 1-wire system device and provides it as gobot.OneWireOperations. +func NewConnection(onewireSystem gobot.OneWireSystemDevicer) *onewireConnection { + return &onewireConnection{onewireSystem: onewireSystem, mutex: &sync.Mutex{}} +} + +// ID returns the device id in the form "family code"-"serial number". Implements gobot.OneWireOperations. +func (d *onewireConnection) ID() string { + return d.onewireSystem.ID() +} + +// ReadData reads the data according the command, e.g. from the specified file on sysfs bus. +// Implements gobot.OneWireOperations. +func (c *onewireConnection) ReadData(command string, data []byte) error { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.onewireSystem.ReadData(command, data) +} + +// WriteData writes the data according the command, e.g. to the specified file on sysfs bus. +// Implements gobot.OneWireOperations. +func (c *onewireConnection) WriteData(command string, data []byte) error { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.onewireSystem.WriteData(command, data) +} + +// ReadInteger reads the value according the command, e.g. to the specified file on sysfs bus. +// Implements gobot.OneWireOperations. +func (c *onewireConnection) ReadInteger(command string) (int, error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.onewireSystem.ReadInteger(command) +} + +// WriteInteger writes the value according the command, e.g. to the specified file on sysfs bus. +// Implements gobot.OneWireOperations. +func (c *onewireConnection) WriteInteger(command string, val int) error { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.onewireSystem.WriteInteger(command, val) +} + +// Close connection to underlying 1-wire device. Implements functions of onewire.Connection respectively +// gobot.OneWireOperations. +func (c *onewireConnection) Close() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + return c.onewireSystem.Close() +} diff --git a/drivers/onewire/onewire_connection_test.go b/drivers/onewire/onewire_connection_test.go new file mode 100644 index 000000000..29a637f60 --- /dev/null +++ b/drivers/onewire/onewire_connection_test.go @@ -0,0 +1,196 @@ +package onewire + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConnection(t *testing.T) { + // arrange & act + c := NewConnection(newOneWireTestSystemDevice("id")) + // assert + assert.IsType(t, &onewireConnection{}, c) + assert.Equal(t, "id", c.ID()) +} + +func TestClose(t *testing.T) { + tests := map[string]struct { + simulateErr bool + wantErr string + }{ + "close_ok": {}, + "error_close": { + wantErr: "close error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + sysCon := newOneWireTestSystemDevice("id") + if tc.wantErr != "" { + sysCon.retErr = errors.New(tc.wantErr) + } + c := NewConnection(sysCon) + // act + err := c.Close() + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestReadData(t *testing.T) { + data := []byte{10, 11, 21, 32} + tests := map[string]struct { + data []byte + wantData []byte + wantErr string + }{ + "read_ok": { + // only to test the parameter passing + data: []byte{0, 0, 0}, + wantData: []byte{10, 11, 21}, + }, + "error_read": { + data: []byte{0, 0}, + wantData: []byte{10, 11}, + wantErr: "read error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + const command = "read data command" + sysCon := newOneWireTestSystemDevice("id") + sysCon.lastData = data + if tc.wantErr != "" { + sysCon.retErr = errors.New(tc.wantErr) + } + c := NewConnection(sysCon) + // act + err := c.ReadData(command, tc.data) + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, command, sysCon.lastCommand) + assert.Equal(t, tc.wantData, tc.data) + }) + } +} + +func TestWriteData(t *testing.T) { + tests := map[string]struct { + data []byte + wantErr string + }{ + "write_ok": { + data: []byte{10, 11, 21}, + }, + "error_write": { + data: []byte{11, 32}, + wantErr: "read error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + const command = "write data command" + sysCon := newOneWireTestSystemDevice("id") + if tc.wantErr != "" { + sysCon.retErr = errors.New(tc.wantErr) + } + c := NewConnection(sysCon) + // act + err := c.WriteData(command, tc.data) + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, command, sysCon.lastCommand) + assert.Equal(t, tc.data, sysCon.lastData) + }) + } +} + +func TestReadInteger(t *testing.T) { + tests := map[string]struct { + wantValue int + wantErr string + }{ + "read_ok": { + wantValue: 12, + }, + "error_read": { + wantErr: "read error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + const command = "read data command" + sysCon := newOneWireTestSystemDevice("id") + sysCon.lastValue = tc.wantValue + if tc.wantErr != "" { + sysCon.retErr = errors.New(tc.wantErr) + } + c := NewConnection(sysCon) + // act + got, err := c.ReadInteger(command) + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, command, sysCon.lastCommand) + assert.Equal(t, tc.wantValue, got) + }) + } +} + +func TestWriteInteger(t *testing.T) { + tests := map[string]struct { + value int + wantErr string + }{ + "write_ok": { + value: 21, + }, + "error_write": { + wantErr: "read error", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + const command = "write data command" + sysCon := newOneWireTestSystemDevice("id") + if tc.wantErr != "" { + sysCon.retErr = errors.New(tc.wantErr) + } + c := NewConnection(sysCon) + // act + err := c.WriteInteger(command, tc.value) + // assert + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } + assert.Equal(t, command, sysCon.lastCommand) + assert.Equal(t, tc.value, sysCon.lastValue) + }) + } +} diff --git a/drivers/onewire/onewire_driver.go b/drivers/onewire/onewire_driver.go new file mode 100644 index 000000000..b0f7bee34 --- /dev/null +++ b/drivers/onewire/onewire_driver.go @@ -0,0 +1,131 @@ +package onewire + +import ( + "fmt" + "log" + "sync" + + "gobot.io/x/gobot/v2" +) + +// connector lets adaptors provide the drivers to get access to the 1-wire devices on platforms. +type connector interface { + // GetOneWireConnection returns a connection to a 1-wire device with family code and serial number. + GetOneWireConnection(familyCode byte, serialNumber uint64) (Connection, error) +} + +// Connection is a connection to a 1-wire device with family code and serial number on a specific bus, provided by +// an adaptor, usually just by calling the onewire package's GetOneWireConnection() function. +type Connection gobot.OneWireOperations + +// optionApplier needs to be implemented by each configurable option type +type optionApplier interface { + apply(cfg *configuration) +} + +// configuration contains all changeable attributes of the driver. +type configuration struct { + name string + familyCode byte + serialNumber uint64 +} + +// nameOption is the type for applying another name to the configuration +type nameOption string + +// Driver implements the interface gobot.Driver. +type driver struct { + driverCfg *configuration + connector connector + connection Connection + afterStart func() error + beforeHalt func() error + gobot.Commander + mutex *sync.Mutex // mutex often needed to ensure that write-read sequences are not interrupted +} + +// newDriver creates a new generic and basic 1-wire gobot driver. +// +// Supported options: +// +// "WithName" +func newDriver(a connector, name string, familyCode byte, serialNumber uint64, opts ...interface{}) *driver { + d := &driver{ + driverCfg: &configuration{name: gobot.DefaultName(name), familyCode: familyCode, serialNumber: serialNumber}, + connector: a, + afterStart: func() error { return nil }, + beforeHalt: func() error { return nil }, + Commander: gobot.NewCommander(), + mutex: &sync.Mutex{}, + } + + for _, opt := range opts { + switch o := opt.(type) { + case optionApplier: + o.apply(d.driverCfg) + default: + panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name)) + } + } + + return d +} + +// WithName is used to replace the default name of the driver. +func WithName(name string) optionApplier { + return nameOption(name) +} + +// Name returns the name of the device. +func (d *driver) Name() string { + return d.driverCfg.name +} + +// SetName sets the name of the device (deprecated, use WithName() instead). +func (d *driver) SetName(name string) { + d.driverCfg.name = name +} + +// Connection returns the connection of the device. +func (d *driver) Connection() gobot.Connection { + if conn, ok := d.connection.(gobot.Connection); ok { + return conn + } + + log.Printf("%s has no gobot connection\n", d.driverCfg.name) + return nil +} + +// Start initializes the device. +func (d *driver) Start() error { + d.mutex.Lock() + defer d.mutex.Unlock() + + var err error + d.connection, err = d.connector.GetOneWireConnection(d.driverCfg.familyCode, d.driverCfg.serialNumber) + if err != nil { + return err + } + + return d.afterStart() +} + +// Halt halts the device. +func (d *driver) Halt() error { + d.mutex.Lock() + defer d.mutex.Unlock() + + // currently there is nothing to do here for the driver, the connection is cached on adaptor side + // and will be closed on adaptor Finalize() + + return d.beforeHalt() +} + +func (o nameOption) String() string { + return "name option for 1-wire drivers" +} + +// apply change the name in the configuration. +func (o nameOption) apply(c *configuration) { + c.name = string(o) +} diff --git a/drivers/onewire/onewire_driver_test.go b/drivers/onewire/onewire_driver_test.go new file mode 100644 index 000000000..ab43ba2f2 --- /dev/null +++ b/drivers/onewire/onewire_driver_test.go @@ -0,0 +1,79 @@ +package onewire + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func initTestDriver() *driver { + d, _ := initDriverWithStubbedAdaptor() + return d +} + +func initDriverWithStubbedAdaptor() (*driver, *oneWireAdaptorMock) { + a := newOneWireTestAdaptor() + return newDriver(a, "name", 28, 9876), a +} + +func Test_newDriver(t *testing.T) { + // arrange + const ( + familyCode = 99 + serialNumber = 1234567890 + ) + // act + d := newDriver(newOneWireTestAdaptor(), "name", familyCode, serialNumber) + // assert + assert.IsType(t, &driver{}, d) + assert.NotNil(t, d.driverCfg) + assert.NotNil(t, d.connector) + assert.NotNil(t, d.afterStart) + assert.NotNil(t, d.beforeHalt) + assert.NotNil(t, d.Commander) + assert.NotNil(t, d.mutex) +} + +func TestConnection(t *testing.T) { + // arrange + d := initTestDriver() + require.NoError(t, d.Start()) + // act, assert + assert.NotNil(t, d.Connection()) +} + +func TestStart(t *testing.T) { + // arrange + d := initTestDriver() + // act, assert + require.NoError(t, d.Start()) +} + +func TestStartConnectError(t *testing.T) { + // arrange + d, c := initDriverWithStubbedAdaptor() + c.retErr = true + // act, assert + require.ErrorContains(t, d.Start(), "GetOneWireConnection error") +} + +func TestHalt(t *testing.T) { + // arrange + d := initTestDriver() + // act, assert + require.NoError(t, d.Halt()) +} + +func TestWithName(t *testing.T) { + // This is a general test, that options are applied by using the WithName() option. + // All other configuration options can also be tested by With..(val).apply(cfg). + // arrange + const newName = "new name" + a := newOneWireTestAdaptor() + // act + d := newDriver(a, "name", 28, 9876, WithName(newName)) + // assert + assert.Equal(t, newName, d.driverCfg.name) + assert.Equal(t, newName, d.Name()) +} diff --git a/examples/tinkerboard_ds18b20.go b/examples/tinkerboard_ds18b20.go new file mode 100644 index 000000000..47d0cfdbc --- /dev/null +++ b/examples/tinkerboard_ds18b20.go @@ -0,0 +1,80 @@ +//go:build example +// +build example + +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/drivers/onewire" + "gobot.io/x/gobot/v2/platforms/tinkerboard" +) + +// Preparation: see /gobot/system/ONEWIRE.md and /gobot/platforms/tinkerboard/README.md +// +// Wiring: +// PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) +// 1-wire Tinkerboard: 7 (DQ) - resistor to VCC, ~1.5kOhm ... 5kOhm +// DS18B20: 1 (GND), 2 (DQ), 3 (VDD, +3 ... 5.5V) for local power mode +func main() { + adaptor := tinkerboard.NewAdaptor() + // resolution change not supported by all devices + temp0 := onewire.NewDS18B20Driver(adaptor, 0x072261452f18, onewire.WithResolution(10)) + temp1 := onewire.NewDS18B20Driver(adaptor, 0x1465421f64ff, onewire.WithFahrenheit(), onewire.WithConversionTime(500)) + + work := func() { + time0, err := temp0.ConversionTime() + if err != nil { + log.Printf("Err CT0: %v\n", err) + } + res0, err := temp0.Resolution() + if err != nil { + log.Printf("Err R0: %v\n", err) + } + log.Printf("Conversion time @%d bit for Temp 0: %d ms\n", res0, time0) + + time1, err := temp1.ConversionTime() + if err != nil { + log.Printf("Err CT1: %v\n", err) + } + res1, err := temp1.Resolution() + if err != nil { + log.Printf("Err R1: %v\n", err) + } + log.Printf("Conversion time @%d bit for Temp 0: %d ms\n", res1, time1) + + gobot.Every(10*(time.Duration(time0))*time.Millisecond, func() { + t0, err := temp0.Temperature() + if err != nil { + log.Printf("Err Temp 0: %v\n", err) + } + + fmt.Printf("Temp 0: %2.1f °C\n", t0) + }) + + gobot.Every(10*(time.Duration(time1))*time.Millisecond, func() { + t1, err := temp1.Temperature() + if err != nil { + log.Printf("Err Temp 1: %v\n", err) + } + + fmt.Printf("Temp 1: %2.3f °F\n", t1) + }) + } + + robot := gobot.NewRobot("onewireBot", + []gobot.Connection{adaptor}, + []gobot.Device{temp0, temp1}, + work, + ) + + if err := robot.Start(); err != nil { + panic(err) + } +} diff --git a/platforms/adaptors/onewirebusadaptor.go b/platforms/adaptors/onewirebusadaptor.go new file mode 100644 index 000000000..c24391cb4 --- /dev/null +++ b/platforms/adaptors/onewirebusadaptor.go @@ -0,0 +1,77 @@ +package adaptors + +import ( + "fmt" + "sync" + + multierror "github.com/hashicorp/go-multierror" + + "gobot.io/x/gobot/v2/drivers/onewire" + "gobot.io/x/gobot/v2/system" +) + +// OneWireBusAdaptor is a adaptor for the 1-wire bus, normally used for composition in platforms. +// note: currently only one controller is supported by most platforms, but it would be possible to activate more, +// see https://forums.raspberrypi.com/viewtopic.php?t=65137 +type OneWireBusAdaptor struct { + sys *system.Accesser + mutex *sync.Mutex + connections map[string]onewire.Connection +} + +// NewOneWireBusAdaptor provides the access to 1-wire devices of the board. +func NewOneWireBusAdaptor(sys *system.Accesser) *OneWireBusAdaptor { + a := &OneWireBusAdaptor{sys: sys, mutex: &sync.Mutex{}} + return a +} + +// Connect prepares the connection to 1-wire devices. +func (a *OneWireBusAdaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() + + a.connections = make(map[string]onewire.Connection) + return nil +} + +// Finalize closes all 1-wire connections. +func (a *OneWireBusAdaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() + + var err error + for _, con := range a.connections { + if con != nil { + if e := con.Close(); e != nil { + err = multierror.Append(err, e) + } + } + } + a.connections = nil + return err +} + +// GetOneWireConnection returns a 1-wire connection to a device with the given family code and serial number. +func (a *OneWireBusAdaptor) GetOneWireConnection(familyCode byte, serialNumber uint64) (onewire.Connection, error) { + a.mutex.Lock() + defer a.mutex.Unlock() + + if a.connections == nil { + return nil, fmt.Errorf("not connected") + } + + id := fmt.Sprintf("%d_%d", familyCode, serialNumber) + + con := a.connections[id] + if con == nil { + var err error + dev, err := a.sys.NewOneWireDevice(familyCode, serialNumber) + if err != nil { + return nil, err + } + con = onewire.NewConnection(dev) + a.connections[id] = con + } + + return con, nil +} diff --git a/platforms/adaptors/onewirebusadaptor_test.go b/platforms/adaptors/onewirebusadaptor_test.go new file mode 100644 index 000000000..3c9506dc9 --- /dev/null +++ b/platforms/adaptors/onewirebusadaptor_test.go @@ -0,0 +1,76 @@ +package adaptors + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gobot.io/x/gobot/v2/system" +) + +func initTestOneWireAdaptor() *OneWireBusAdaptor { + a := NewOneWireBusAdaptor(system.NewAccesser()) + if err := a.Connect(); err != nil { + panic(err) + } + return a +} + +func TestNewOneWireBusAdaptor(t *testing.T) { + // arrange + sys := system.NewAccesser() + // act + a := NewOneWireBusAdaptor(sys) + // assert + assert.IsType(t, &OneWireBusAdaptor{}, a) + assert.NotNil(t, a.mutex) + assert.Nil(t, a.connections) +} + +func TestOneWireGetOneWireConnection(t *testing.T) { + // arrange + const ( + familyCode = 28 + serialNumber = 123456789 + ) + a := initTestOneWireAdaptor() + // assert working connection + c1, e1 := a.GetOneWireConnection(familyCode, serialNumber) + require.NoError(t, e1) + assert.NotNil(t, c1) + assert.Len(t, a.connections, 1) + // assert unconnected gets error + require.NoError(t, a.Finalize()) + c2, e2 := a.GetOneWireConnection(familyCode, serialNumber+1) + require.ErrorContains(t, e2, "not connected") + assert.Nil(t, c2) + assert.Empty(t, a.connections) +} + +func TestOneWireFinalize(t *testing.T) { + // arrange + a := initTestOneWireAdaptor() + // assert that finalize before connect is working + require.NoError(t, a.Finalize()) + // arrange + require.NoError(t, a.Connect()) + _, _ = a.GetOneWireConnection(28, 54321) + assert.Len(t, a.connections, 1) + // assert that Finalize after GetOneWireConnection is working and clean up + require.NoError(t, a.Finalize()) + assert.Empty(t, a.connections) + // assert that finalize after finalize is working + require.NoError(t, a.Finalize()) +} + +func TestOneWireReConnect(t *testing.T) { + // arrange + a := initTestOneWireAdaptor() + require.NoError(t, a.Finalize()) + // act + require.NoError(t, a.Connect()) + // assert + assert.NotNil(t, a.connections) + assert.Empty(t, a.connections) +} diff --git a/platforms/tinkerboard/README.md b/platforms/tinkerboard/README.md index de937eaf2..02c3dd6eb 100644 --- a/platforms/tinkerboard/README.md +++ b/platforms/tinkerboard/README.md @@ -14,8 +14,11 @@ Tested OS: * [Debian TinkerOS](https://github.com/TinkerBoard/debian_kernel/releases) * [armbian](https://www.armbian.com/tinkerboard/) with Debian or Ubuntu -> The latest "Tinker Board Debian 10 V3.0.11" is official discontinued. Nevertheless it is well tested with gobot. There -> is a known i2c issue with the Kernel 4.4.194 if using block reads. armbian is known to work in this area. +> The latest "Tinker Board Debian 10 V3.0.11" is official discontinued. Nevertheless it is tested with gobot. There +> is a known i2c issue with the Kernel 4.4.194 if using block reads. armbian is known to work in this area. We recommend +> to use "armbian bookworm minimal", because it is used for the latest development steps of gobot. + +## Configuration steps for the OS ### System access and configuration basics @@ -26,7 +29,7 @@ Note that these configuration steps must be performed on the Tinker Board itself Board via SSH (option "-4" is used to force IPv4, which is needed for some versions of TinkerOS): ```sh -ssh -4 linaro@192.168.1.xxx +ssh -4 @192.168.1.xxx # linaro@192.168.1.xxx ``` ### Enabling hardware drivers @@ -35,19 +38,20 @@ Not all drivers are enabled by default. You can have a look at the configuration your system: ```sh -cat /boot/config.txt +cat /boot/armbianEnv.txt #/boot/config.txt ``` This file can be modified by "vi" or "nano", it is self explanatory: ```sh -sudo vi /boot/config.txt +sudo vi /boot/armbianEnv.txt ``` -Newer versions of Tinker Board provide an user interface for configuration with: +Newer versions of OS provide an user interface for configuration with: ```sh -sudo tinker-config +sudo apt install armbian-config +sudo armbian-config # tinker-config ``` After configuration was changed, an reboot is necessary. @@ -68,17 +72,17 @@ sudo groupadd -f --system gpio If you already have a "gpio" group, you can skip to the next step. -#### Add the "linaro" user to the new "gpio" group +#### Add the user to the new "gpio" group (TinkerOS only) -Add the user "linaro" to be a member of the Linux group named "gpio" by running the following command: +Add the user to be a member of the Linux group named "gpio" by running the following command: ```sh -sudo usermod -a -G gpio linaro +sudo usermod -a -G gpio ``` If you already have added the "gpio" group, you can skip to the next step. -#### Add a "udev" rules file for gpio +#### Add a "udev" rules file for gpio (TinkerOS only) Create a new "udev" rules file for the GPIO on the Tinker Board by running the following command: @@ -108,17 +112,17 @@ Create a Linux group named "i2c" by running the following command: sudo groupadd -f --system i2c ``` -#### Add the "linaro" user to the new "i2c" group +#### Add the user to the new "i2c" group If you already have added the "i2c" group, you can skip to the next step. -Add the user "linaro" to be a member of the Linux group named "i2c" by running the following command: +Add the user to be a member of the Linux group named "i2c" by running the following command: ```sh -sudo usermod -a -G gpio linaro +sudo usermod -a -G gpio ``` -#### Add a "udev" rules file for I2C +#### Add a "udev" rules file for I2C (TinkerOS only) Create a new "udev" rules file for the I2C on the Tinker Board by running the following command: @@ -159,8 +163,8 @@ Once you have compiled your code, you can upload your program and execute it on using the `scp` and `ssh` commands like this: ```sh -scp tinkerboard_blink linaro@192.168.1.xxx:/home/linaro/ -ssh -t linaro@192.168.1.xxx "./tinkerboard_blink" +scp tinkerboard_blink @192.168.1.xxx:/home// +ssh -t @192.168.1.xxx "./tinkerboard_blink" ``` ## Troubleshooting diff --git a/platforms/tinkerboard/adaptor.go b/platforms/tinkerboard/adaptor.go index 49f8eca72..e7a373c11 100644 --- a/platforms/tinkerboard/adaptor.go +++ b/platforms/tinkerboard/adaptor.go @@ -54,6 +54,7 @@ type Adaptor struct { *adaptors.PWMPinsAdaptor *adaptors.I2cBusAdaptor *adaptors.SpiBusAdaptor + *adaptors.OneWireBusAdaptor } // NewAdaptor creates a Tinkerboard Adaptor @@ -96,6 +97,7 @@ func NewAdaptor(opts ...interface{}) *Adaptor { a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) + a.OneWireBusAdaptor = adaptors.NewOneWireBusAdaptor(sys) return a } @@ -110,6 +112,10 @@ func (a *Adaptor) Connect() error { a.mutex.Lock() defer a.mutex.Unlock() + if err := a.OneWireBusAdaptor.Connect(); err != nil { + return err + } + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } @@ -150,6 +156,10 @@ func (a *Adaptor) Finalize() error { if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } + + if e := a.OneWireBusAdaptor.Finalize(); e != nil { + err = multierror.Append(err, e) + } return err } diff --git a/platforms/tinkerboard/adaptor_test.go b/platforms/tinkerboard/adaptor_test.go index 9667dcea1..77309b971 100644 --- a/platforms/tinkerboard/adaptor_test.go +++ b/platforms/tinkerboard/adaptor_test.go @@ -95,6 +95,7 @@ func TestNewAdaptor(t *testing.T) { assert.NotNil(t, a.PWMPinsAdaptor) assert.NotNil(t, a.I2cBusAdaptor) assert.NotNil(t, a.SpiBusAdaptor) + assert.NotNil(t, a.OneWireBusAdaptor) // act & assert a.SetName("NewName") assert.Equal(t, "NewName", a.Name()) diff --git a/system/GPIO.md b/system/GPIO.md index 66045b258..062de9b01 100644 --- a/system/GPIO.md +++ b/system/GPIO.md @@ -1,6 +1,6 @@ # GPIOs -This document describes some basics for developers. This is useful to understand programming in gobot's [digital pin driver](digital_pin.go). +This document describes some basics for developers. This is useful to understand programming in gobot's [digital pin driver](./digitalpin_access.go). ## GPIOs with sysfs diff --git a/system/ONEWIRE.md b/system/ONEWIRE.md new file mode 100644 index 000000000..382972233 --- /dev/null +++ b/system/ONEWIRE.md @@ -0,0 +1,71 @@ +# 1-wire bus + +This document describes some basics for developers. This is useful to understand programming in gobot's [1-wire driver](./onewiredevice_sysfs.go). + +## 1-wire with sysfs + +If the 1-wire bus is enabled on the board, the bus data can be connected to one pin of the used platform. The enabling +activates the Kernel drivers for common devices (family drivers), which are than mapped to the sysfs, see +. + +## Check available 1-wire devices + +Example for Tinkerboard (RK3288) with armbian and 3 connected temperature sensors DS18B20: + +```sh +ls -la /sys/bus/w1/devices/ +insgesamt 0 +drwxr-xr-x 2 root root 0 29. Okt 08:58 . +drwxr-xr-x 4 root root 0 29. Okt 08:58 .. +lrwxrwxrwx 1 root root 0 31. Okt 07:55 28-072261452f18 -> ../../../devices/w1_bus_master1/28-072261452f18 +lrwxrwxrwx 1 root root 0 31. Okt 07:55 28-08225482b0de -> ../../../devices/w1_bus_master1/28-08225482b0de +lrwxrwxrwx 1 root root 0 31. Okt 07:55 28-1e40710a6461 -> ../../../devices/w1_bus_master1/28-1e40710a6461 +lrwxrwxrwx 1 root root 0 29. Okt 08:58 w1_bus_master1 -> ../../../devices/w1_bus_master1 +``` + +Within a device folder different files are available for typical access. + +```sh +ls -la /sys/bus/w1/devices/28-072261452f18/ +insgesamt 0 +drwxr-xr-x 4 root root 0 29. Okt 08:58 . +drwxr-xr-x 6 root root 0 29. Okt 08:58 .. +-rw-r--r-- 1 root root 4096 31. Okt 07:57 alarms +-rw-r--r-- 1 root root 4096 31. Okt 07:57 conv_time +lrwxrwxrwx 1 root root 0 31. Okt 07:57 driver -> ../../../bus/w1/drivers/w1_slave_driver +--w------- 1 root root 4096 31. Okt 07:57 eeprom_cmd +-r--r--r-- 1 root root 4096 31. Okt 07:57 ext_power +-rw-r--r-- 1 root root 4096 31. Okt 07:57 features +drwxr-xr-x 3 root root 0 29. Okt 08:58 hwmon +-r--r--r-- 1 root root 4096 31. Okt 07:57 id +-r--r--r-- 1 root root 4096 31. Okt 07:57 name +drwxr-xr-x 2 root root 0 31. Okt 07:13 power +-rw-r--r-- 1 root root 4096 31. Okt 11:10 resolution +lrwxrwxrwx 1 root root 0 29. Okt 08:58 subsystem -> ../../../bus/w1 +-r--r--r-- 1 root root 4096 31. Okt 07:57 temperature +-rw-r--r-- 1 root root 4096 29. Okt 08:58 uevent +-rw-r--r-- 1 root root 4096 31. Okt 07:57 w1_slave +``` + +This files depends on the family driver. + +## Different access levels and modes + +Currently gobot supports only direct access to the devices in automatic search mode of the controller device. The +implementation is similar to the sysfs access of the analog pin driver. + +E.g. if the cyclic device search should be avoided, the access to the controller device is needed, see Kernel +documentation. If this will be implemented in the future, have in mind that more than one controller devices are +possible. The gobot's 1-wire architecture can be changed then similar to SPI or I2C. + +## Troubleshooting + +If something is not working, please check this points: + +Is the correct gpio used: `cat /sys/kernel/debug/gpio` +Does the base path exist: `ls /sys/bus/w1/` +Is the Kernel module loaded: `lsmod | grep wire` +Is the onewire support activated in the device tree: `dtc -I fs -O dts /sys/firmware/devicetree/base | grep onewire` +Is there an according overlay on the system: `locate w1-gpio` +Is the overlay loading configured: `cat /boot/armbianEnv.txt` +Is the content of the overlay correct: `dtc -I dtb -O dts -o -w1-gpio.dtbo.dts /boot/dtb/overlay/-w1-gpio.dtbo` diff --git a/system/fs_mock.go b/system/fs_mock.go index afcb11a3f..2ac77079d 100644 --- a/system/fs_mock.go +++ b/system/fs_mock.go @@ -149,7 +149,10 @@ func (fs *MockFilesystem) stat(name string) (os.FileInfo, error) { log.Println("A") return nil, err } - defer os.Remove(tmpFile.Name()) + defer func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }() return os.Stat(tmpFile.Name()) } diff --git a/system/onewiredevice_sysfs.go b/system/onewiredevice_sysfs.go new file mode 100644 index 000000000..846ea3d6d --- /dev/null +++ b/system/onewiredevice_sysfs.go @@ -0,0 +1,61 @@ +package system + +import ( + "path" +) + +type onewireDeviceSysfs struct { + id string + sysfsPath string + sfa *sysfsFileAccess +} + +func newOneWireDeviceSysfs(sfa *sysfsFileAccess, id string) *onewireDeviceSysfs { + p := &onewireDeviceSysfs{ + id: id, + sysfsPath: path.Join("/sys/bus/w1/devices", id), + sfa: sfa, + } + return p +} + +// ID returns the device id in the form "family code"-"serial number". Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) ID() string { + return o.id +} + +// ReadData reads from the sysfs path specified by the command. Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) ReadData(command string, data []byte) error { + p := path.Join(o.sysfsPath, command) + buf, err := o.sfa.read(p) + if err != nil { + return err + } + copy(data, buf) + + return nil +} + +// WriteData writes to the path specified by the command. Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) WriteData(command string, data []byte) error { + p := path.Join(o.sysfsPath, command) + return o.sfa.write(p, data) +} + +// ReadInteger reads an integer value from the device. Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) ReadInteger(command string) (int, error) { + p := path.Join(o.sysfsPath, command) + return o.sfa.readInteger(p) +} + +// WriteInteger writes an integer value to the device. Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) WriteInteger(command string, val int) error { + p := path.Join(o.sysfsPath, command) + return o.sfa.writeInteger(p, val) +} + +// Close the 1-wire connection. Implements gobot.OneWireSystemDevicer. +func (o *onewireDeviceSysfs) Close() error { + // currently nothing to do here - the file descriptors will be closed immediately after read/write + return nil +} diff --git a/system/system.go b/system/system.go index 210b6b001..63a865e9e 100644 --- a/system/system.go +++ b/system/system.go @@ -1,6 +1,7 @@ package system import ( + "fmt" "os" "unsafe" @@ -151,6 +152,15 @@ func (a *Accesser) NewSpiDevice(busNum, chipNum, mode, bits int, maxSpeed int64) return a.spiAccess.createDevice(busNum, chipNum, mode, bits, maxSpeed) } +// NewOneWireDevice returns a new 1-wire device with the given parameters. +// note: this is a basic implementation without using the possibilities of bus controller +// it depends on automatic device search, see https://www.kernel.org/doc/Documentation/w1/w1.generic +func (a *Accesser) NewOneWireDevice(familyCode byte, serialNumber uint64) (gobot.OneWireSystemDevicer, error) { + sfa := &sysfsFileAccess{fs: a.fs, readBufLen: 200} + deviceID := fmt.Sprintf("%02x-%012x", familyCode, serialNumber) + return newOneWireDeviceSysfs(sfa, deviceID), nil +} + // OpenFile opens file of given name from native or the mocked file system func (a *Accesser) OpenFile(name string, flag int, perm os.FileMode) (File, error) { return a.fs.openFile(name, flag, perm)