Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aio(thermalzone): add driver for read a thermalzone from system #1040

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,15 @@ Support for many devices that use Analog Input/Output (AIO) have
a shared set of drivers provided using the `gobot/drivers/aio` package:

- [AIO](https://en.wikipedia.org/wiki/Analog-to-digital_converter) <=> [Drivers](https://github.com/hybridgroup/gobot/tree/master/drivers/aio)
- Analog Actuator
- Analog Sensor
- Grove Light Sensor
- Grove Piezo Vibration Sensor
- Grove Rotary Dial
- Grove Sound Sensor
- Grove Temperature Sensor
- Temperature Sensor (supports linear and NTC thermistor in normal and inverse mode)
- Thermal Zone Temperature Sensor

Support for devices that use Inter-Integrated Circuit (I2C) have a shared set of
drivers provided using the `gobot/drivers/i2c` package:
Expand Down
6 changes: 3 additions & 3 deletions drivers/aio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ Please refer to the main [README.md](https://github.com/hybridgroup/gobot/blob/r

Gobot has a extensible system for connecting to hardware devices. The following AIO devices are currently supported:

- Analog Sensor
- Analog Actuator
- Analog Sensor
- Grove Light Sensor
- Grove Piezo Vibration Sensor
- Grove Rotary Dial
- Grove Sound Sensor
- Grove Temperature Sensor
- Temperature Sensor (supports linear and NTC thermistor in normal and inverse mode)

More drivers are coming soon...
- Thermal Zone Temperature Sensor
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
}
91 changes: 91 additions & 0 deletions drivers/aio/thermalzone_driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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.Nil(t, d.halt) // will be created on initialize, if cyclic reading is on
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
}
// act: start cyclic reading
require.NoError(t, d.Start())
// assert
_ = d.Once(d.Event(Value), func(data interface{}) {
//nolint:forcetypeassert // ok here
assert.InDelta(t, -148.0, data.(float64), 0.0)
sem <- true
})

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/raspi_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/raspi"
)

// Wiring: no wiring needed
func main() {
adaptor := raspi.NewAdaptor()
therm0C := aio.NewThermalZoneDriver(adaptor, "thermal_zone0")
therm0F := aio.NewThermalZoneDriver(adaptor, "thermal_zone0", aio.WithFahrenheit())

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

t0F, err := therm0F.Read()
if err != nil {
log.Println(err)
}

fmt.Printf("Zone 0: %2.3f °C, %2.3f °F\n", t0C, t0F)
})
}

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

if err := robot.Start(); err != nil {
panic(err)
}
}
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