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

platforms(adaptors): add a generic analog pin adaptor #1041

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