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 3980845 commit 475845d
Show file tree
Hide file tree
Showing 28 changed files with 1,292 additions and 351 deletions.
8 changes: 8 additions & 0 deletions adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ type PWMPinnerProvider interface {
PWMPin(id string) (PWMPinner, error)
}

// AnalogPinner is the interface for system analog io interactions
type AnalogPinner interface {
// Read reads the current value of the pin
Read() (int, error)
// Write writes to the pin
Write(val int) error
}

// I2cSystemDevicer is the interface to a i2c bus at system level, according to I2C/SMBus specification.
// Some functions are not in the interface yet:
// * Process Call (WriteWordDataReadWordData)
Expand Down
71 changes: 37 additions & 34 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 @@ -61,6 +62,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 @@ -112,6 +114,37 @@ func (a *AnalogSensorDriver) SetScaler(scaler func(int) float64) {
WithSensorScaler(scaler).apply(a.sensorCfg)
}

// Pin returns the AnalogSensorDrivers pin
func (a *AnalogSensorDriver) Pin() string { return a.pin }

// Read returns the current reading from the sensor, scaled by the current scaler
func (a *AnalogSensorDriver) Read() (float64, error) {
_, value, err := a.analogRead()
return value, err
}

// ReadRaw returns the current reading from the sensor without scaling
func (a *AnalogSensorDriver) ReadRaw() (int, error) {
rawValue, _, err := a.analogRead()
return rawValue, err
}

// Value returns the last read value from the sensor
func (a *AnalogSensorDriver) Value() float64 {
a.mutex.Lock()
defer a.mutex.Unlock()

return a.lastValue
}

// RawValue returns the last read raw value from the sensor
func (a *AnalogSensorDriver) RawValue() int {
a.mutex.Lock()
defer a.mutex.Unlock()

return a.lastRawValue
}

// initialize the AnalogSensorDriver and if the cyclic reading is active, reads the sensor at the given interval.
// Emits the Events:
//
Expand Down Expand Up @@ -165,39 +198,9 @@ func (a *AnalogSensorDriver) shutdown() error {
return nil
}

// Pin returns the AnalogSensorDrivers pin
func (a *AnalogSensorDriver) Pin() string { return a.pin }

// Read returns the current reading from the sensor, scaled by the current scaler
func (a *AnalogSensorDriver) Read() (float64, error) {
_, value, err := a.analogRead()
return value, err
}

// ReadRaw returns the current reading from the sensor without scaling
func (a *AnalogSensorDriver) ReadRaw() (int, error) {
rawValue, _, err := a.analogRead()
return rawValue, err
}

// Value returns the last read value from the sensor
func (a *AnalogSensorDriver) Value() float64 {
a.mutex.Lock()
defer a.mutex.Unlock()

return a.lastValue
}

// RawValue returns the last read raw value from the sensor
func (a *AnalogSensorDriver) RawValue() int {
a.mutex.Lock()
defer a.mutex.Unlock()

return a.lastRawValue
}

// 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 @@ -154,7 +154,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)
}
}
Loading

0 comments on commit 475845d

Please sign in to comment.