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
  • Loading branch information
gen2thomas committed Nov 7, 2024
1 parent b5f5ac6 commit 501a1e7
Show file tree
Hide file tree
Showing 14 changed files with 776 additions and 19 deletions.
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
218 changes: 218 additions & 0 deletions drivers/onewire/ds18b20_driver.go
Original file line number Diff line number Diff line change
@@ -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)
}
69 changes: 69 additions & 0 deletions drivers/onewire/onewire_connection.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit 501a1e7

Please sign in to comment.