From 501a1e707cbadbb0825dab955a449974afad380a 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 --- adaptor.go | 32 ++++ drivers/onewire/ds18b20_driver.go | 218 ++++++++++++++++++++++++ drivers/onewire/onewire_connection.go | 69 ++++++++ drivers/onewire/onewire_driver.go | 131 ++++++++++++++ examples/tinkerboard_ds18b20.go | 80 +++++++++ platforms/adaptors/onewirebusadaptor.go | 77 +++++++++ platforms/tinkerboard/README.md | 38 +++-- platforms/tinkerboard/adaptor.go | 10 ++ system/GPIO.md | 2 +- system/ONEWIRE.md | 59 +++++++ system/digitalpin_poll.go | 3 + system/fs_mock.go | 5 +- system/onewiredevice_sysfs.go | 61 +++++++ system/system.go | 10 ++ 14 files changed, 776 insertions(+), 19 deletions(-) create mode 100644 drivers/onewire/ds18b20_driver.go create mode 100644 drivers/onewire/onewire_connection.go create mode 100644 drivers/onewire/onewire_driver.go create mode 100644 examples/tinkerboard_ds18b20.go create mode 100644 platforms/adaptors/onewirebusadaptor.go create mode 100644 system/ONEWIRE.md create mode 100644 system/onewiredevice_sysfs.go 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/drivers/onewire/ds18b20_driver.go b/drivers/onewire/ds18b20_driver.go new file mode 100644 index 000000000..0d4afb8d3 --- /dev/null +++ b/drivers/onewire/ds18b20_driver.go @@ -0,0 +1,218 @@ +package onewire + +import ( + "fmt" + "math" + "time" +) + +const ( + ds18b20DefaultResolution = 12 + ds18b20DefaultConversionTime = 750 +) + +// 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) +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, + }, + } + 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 drop setting). +// 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("temperature") + 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("resolution") + 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("ext_power") + 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("conv_time") + 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("conv_time", 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("resolution", int(d.ds18b20Cfg.resolution)); err != nil { + return err + } + } + + if d.ds18b20Cfg.conversionTime != ds18b20DefaultConversionTime { + return d.connection.WriteInteger("conv_time", int(d.ds18b20Cfg.conversionTime)) + } + + return nil +} + +func (d *DS18B20Driver) shutdown() error { + if d.ds18b20Cfg.resolution != ds18b20DefaultResolution { + return d.connection.WriteInteger("resolution", ds18b20DefaultResolution) + } + + if d.ds18b20Cfg.conversionTime != ds18b20DefaultConversionTime { + return d.connection.WriteInteger("conv_time", 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/onewire_connection.go b/drivers/onewire/onewire_connection.go new file mode 100644 index 000000000..2791f18f2 --- /dev/null +++ b/drivers/onewire/onewire_connection.go @@ -0,0 +1,69 @@ +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 +// and Implements gobot.BusOperations. +func NewConnection(onewireSystem gobot.OneWireSystemDevicer) *onewireConnection { + return &onewireConnection{onewireSystem: onewireSystem} +} + +// 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_driver.go b/drivers/onewire/onewire_driver.go new file mode 100644 index 000000000..9d81229e6 --- /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. +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/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..72bfa2fb5 --- /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} + 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/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/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..0dcf1f077 --- /dev/null +++ b/system/ONEWIRE.md @@ -0,0 +1,59 @@ +# 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. diff --git a/system/digitalpin_poll.go b/system/digitalpin_poll.go index 6526acfee..7516d0dc9 100644 --- a/system/digitalpin_poll.go +++ b/system/digitalpin_poll.go @@ -45,6 +45,9 @@ func startEdgePolling( for { select { case <-quitChan: + if !firstLoopDone { + wg.Done() + } return default: // note: pure reading takes between 30us and 1ms on rasperry Pi1, typically 50us, with sysfs also 500us 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)