Skip to content

Commit

Permalink
aio(thermalzone): add driver for read a thermalzone from system
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas committed Nov 26, 2023
1 parent d39848e commit 3febcf1
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 15 deletions.
11 changes: 8 additions & 3 deletions drivers/aio/analog_sensor_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type sensorScaleOption struct {
scaler func(input int) (value float64)
}

// AnalogSensorDriver represents an Analog Sensor
// AnalogSensorDriver represents an analog sensor
type AnalogSensorDriver struct {
*driver
sensorCfg *sensorConfiguration
Expand All @@ -35,6 +35,7 @@ type AnalogSensorDriver struct {
gobot.Eventer
lastRawValue int
lastValue float64
analogRead func() (int, float64, error)
}

// NewAnalogSensorDriver returns a new driver for analog sensors, given an AnalogReader and pin.
Expand All @@ -60,6 +61,7 @@ func NewAnalogSensorDriver(a AnalogReader, pin string, opts ...interface{}) *Ana
}
d.afterStart = d.initialize
d.beforeHalt = d.shutdown
d.analogRead = d.analogSensorRead

for _, opt := range opts {
switch o := opt.(type) {
Expand Down Expand Up @@ -168,6 +170,7 @@ func (a *AnalogSensorDriver) initialize() error {
go func() {
timer := time.NewTimer(a.sensorCfg.readInterval)
timer.Stop()

for {
// please note, that this ensures the first read is done immediately, but has drawbacks, see notes above
rawValue, value, err := a.analogRead()
Expand All @@ -183,6 +186,7 @@ func (a *AnalogSensorDriver) initialize() error {
oldValue = value
}
}

timer.Reset(a.sensorCfg.readInterval) // ensure that after each read is a wait, independent of duration of read
select {
case <-timer.C:
Expand All @@ -205,8 +209,9 @@ func (a *AnalogSensorDriver) shutdown() error {
return nil
}

// analogRead performs an reading from the sensor and sets the internal attributes and returns the raw and scaled value
func (a *AnalogSensorDriver) analogRead() (int, float64, error) {
// analogSensorRead performs an reading from the sensor, sets the internal attributes and returns
// the raw and scaled value
func (a *AnalogSensorDriver) analogSensorRead() (int, float64, error) {
a.mutex.Lock()
defer a.mutex.Unlock()

Expand Down
4 changes: 2 additions & 2 deletions drivers/aio/temperature_sensor_driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestTemperatureSensorDriver_LinearScaler(t *testing.T) {
}
}

func TestTemperatureSensorPublishesTemperatureInCelsius(t *testing.T) {
func TestTemperatureSensorWithSensorCyclicRead_PublishesTemperatureInCelsius(t *testing.T) {
// arrange
sem := make(chan bool)
a := newAioTestAdaptor()
Expand All @@ -155,7 +155,7 @@ func TestTemperatureSensorPublishesTemperatureInCelsius(t *testing.T) {
assert.InDelta(t, 31.61532462352477, d.Value(), 0.0)
}

func TestTemperatureSensorWithSensorCyclicReadPublishesError(t *testing.T) {
func TestTemperatureSensorWithSensorCyclicRead_PublishesError(t *testing.T) {
// arrange
sem := make(chan bool)
a := newAioTestAdaptor()
Expand Down
83 changes: 83 additions & 0 deletions drivers/aio/thermalzone_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package aio

import (
"fmt"

"gobot.io/x/gobot/v2"
)

// thermalZoneOptionApplier needs to be implemented by each configurable option type
type thermalZoneOptionApplier interface {
apply(cfg *thermalZoneConfiguration)
}

// thermalZoneConfiguration contains all changeable attributes of the driver.
type thermalZoneConfiguration struct {
scaleUnit func(float64) float64
}

// thermalZoneUnitscalerOption is the type for applying another unit scaler to the configuration
type thermalZoneUnitscalerOption struct {
unitscaler func(float64) float64
}

// ThermalZoneDriver represents an driver for reading the system thermal zone temperature
type ThermalZoneDriver struct {
*AnalogSensorDriver
thermalZoneCfg *thermalZoneConfiguration
}

// NewThermalZoneDriver is a driver for reading the system thermal zone temperature, given an AnalogReader and zone id.
//
// Supported options: see also [aio.NewAnalogSensorDriver]
//
// "WithFahrenheit()"
//
// Adds the following API Commands: see [aio.NewAnalogSensorDriver]
func NewThermalZoneDriver(a AnalogReader, zoneID string, opts ...interface{}) *ThermalZoneDriver {
degreeScaler := func(input int) float64 { return float64(input) / 1000 }
d := ThermalZoneDriver{
AnalogSensorDriver: NewAnalogSensorDriver(a, zoneID, WithSensorScaler(degreeScaler)),
thermalZoneCfg: &thermalZoneConfiguration{
scaleUnit: func(input float64) float64 { return input }, // 1:1 in °C
},
}
d.driverCfg.name = gobot.DefaultName("ThermalZone")
d.analogRead = d.thermalZoneRead

for _, opt := range opts {
switch o := opt.(type) {
case optionApplier:
o.apply(d.driverCfg)
case sensorOptionApplier:
o.apply(d.sensorCfg)
case thermalZoneOptionApplier:
o.apply(d.thermalZoneCfg)
default:
panic(fmt.Sprintf("'%s' can not be applied on '%s'", opt, d.driverCfg.name))
}
}

return &d
}

// WithFahrenheit substitute the default 1:1 °C scaler by a scaler for °F
func WithFahrenheit() thermalZoneOptionApplier {
// (1°C × 9/5) + 32 = 33,8°F
unitscaler := func(input float64) float64 { return input*9.0/5.0 + 32.0 }
return thermalZoneUnitscalerOption{unitscaler: unitscaler}
}

// thermalZoneRead overrides and extends the analogSensorRead() function to add the unit scaler
func (d *ThermalZoneDriver) thermalZoneRead() (int, float64, error) {
if _, _, err := d.analogSensorRead(); err != nil {
return 0, 0, err
}
// apply unit scaler on value
d.lastValue = d.thermalZoneCfg.scaleUnit(d.lastValue)
return d.lastRawValue, d.lastValue, nil
}

func (o thermalZoneUnitscalerOption) apply(cfg *thermalZoneConfiguration) {
cfg.scaleUnit = o.unitscaler
}
89 changes: 89 additions & 0 deletions drivers/aio/thermalzone_driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package aio

import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewThermalZoneDriver(t *testing.T) {
// arrange
const pin = "thermal_zone0"
a := newAioTestAdaptor()
// act
d := NewThermalZoneDriver(a, pin)
// assert: driver attributes
assert.IsType(t, &ThermalZoneDriver{}, d)
assert.NotNil(t, d.driverCfg)
assert.True(t, strings.HasPrefix(d.Name(), "ThermalZone"))
assert.Equal(t, a, d.Connection())
require.NoError(t, d.afterStart())
require.NoError(t, d.beforeHalt())
assert.NotNil(t, d.Commander)
assert.NotNil(t, d.mutex)
// assert: sensor attributes
assert.Equal(t, pin, d.Pin())
assert.InDelta(t, 0.0, d.lastValue, 0, 0)
assert.Equal(t, 0, d.lastRawValue)
assert.NotNil(t, d.halt)
assert.NotNil(t, d.Eventer)
require.NotNil(t, d.sensorCfg)
assert.Equal(t, time.Duration(0), d.sensorCfg.readInterval)
assert.NotNil(t, d.sensorCfg.scale)
// assert: thermal zone attributes
require.NotNil(t, d.thermalZoneCfg)
require.NotNil(t, d.thermalZoneCfg.scaleUnit)
assert.InDelta(t, 1.0, d.thermalZoneCfg.scaleUnit(1), 0.0)
}

func TestNewThermalZoneDriver_options(t *testing.T) {
// This is a general test, that options are applied in constructor by using the common WithName() option, least one
// option of this driver and one of another driver (which should lead to panic). Further tests for options can also
// be done by call of "WithOption(val).apply(cfg)".
// arrange
const (
myName = "outlet temperature"
cycReadDur = 10 * time.Millisecond
)
panicFunc := func() {
NewThermalZoneDriver(newAioTestAdaptor(), "1", WithName("crazy"),
WithActuatorScaler(func(float64) int { return 0 }))
}
// act
d := NewThermalZoneDriver(newAioTestAdaptor(), "1",
WithName(myName),
WithSensorCyclicRead(cycReadDur),
WithFahrenheit())
// assert
assert.Equal(t, cycReadDur, d.sensorCfg.readInterval)
assert.InDelta(t, 33.8, d.thermalZoneCfg.scaleUnit(1), 0.0) // (1°C × 9/5) + 32 = 33,8°F
assert.Equal(t, myName, d.Name())
assert.PanicsWithValue(t, "'scaler option for analog actuators' can not be applied on 'crazy'", panicFunc)
}

func TestThermalZoneWithSensorCyclicRead_PublishesTemperatureInFahrenheit(t *testing.T) {
// arrange
sem := make(chan bool)
a := newAioTestAdaptor()
d := NewThermalZoneDriver(a, "1", WithSensorCyclicRead(10*time.Millisecond), WithFahrenheit())
a.analogReadFunc = func() (int, error) {
return -100000, nil // -100.000 °C
}
_ = d.Once(d.Event(Value), func(data interface{}) {
//nolint:forcetypeassert // ok here
assert.InDelta(t, -148.0, data.(float64), 0.0)
sem <- true
})
require.NoError(t, d.Start())

select {
case <-sem:
case <-time.After(1 * time.Second):
t.Errorf(" Temperature Sensor Event \"Data\" was not published")
}

assert.InDelta(t, -148.0, d.Value(), 0.0)
}
50 changes: 50 additions & 0 deletions examples/tinkerboard_thermalzone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//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/aio"
"gobot.io/x/gobot/v2/platforms/tinkerboard"
)

// Wiring: no wiring needed
func main() {
adaptor := tinkerboard.NewAdaptor()
therm0 := aio.NewThermalZoneDriver(adaptor, "thermal_zone0")
therm1 := aio.NewThermalZoneDriver(adaptor, "thermal_zone1", aio.WithFahrenheit())

work := func() {
gobot.Every(500*time.Millisecond, func() {
t0, err := therm0.Read()
if err != nil {
log.Println(err)
}

t1, err := therm1.Read()
if err != nil {
log.Println(err)
}

fmt.Printf("Zone 0: %2.3f °C, Zone 1: %2.3f °F\n", t0, t1)
})
}

robot := gobot.NewRobot("thermalBot",
[]gobot.Connection{adaptor},
[]gobot.Device{therm0, therm1},
work,
)

if err := robot.Start(); err != nil {
panic(err)
}
}
42 changes: 32 additions & 10 deletions platforms/tinkerboard/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,12 @@ const (
defaultSpiMaxSpeed = 500000
)

type pwmPinDefinition struct {
channel int
dir string
dirRegexp string
}

// Adaptor represents a Gobot Adaptor for the ASUS Tinker Board
type Adaptor struct {
name string
sys *system.Accesser
mutex sync.Mutex
*adaptors.AnalogPinsAdaptor
*adaptors.DigitalPinsAdaptor
*adaptors.PWMPinsAdaptor
*adaptors.I2cBusAdaptor
Expand All @@ -44,10 +39,10 @@ type Adaptor struct {
//
// Optional parameters:
//
// adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs (still used by default)
// adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.#
// adaptors.WithGpiosActiveLow(pin's): invert the pin behavior
// adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor
// adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs (still used by default)
// adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.#
// adaptors.WithGpiosActiveLow(pin's): invert the pin behavior
// adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor
//
// note from RK3288 datasheet: "The pull direction (pullup or pulldown) for all of GPIOs are software-programmable", but
// the latter is not working for any pin (armbian 22.08.7)
Expand All @@ -57,6 +52,7 @@ func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
name: gobot.DefaultName("Tinker Board"),
sys: sys,
}
c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin)
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...)
c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin,
adaptors.WithPolarityInvertedIdentifier(pwmInvertedIdentifier))
Expand Down Expand Up @@ -85,6 +81,10 @@ func (c *Adaptor) Connect() error {
return err
}

if err := c.AnalogPinsAdaptor.Connect(); err != nil {
return err
}

if err := c.PWMPinsAdaptor.Connect(); err != nil {
return err
}
Expand All @@ -102,6 +102,10 @@ func (c *Adaptor) Finalize() error {
err = multierror.Append(err, e)
}

if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}

if e := c.I2cBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
Expand Down Expand Up @@ -130,6 +134,24 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error {
return nil
}

func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) {
pinInfo, ok := analogPinDefinitions[id]
if !ok {
return "", false, false, 0, fmt.Errorf("'%s' is not a valid id for a analog pin", id)
}

path := pinInfo.path
info, err := c.sys.Stat(path)
if err != nil {
return "", false, false, 0, fmt.Errorf("Error (%v) on access '%s'", err, path)
}
if info.IsDir() {
return "", false, false, 0, fmt.Errorf("The item '%s' is a directory, which is not expected", path)
}

return path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil
}

func (c *Adaptor) translateDigitalPin(id string) (string, int, error) {
pindef, ok := gpioPinDefinitions[id]
if !ok {
Expand Down
Loading

0 comments on commit 3febcf1

Please sign in to comment.