Skip to content

Commit

Permalink
platforms(adaptors): add a generic analog pin adaptor (hybridgroup#1041)
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas authored Nov 26, 2023
1 parent 916c2ba commit d39848e
Show file tree
Hide file tree
Showing 20 changed files with 915 additions and 305 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
95 changes: 95 additions & 0 deletions platforms/adaptors/analogpinsadaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package adaptors

import (
"fmt"
"sync"

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

type analogPinTranslator func(pin string) (path string, r, w bool, bufLen uint16, err error)

// AnalogPinsAdaptor is a adaptor for analog pins, normally used for composition in platforms.
// It is also usable for general sysfs access.
type AnalogPinsAdaptor struct {
sys *system.Accesser
translate analogPinTranslator
pins map[string]gobot.AnalogPinner
mutex sync.Mutex
}

// NewAnalogPinsAdaptor provides the access to analog pins of the board. Usually sysfs system drivers are used.
// The translator is used to adapt the pin header naming, which is given by user, to the internal file name
// nomenclature. This varies by each platform.
func NewAnalogPinsAdaptor(sys *system.Accesser, t analogPinTranslator) *AnalogPinsAdaptor {
a := AnalogPinsAdaptor{
sys: sys,
translate: t,
}
return &a
}

// Connect prepare new connection to analog pins.
func (a *AnalogPinsAdaptor) Connect() error {
a.mutex.Lock()
defer a.mutex.Unlock()

a.pins = make(map[string]gobot.AnalogPinner)
return nil
}

// Finalize closes connection to analog pins
func (a *AnalogPinsAdaptor) Finalize() error {
a.mutex.Lock()
defer a.mutex.Unlock()

a.pins = nil
return nil
}

// AnalogRead returns an analog value from specified pin or identifier, defined by the translation function.
func (a *AnalogPinsAdaptor) AnalogRead(id string) (int, error) {
a.mutex.Lock()
defer a.mutex.Unlock()

pin, err := a.analogPin(id)
if err != nil {
return 0, err
}

return pin.Read()
}

// AnalogWrite writes an analog value to the specified pin or identifier, defined by the translation function.
func (a *AnalogPinsAdaptor) AnalogWrite(id string, val int) error {
a.mutex.Lock()
defer a.mutex.Unlock()

pin, err := a.analogPin(id)
if err != nil {
return err
}

return pin.Write(val)
}

// analogPin initializes the pin for analog access and returns matched pin for specified identifier.
func (a *AnalogPinsAdaptor) analogPin(id string) (gobot.AnalogPinner, error) {
if a.pins == nil {
return nil, fmt.Errorf("not connected for pin %s", id)
}

pin := a.pins[id]

if pin == nil {
path, r, w, bufLen, err := a.translate(id)
if err != nil {
return nil, err
}
pin = a.sys.NewAnalogPin(path, r, w, bufLen)
a.pins[id] = pin
}

return pin, nil
}
249 changes: 249 additions & 0 deletions platforms/adaptors/analogpinsadaptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//nolint:nonamedreturns // ok for tests
package adaptors

import (
"fmt"
"testing"

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

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

const (
analogReadPath = "/sys/bus/iio/devices/iio:device0/in_voltage0_raw"
analogWritePath = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/export"
analogReadWritePath = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/period"
analogReadWriteStringPath = "/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/pwm44/polarity"
)

var analogMockPaths = []string{
analogReadPath,
analogWritePath,
analogReadWritePath,
analogReadWriteStringPath,
}

func initTestAnalogPinsAdaptorWithMockedFilesystem(mockPaths []string) (*AnalogPinsAdaptor, *system.MockFilesystem) {
sys := system.NewAccesser()
fs := sys.UseMockFilesystem(mockPaths)
a := NewAnalogPinsAdaptor(sys, testAnalogPinTranslator)
fs.Files[analogReadPath].Contents = "54321"
fs.Files[analogWritePath].Contents = "0"
fs.Files[analogReadWritePath].Contents = "30000"
fs.Files[analogReadWriteStringPath].Contents = "inverted"
if err := a.Connect(); err != nil {
panic(err)
}
return a, fs
}

func testAnalogPinTranslator(id string) (string, bool, bool, uint16, error) {
switch id {
case "read":
return analogReadPath, true, false, 10, nil
case "write":
return analogWritePath, false, true, 11, nil
case "read/write":
return analogReadWritePath, true, true, 12, nil
case "read/write_string":
return analogReadWriteStringPath, true, true, 13, nil
}

return "", false, false, 0, fmt.Errorf("'%s' is not a valid id of a analog pin", id)
}

func TestAnalogPinsConnect(t *testing.T) {
translate := func(id string) (path string, r, w bool, bufLen uint16, err error) { return }
a := NewAnalogPinsAdaptor(system.NewAccesser(), translate)
assert.Equal(t, (map[string]gobot.AnalogPinner)(nil), a.pins)

err := a.AnalogWrite("write", 1)
require.ErrorContains(t, err, "not connected")

err = a.Connect()
require.NoError(t, err)
assert.NotEqual(t, (map[string]gobot.AnalogPinner)(nil), a.pins)
assert.Empty(t, a.pins)
}

func TestAnalogPinsFinalize(t *testing.T) {
// arrange
sys := system.NewAccesser()
fs := sys.UseMockFilesystem(analogMockPaths)
a := NewAnalogPinsAdaptor(sys, testAnalogPinTranslator)
fs.Files[analogReadPath].Contents = "0"
// assert that finalize before connect is working
require.NoError(t, a.Finalize())
// arrange
require.NoError(t, a.Connect())
require.NoError(t, a.AnalogWrite("write", 1))
assert.Len(t, a.pins, 1)
// act
err := a.Finalize()
// assert
require.NoError(t, err)
assert.Empty(t, a.pins)
// assert that finalize after finalize is working
require.NoError(t, a.Finalize())
// arrange missing file
require.NoError(t, a.Connect())
require.NoError(t, a.AnalogWrite("write", 2))
delete(fs.Files, analogWritePath)
err = a.Finalize()
require.NoError(t, err) // because there is currently no access on finalize
// arrange write error
require.NoError(t, a.Connect())
require.NoError(t, a.AnalogWrite("read/write_string", 5))
fs.WithWriteError = true
err = a.Finalize()
require.NoError(t, err) // because there is currently no access on finalize
}

func TestAnalogPinsReConnect(t *testing.T) {
// arrange
a, _ := initTestAnalogPinsAdaptorWithMockedFilesystem(analogMockPaths)
require.NoError(t, a.AnalogWrite("read/write_string", 1))
assert.Len(t, a.pins, 1)
require.NoError(t, a.Finalize())
// act
err := a.Connect()
// assert
require.NoError(t, err)
assert.NotNil(t, a.pins)
assert.Empty(t, a.pins)
}

func TestAnalogWrite(t *testing.T) {
tests := map[string]struct {
pin string
simulateWriteErr bool
simulateReadErr bool
wantValW string
wantValRW string
wantValRWS string
wantErr string
}{
"write_w_pin": {
pin: "write",
wantValW: "100",
wantValRW: "30000",
wantValRWS: "inverted",
},
"write_rw_pin": {
pin: "read/write_string",
wantValW: "0",
wantValRW: "30000",
wantValRWS: "100",
},
"ok_on_read_error": {
pin: "read/write_string",
simulateReadErr: true,
wantValW: "0",
wantValRW: "30000",
wantValRWS: "100",
},
"error_write_error": {
pin: "read/write_string",
simulateWriteErr: true,
wantValW: "0",
wantValRW: "30000",
wantValRWS: "inverted",
wantErr: "write error",
},
"error_notexist": {
pin: "notexist",
wantValW: "0",
wantValRW: "30000",
wantValRWS: "inverted",
wantErr: "'notexist' is not a valid id of a analog pin",
},
"error_write_not_allowed": {
pin: "read",
wantValW: "0",
wantValRW: "30000",
wantValRWS: "inverted",
wantErr: "the pin '/sys/bus/iio/devices/iio:device0/in_voltage0_raw' is not allowed to write (val: 100)",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, fs := initTestAnalogPinsAdaptorWithMockedFilesystem(analogMockPaths)
fs.WithWriteError = tc.simulateWriteErr
fs.WithReadError = tc.simulateReadErr
// act
err := a.AnalogWrite(tc.pin, 100)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, "54321", fs.Files[analogReadPath].Contents)
assert.Equal(t, tc.wantValW, fs.Files[analogWritePath].Contents)
assert.Equal(t, tc.wantValRW, fs.Files[analogReadWritePath].Contents)
assert.Equal(t, tc.wantValRWS, fs.Files[analogReadWriteStringPath].Contents)
})
}
}

func TestAnalogRead(t *testing.T) {
tests := map[string]struct {
pin string
simulateReadErr bool
simulateWriteErr bool
wantVal int
wantErr string
}{
"read_r_pin": {
pin: "read",
wantVal: 54321,
},
"read_rw_pin": {
pin: "read/write",
wantVal: 30000,
},
"ok_on_write_error": {
pin: "read",
simulateWriteErr: true,
wantVal: 54321,
},
"error_read_error": {
pin: "read",
simulateReadErr: true,
wantErr: "read error",
},
"error_notexist": {
pin: "notexist",
wantErr: "'notexist' is not a valid id of a analog pin",
},
"error_invalid_syntax": {
pin: "read/write_string",
wantErr: "strconv.Atoi: parsing \"inverted\": invalid syntax",
},
"error_read_not_allowed": {
pin: "write",
wantErr: "the pin '/sys/devices/platform/ff680020.pwm/pwm/pwmchip3/export' is not allowed to read",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
a, fs := initTestAnalogPinsAdaptorWithMockedFilesystem(analogMockPaths)
fs.WithReadError = tc.simulateReadErr
fs.WithWriteError = tc.simulateWriteErr
// act
got, err := a.AnalogRead(tc.pin)
// assert
if tc.wantErr != "" {
require.EqualError(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.wantVal, got)
})
}
}
Loading

0 comments on commit d39848e

Please sign in to comment.