Skip to content

Commit

Permalink
onewire(ds18b20): introduce 1-wire device access by sysfs and temp dr…
Browse files Browse the repository at this point in the history
…iver (#1091)
  • Loading branch information
gen2thomas authored Dec 11, 2024
1 parent 53d791a commit 9dfcfc5
Show file tree
Hide file tree
Showing 24 changed files with 1,640 additions and 23 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions drivers/i2c/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (t *i2cTestAdaptor) ReadByte() (byte, error) {
return 0, err
}
val := bytes[0]

return val, nil
}

Expand All @@ -80,6 +81,7 @@ func (t *i2cTestAdaptor) ReadByteData(reg uint8) (uint8, error) {
return 0, err
}
val := bytes[0]

return val, nil
}

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

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

Expand Down Expand Up @@ -153,6 +157,7 @@ func (t *i2cTestAdaptor) WriteBytes(b []byte) error {
if len(b) > 32 {
b = b[:32]
}

return t.writeBytes(b)
}

Expand All @@ -162,6 +167,7 @@ func (t *i2cTestAdaptor) GetI2cConnection(address int, bus int) (Connection, err
}
t.bus = bus
t.address = address

return t, nil
}

Expand Down Expand Up @@ -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
}

Expand All @@ -204,5 +211,6 @@ func (t *i2cTestAdaptor) writeBytes(b []byte) error {
if err != nil {
return err
}

return nil
}
15 changes: 15 additions & 0 deletions drivers/onewire/README.md
Original file line number Diff line number Diff line change
@@ -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
227 changes: 227 additions & 0 deletions drivers/onewire/ds18b20_driver.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 9dfcfc5

Please sign in to comment.