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

This commit is contained in:
Thomas Kohler 2023-11-27 16:42:42 +01:00 committed by GitHub
parent d39848e368
commit d139c0ac7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 650 additions and 37 deletions

View File

@ -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:

View File

@ -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

View File

@ -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
@ -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.
@ -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) {
@ -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()
@ -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:
@ -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()

View File

@ -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()
@ -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()

View File

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

View File

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

View File

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

View File

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

View File

@ -33,6 +33,13 @@ type gpioPinDefinition struct {
cdev cdevPin
}
type analogPinDefinition struct {
path string
r bool // readable
w bool // writable
bufLen uint16
}
type pwmPinDefinition struct {
channel int
dir string
@ -46,6 +53,7 @@ type Adaptor struct {
gpioPinMap map[string]gpioPinDefinition
pwmPinMap map[string]pwmPinDefinition
mutex sync.Mutex
*adaptors.AnalogPinsAdaptor
*adaptors.DigitalPinsAdaptor
*adaptors.PWMPinsAdaptor
*adaptors.I2cBusAdaptor
@ -56,13 +64,13 @@ 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.WithGpiosOpenDrain/Source(pin's): sets the output behavior
// adaptors.WithGpioDebounce(pin, period): sets the input debouncer
// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection
// 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.WithGpiosOpenDrain/Source(pin's): sets the output behavior
// adaptors.WithGpioDebounce(pin, period): sets the input debouncer
// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection
func NewNeoAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
sys := system.NewAccesser(system.WithDigitalPinGpiodAccess())
c := &Adaptor{
@ -71,6 +79,7 @@ func NewNeoAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
gpioPinMap: neoGpioPins,
pwmPinMap: neoPwmPins,
}
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))
@ -99,6 +108,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
}
@ -116,6 +129,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)
}
@ -143,6 +160,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 := c.gpioPinMap[id]
if !ok {

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/drivers/aio"
"gobot.io/x/gobot/v2/drivers/gpio"
"gobot.io/x/gobot/v2/drivers/i2c"
"gobot.io/x/gobot/v2/system"
@ -58,6 +59,7 @@ var (
_ gpio.DigitalWriter = (*Adaptor)(nil)
_ gpio.PwmWriter = (*Adaptor)(nil)
_ gpio.ServoWriter = (*Adaptor)(nil)
_ aio.AnalogReader = (*Adaptor)(nil)
_ i2c.Connector = (*Adaptor)(nil)
)
@ -99,6 +101,29 @@ func TestDigitalIO(t *testing.T) {
require.NoError(t, a.Finalize())
}
func TestAnalog(t *testing.T) {
mockPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
}
a, fs := initTestAdaptorWithMockedFilesystem(mockPaths)
fs.Files["/sys/class/thermal/thermal_zone0/temp"].Contents = "567\n"
got, err := a.AnalogRead("thermal_zone0")
require.NoError(t, err)
assert.Equal(t, 567, got)
_, err = a.AnalogRead("thermal_zone10")
require.ErrorContains(t, err, "'thermal_zone10' is not a valid id for a analog pin")
fs.WithReadError = true
_, err = a.AnalogRead("thermal_zone0")
require.ErrorContains(t, err, "read error")
fs.WithReadError = false
require.NoError(t, a.Finalize())
}
func TestInvalidPWMPin(t *testing.T) {
a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths)
preparePwmFs(fs)
@ -342,6 +367,49 @@ func Test_translateDigitalPin(t *testing.T) {
}
}
func Test_translateAnalogPin(t *testing.T) {
mockedPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
"/sys/class/thermal/thermal_zone1/temp",
}
tests := map[string]struct {
id string
wantPath string
wantReadable bool
wantBufLen uint16
wantErr string
}{
"translate_thermal_zone0": {
id: "thermal_zone0",
wantPath: "/sys/class/thermal/thermal_zone0/temp",
wantReadable: true,
wantBufLen: 7,
},
"unknown_id": {
id: "thermal_zone1",
wantErr: "'thermal_zone1' is not a valid id for a analog pin",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, _ := initTestAdaptorWithMockedFilesystem(mockedPaths)
// act
path, r, w, buf, err := a.translateAnalogPin(tc.id)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantPath, path)
assert.Equal(t, tc.wantReadable, r)
assert.False(t, w)
assert.Equal(t, tc.wantBufLen, buf)
})
}
}
func Test_translatePWMPin(t *testing.T) {
basePaths := []string{"/sys/devices/platform/soc/1c21400.pwm/pwm/"}
tests := map[string]struct {

View File

@ -23,3 +23,8 @@ var neoPwmPins = map[string]pwmPinDefinition{
// UART_RXD0, GPIOA5, PWM
"PWM": {dir: "/sys/devices/platform/soc/1c21400.pwm/pwm/", dirRegexp: "pwmchip[0]$", channel: 0},
}
var analogPinDefinitions = map[string]analogPinDefinition{
// +/-273.200 °C need >=7 characters to read: +/-273200 millidegree Celsius
"thermal_zone0": {path: "/sys/class/thermal/thermal_zone0/temp", r: true, w: false, bufLen: 7},
}

View File

@ -24,6 +24,13 @@ const (
defaultSpiMaxSpeed = 500000
)
type analogPinDefinition struct {
path string
r bool // readable
w bool // writable
bufLen uint16
}
// Adaptor is the Gobot Adaptor for the Raspberry Pi
type Adaptor struct {
name string
@ -31,6 +38,7 @@ type Adaptor struct {
sys *system.Accesser
revision string
pwmPins map[string]gobot.PWMPinner
*adaptors.AnalogPinsAdaptor
*adaptors.DigitalPinsAdaptor
*adaptors.I2cBusAdaptor
*adaptors.SpiBusAdaptor
@ -41,13 +49,13 @@ 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.WithGpiosOpenDrain/Source(pin's): sets the output behavior
// adaptors.WithGpioDebounce(pin, period): sets the input debouncer
// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection
// 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.WithGpiosOpenDrain/Source(pin's): sets the output behavior
// adaptors.WithGpioDebounce(pin, period): sets the input debouncer
// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection
func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
sys := system.NewAccesser(system.WithDigitalPinGpiodAccess())
c := &Adaptor{
@ -55,6 +63,7 @@ func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
sys: sys,
PiBlasterPeriod: 10000000,
}
c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin)
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.getPinTranslatorFunction(), opts...)
c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, 1)
c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber,
@ -91,6 +100,10 @@ func (c *Adaptor) Connect() error {
return err
}
if err := c.AnalogPinsAdaptor.Connect(); err != nil {
return err
}
c.pwmPins = make(map[string]gobot.PWMPinner)
return c.DigitalPinsAdaptor.Connect()
}
@ -111,6 +124,10 @@ func (c *Adaptor) Finalize() error {
}
c.pwmPins = nil
if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
if e := c.I2cBusAdaptor.Finalize(); e != nil {
err = multierror.Append(err, e)
}
@ -184,6 +201,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) getPinTranslatorFunction() func(string) (string, int, error) {
return func(pin string) (string, int, error) {
var line int

View File

@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/drivers/aio"
"gobot.io/x/gobot/v2/drivers/gpio"
"gobot.io/x/gobot/v2/drivers/i2c"
"gobot.io/x/gobot/v2/drivers/spi"
@ -27,6 +28,7 @@ var (
_ gpio.DigitalWriter = (*Adaptor)(nil)
_ gpio.PwmWriter = (*Adaptor)(nil)
_ gpio.ServoWriter = (*Adaptor)(nil)
_ aio.AnalogReader = (*Adaptor)(nil)
_ i2c.Connector = (*Adaptor)(nil)
_ spi.Connector = (*Adaptor)(nil)
)
@ -108,6 +110,29 @@ func TestFinalize(t *testing.T) {
require.NoError(t, a.Finalize())
}
func TestAnalog(t *testing.T) {
mockPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
}
a, fs := initTestAdaptorWithMockedFilesystem(mockPaths)
fs.Files["/sys/class/thermal/thermal_zone0/temp"].Contents = "567\n"
got, err := a.AnalogRead("thermal_zone0")
require.NoError(t, err)
assert.Equal(t, 567, got)
_, err = a.AnalogRead("thermal_zone10")
require.ErrorContains(t, err, "'thermal_zone10' is not a valid id for a analog pin")
fs.WithReadError = true
_, err = a.AnalogRead("thermal_zone0")
require.ErrorContains(t, err, "read error")
fs.WithReadError = false
require.NoError(t, a.Finalize())
}
func TestDigitalPWM(t *testing.T) {
mockedPaths := []string{"/dev/pi-blaster"}
a, fs := initTestAdaptorWithMockedFilesystem(mockedPaths)
@ -339,3 +364,46 @@ func Test_validateI2cBusNumber(t *testing.T) {
})
}
}
func Test_translateAnalogPin(t *testing.T) {
mockedPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
"/sys/class/thermal/thermal_zone1/temp",
}
tests := map[string]struct {
id string
wantPath string
wantReadable bool
wantBufLen uint16
wantErr string
}{
"translate_thermal_zone0": {
id: "thermal_zone0",
wantPath: "/sys/class/thermal/thermal_zone0/temp",
wantReadable: true,
wantBufLen: 7,
},
"unknown_id": {
id: "thermal_zone1",
wantErr: "'thermal_zone1' is not a valid id for a analog pin",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, _ := initTestAdaptorWithMockedFilesystem(mockedPaths)
// act
path, r, w, buf, err := a.translateAnalogPin(tc.id)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantPath, path)
assert.Equal(t, tc.wantReadable, r)
assert.False(t, w)
assert.Equal(t, tc.wantBufLen, buf)
})
}
}

View File

@ -86,3 +86,8 @@ var pins = map[string]map[string]int{
"3": 21,
},
}
var analogPinDefinitions = map[string]analogPinDefinition{
// +/-273.200 °C need >=7 characters to read: +/-273200 millidegree Celsius
"thermal_zone0": {path: "/sys/class/thermal/thermal_zone0/temp", r: true, w: false, bufLen: 7},
}

View File

@ -23,10 +23,27 @@ const (
defaultSpiMaxSpeed = 500000
)
type cdevPin struct {
chip uint8
line uint8
}
type gpioPinDefinition struct {
sysfs int
cdev cdevPin
}
type analogPinDefinition struct {
path string
r bool // readable
w bool // writable
bufLen uint16
}
type pwmPinDefinition struct {
channel int
dir string
dirRegexp string
channel int
}
// Adaptor represents a Gobot Adaptor for the ASUS Tinker Board
@ -34,6 +51,7 @@ type Adaptor struct {
name string
sys *system.Accesser
mutex sync.Mutex
*adaptors.AnalogPinsAdaptor
*adaptors.DigitalPinsAdaptor
*adaptors.PWMPinsAdaptor
*adaptors.I2cBusAdaptor
@ -44,10 +62,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)
@ -57,6 +75,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))
@ -85,6 +104,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
}
@ -102,6 +125,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)
}
@ -130,6 +157,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 {

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require"
"gobot.io/x/gobot/v2"
"gobot.io/x/gobot/v2/drivers/aio"
"gobot.io/x/gobot/v2/drivers/gpio"
"gobot.io/x/gobot/v2/drivers/i2c"
"gobot.io/x/gobot/v2/system"
@ -58,6 +59,7 @@ var (
_ gpio.DigitalWriter = (*Adaptor)(nil)
_ gpio.PwmWriter = (*Adaptor)(nil)
_ gpio.ServoWriter = (*Adaptor)(nil)
_ aio.AnalogReader = (*Adaptor)(nil)
_ i2c.Connector = (*Adaptor)(nil)
)
@ -99,6 +101,29 @@ func TestDigitalIO(t *testing.T) {
require.NoError(t, a.Finalize())
}
func TestAnalog(t *testing.T) {
mockPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
}
a, fs := initTestAdaptorWithMockedFilesystem(mockPaths)
fs.Files["/sys/class/thermal/thermal_zone0/temp"].Contents = "567\n"
got, err := a.AnalogRead("thermal_zone0")
require.NoError(t, err)
assert.Equal(t, 567, got)
_, err = a.AnalogRead("thermal_zone10")
require.ErrorContains(t, err, "'thermal_zone10' is not a valid id for a analog pin")
fs.WithReadError = true
_, err = a.AnalogRead("thermal_zone0")
require.ErrorContains(t, err, "read error")
fs.WithReadError = false
require.NoError(t, a.Finalize())
}
func TestInvalidPWMPin(t *testing.T) {
a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths)
preparePwmFs(fs)
@ -355,6 +380,55 @@ func Test_translateDigitalPin(t *testing.T) {
}
}
func Test_translateAnalogPin(t *testing.T) {
mockedPaths := []string{
"/sys/class/thermal/thermal_zone0/temp",
"/sys/class/thermal/thermal_zone1/temp",
}
tests := map[string]struct {
id string
wantPath string
wantReadable bool
wantBufLen uint16
wantErr string
}{
"translate_thermal_zone0": {
id: "thermal_zone0",
wantPath: "/sys/class/thermal/thermal_zone0/temp",
wantReadable: true,
wantBufLen: 7,
},
"translate_thermal_zone1": {
id: "thermal_zone1",
wantPath: "/sys/class/thermal/thermal_zone1/temp",
wantReadable: true,
wantBufLen: 7,
},
"unknown_id": {
id: "99",
wantErr: "'99' is not a valid id for a analog pin",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, _ := initTestAdaptorWithMockedFilesystem(mockedPaths)
// act
path, r, w, buf, err := a.translateAnalogPin(tc.id)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantPath, path)
assert.Equal(t, tc.wantReadable, r)
assert.False(t, w)
assert.Equal(t, tc.wantBufLen, buf)
})
}
}
func Test_translatePWMPin(t *testing.T) {
basePaths := []string{
"/sys/devices/platform/ff680020.pwm/pwm/",

View File

@ -1,15 +1,5 @@
package tinkerboard
type cdevPin struct {
chip uint8
line uint8
}
type gpioPinDefinition struct {
sysfs int
cdev cdevPin
}
// notes for character device
// pins: A=0+Nr, B=8+Nr, C=16+Nr
// tested: armbian Linux, OK: work as input and output, IN: work only as input
@ -50,3 +40,9 @@ var pwmPinDefinitions = map[string]pwmPinDefinition{
// GPIO7_C7_UART2TX_PWM3
"32": {dir: "/sys/devices/platform/ff680030.pwm/pwm/", dirRegexp: "pwmchip[0|1|2|3]$", channel: 0},
}
var analogPinDefinitions = map[string]analogPinDefinition{
// +/-273.200 °C need >=7 characters to read: +/-273200 millidegree Celsius
"thermal_zone0": {path: "/sys/class/thermal/thermal_zone0/temp", r: true, w: false, bufLen: 7},
"thermal_zone1": {path: "/sys/class/thermal/thermal_zone1/temp", r: true, w: false, bufLen: 7},
}