mirror of
https://github.com/hybridgroup/gobot.git
synced 2025-04-29 13:49:14 +08:00
platforms(adaptors): add a generic analog pin adaptor (#1041)
This commit is contained in:
parent
916c2bad18
commit
d39848e368
@ -99,6 +99,14 @@ type PWMPinnerProvider interface {
|
|||||||
PWMPin(id string) (PWMPinner, error)
|
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.
|
// I2cSystemDevicer is the interface to a i2c bus at system level, according to I2C/SMBus specification.
|
||||||
// Some functions are not in the interface yet:
|
// Some functions are not in the interface yet:
|
||||||
// * Process Call (WriteWordDataReadWordData)
|
// * Process Call (WriteWordDataReadWordData)
|
||||||
|
95
platforms/adaptors/analogpinsadaptor.go
Normal file
95
platforms/adaptors/analogpinsadaptor.go
Normal file
@ -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
platforms/adaptors/analogpinsadaptor_test.go
Normal file
249
platforms/adaptors/analogpinsadaptor_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -14,12 +14,19 @@ import (
|
|||||||
"gobot.io/x/gobot/v2/system"
|
"gobot.io/x/gobot/v2/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pwmPinData struct {
|
type pwmPinDefinition struct {
|
||||||
channel int
|
channel int
|
||||||
dir string
|
dir string
|
||||||
dirRegexp string
|
dirRegexp string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type analogPinDefinition struct {
|
||||||
|
path string
|
||||||
|
r bool // readable
|
||||||
|
w bool // writable
|
||||||
|
bufLen uint16
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pwmPeriodDefault = 500000 // 0.5 ms = 2 kHz
|
pwmPeriodDefault = 500000 // 0.5 ms = 2 kHz
|
||||||
|
|
||||||
@ -37,15 +44,15 @@ type Adaptor struct {
|
|||||||
name string
|
name string
|
||||||
sys *system.Accesser
|
sys *system.Accesser
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
*adaptors.AnalogPinsAdaptor
|
||||||
*adaptors.DigitalPinsAdaptor
|
*adaptors.DigitalPinsAdaptor
|
||||||
*adaptors.PWMPinsAdaptor
|
*adaptors.PWMPinsAdaptor
|
||||||
*adaptors.I2cBusAdaptor
|
*adaptors.I2cBusAdaptor
|
||||||
*adaptors.SpiBusAdaptor
|
*adaptors.SpiBusAdaptor
|
||||||
usrLed string
|
usrLed string
|
||||||
analogPath string
|
|
||||||
pinMap map[string]int
|
pinMap map[string]int
|
||||||
pwmPinMap map[string]pwmPinData
|
pwmPinMap map[string]pwmPinDefinition
|
||||||
analogPinMap map[string]string
|
analogPinMap map[string]analogPinDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAdaptor returns a new Beaglebone Black/Green Adaptor
|
// NewAdaptor returns a new Beaglebone Black/Green Adaptor
|
||||||
@ -63,9 +70,8 @@ func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor {
|
|||||||
pwmPinMap: bbbPwmPinMap,
|
pwmPinMap: bbbPwmPinMap,
|
||||||
analogPinMap: bbbAnalogPinMap,
|
analogPinMap: bbbAnalogPinMap,
|
||||||
usrLed: "/sys/class/leds/beaglebone:green:",
|
usrLed: "/sys/class/leds/beaglebone:green:",
|
||||||
analogPath: "/sys/bus/iio/devices/iio:device0",
|
|
||||||
}
|
}
|
||||||
|
c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin)
|
||||||
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateAndMuxDigitalPin, opts...)
|
c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateAndMuxDigitalPin, opts...)
|
||||||
c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin,
|
c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin,
|
||||||
adaptors.WithPWMPinDefaultPeriod(pwmPeriodDefault))
|
adaptors.WithPWMPinDefaultPeriod(pwmPeriodDefault))
|
||||||
@ -94,6 +100,10 @@ func (c *Adaptor) Connect() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.AnalogPinsAdaptor.Connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.PWMPinsAdaptor.Connect(); err != nil {
|
if err := c.PWMPinsAdaptor.Connect(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -111,6 +121,10 @@ func (c *Adaptor) Finalize() error {
|
|||||||
err = multierror.Append(err, e)
|
err = multierror.Append(err, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
|
||||||
|
err = multierror.Append(err, e)
|
||||||
|
}
|
||||||
|
|
||||||
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
||||||
err = multierror.Append(err, e)
|
err = multierror.Append(err, e)
|
||||||
}
|
}
|
||||||
@ -140,29 +154,6 @@ func (c *Adaptor) DigitalWrite(id string, val byte) error {
|
|||||||
return c.DigitalPinsAdaptor.DigitalWrite(id, val)
|
return c.DigitalPinsAdaptor.DigitalWrite(id, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnalogRead returns an analog value from specified pin
|
|
||||||
func (c *Adaptor) AnalogRead(pin string) (int, error) {
|
|
||||||
analogPin, err := c.translateAnalogPin(pin)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
fi, err := c.sys.OpenFile(fmt.Sprintf("%v/%v", c.analogPath, analogPin), os.O_RDONLY, 0o644)
|
|
||||||
defer fi.Close() //nolint:staticcheck // for historical reasons
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
_, err = fi.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
val, _ := strconv.Atoi(strings.Split(string(buf), "\n")[0])
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Adaptor) validateSpiBusNumber(busNr int) error {
|
func (c *Adaptor) validateSpiBusNumber(busNr int) error {
|
||||||
// Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x.
|
// Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x.
|
||||||
// x is the chip number <255
|
// x is the chip number <255
|
||||||
@ -181,11 +172,13 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// translateAnalogPin converts analog pin name to pin position
|
// translateAnalogPin converts analog pin name to pin position
|
||||||
func (c *Adaptor) translateAnalogPin(pin string) (string, error) {
|
func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, error) {
|
||||||
if val, ok := c.analogPinMap[pin]; ok {
|
pinInfo, ok := c.analogPinMap[pin]
|
||||||
return val, nil
|
if !ok {
|
||||||
|
return "", false, false, 0, fmt.Errorf("Not a valid analog pin")
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("Not a valid analog pin")
|
|
||||||
|
return pinInfo.path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// translatePin converts digital pin name to pin position
|
// translatePin converts digital pin name to pin position
|
||||||
@ -219,7 +212,7 @@ func (c *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) {
|
|||||||
return path, pinInfo.channel, nil
|
return path, pinInfo.channel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p pwmPinData) findPWMDir(sys *system.Accesser) (string, error) {
|
func (p pwmPinDefinition) findPWMDir(sys *system.Accesser) (string, error) {
|
||||||
items, _ := sys.Find(p.dir, p.dirRegexp)
|
items, _ := sys.Find(p.dir, p.dirRegexp)
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return "", fmt.Errorf("No path found for PWM directory pattern, '%s' in path '%s'", p.dirRegexp, p.dir)
|
return "", fmt.Errorf("No path found for PWM directory pattern, '%s' in path '%s'", p.dirRegexp, p.dir)
|
||||||
|
@ -49,7 +49,7 @@ var bbbPinMap = map[string]int{
|
|||||||
"P9_31": 110,
|
"P9_31": 110,
|
||||||
}
|
}
|
||||||
|
|
||||||
var bbbPwmPinMap = map[string]pwmPinData{
|
var bbbPwmPinMap = map[string]pwmPinDefinition{
|
||||||
"P8_13": {
|
"P8_13": {
|
||||||
dir: "/sys/devices/platform/ocp/48304000.epwmss/48304200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1,
|
dir: "/sys/devices/platform/ocp/48304000.epwmss/48304200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1,
|
||||||
},
|
},
|
||||||
@ -73,12 +73,12 @@ var bbbPwmPinMap = map[string]pwmPinData{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var bbbAnalogPinMap = map[string]string{
|
var bbbAnalogPinMap = map[string]analogPinDefinition{
|
||||||
"P9_39": "in_voltage0_raw",
|
"P9_39": {path: "/sys/bus/iio/devices/iio:device0/in_voltage0_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_40": "in_voltage1_raw",
|
"P9_40": {path: "/sys/bus/iio/devices/iio:device0/in_voltage1_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_37": "in_voltage2_raw",
|
"P9_37": {path: "/sys/bus/iio/devices/iio:device0/in_voltage2_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_38": "in_voltage3_raw",
|
"P9_38": {path: "/sys/bus/iio/devices/iio:device0/in_voltage3_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_33": "in_voltage4_raw",
|
"P9_33": {path: "/sys/bus/iio/devices/iio:device0/in_voltage4_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_36": "in_voltage5_raw",
|
"P9_36": {path: "/sys/bus/iio/devices/iio:device0/in_voltage5_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P9_35": "in_voltage6_raw",
|
"P9_35": {path: "/sys/bus/iio/devices/iio:device0/in_voltage6_raw", r: true, w: false, bufLen: 1024},
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ var pocketBeaglePinMap = map[string]int{
|
|||||||
// P2_36 - AIO7
|
// P2_36 - AIO7
|
||||||
}
|
}
|
||||||
|
|
||||||
var pocketBeaglePwmPinMap = map[string]pwmPinData{
|
var pocketBeaglePwmPinMap = map[string]pwmPinDefinition{
|
||||||
"P1_33": {dir: "/sys/devices/platform/ocp/48300000.epwmss/48300200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1},
|
"P1_33": {dir: "/sys/devices/platform/ocp/48300000.epwmss/48300200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1},
|
||||||
"P1_36": {dir: "/sys/devices/platform/ocp/48300000.epwmss/48300200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 0},
|
"P1_36": {dir: "/sys/devices/platform/ocp/48300000.epwmss/48300200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 0},
|
||||||
|
|
||||||
@ -84,11 +84,11 @@ var pocketBeaglePwmPinMap = map[string]pwmPinData{
|
|||||||
"P2_3": {dir: "/sys/devices/platform/ocp/48304000.epwmss/48304200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1},
|
"P2_3": {dir: "/sys/devices/platform/ocp/48304000.epwmss/48304200.pwm/pwm/", dirRegexp: "pwmchip[0-9]+$", channel: 1},
|
||||||
}
|
}
|
||||||
|
|
||||||
var pocketBeagleAnalogPinMap = map[string]string{
|
var pocketBeagleAnalogPinMap = map[string]analogPinDefinition{
|
||||||
"P1_19": "in_voltage0_raw",
|
"P1_19": {path: "/sys/bus/iio/devices/iio:device0/in_voltage0_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P1_21": "in_voltage1_raw",
|
"P1_21": {path: "/sys/bus/iio/devices/iio:device0/in_voltage1_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P1_23": "in_voltage2_raw",
|
"P1_23": {path: "/sys/bus/iio/devices/iio:device0/in_voltage2_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P1_25": "in_voltage3_raw",
|
"P1_25": {path: "/sys/bus/iio/devices/iio:device0/in_voltage3_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P1_27": "in_voltage4_raw",
|
"P1_27": {path: "/sys/bus/iio/devices/iio:device0/in_voltage4_raw", r: true, w: false, bufLen: 1024},
|
||||||
"P2_36": "in_voltage7_raw",
|
"P2_36": {path: "/sys/bus/iio/devices/iio:device0/in_voltage7_raw", r: true, w: false, bufLen: 1024},
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ type Adaptor struct {
|
|||||||
pinmap map[string]sysfsPin
|
pinmap map[string]sysfsPin
|
||||||
tristate gobot.DigitalPinner
|
tristate gobot.DigitalPinner
|
||||||
digitalPins map[int]gobot.DigitalPinner
|
digitalPins map[int]gobot.DigitalPinner
|
||||||
|
*adaptors.AnalogPinsAdaptor
|
||||||
*adaptors.PWMPinsAdaptor
|
*adaptors.PWMPinsAdaptor
|
||||||
*adaptors.I2cBusAdaptor
|
*adaptors.I2cBusAdaptor
|
||||||
arduinoI2cInitialized bool
|
arduinoI2cInitialized bool
|
||||||
@ -58,6 +59,7 @@ func NewAdaptor(boardType ...string) *Adaptor {
|
|||||||
if len(boardType) > 0 && boardType[0] != "" {
|
if len(boardType) > 0 && boardType[0] != "" {
|
||||||
c.board = boardType[0]
|
c.board = boardType[0]
|
||||||
}
|
}
|
||||||
|
c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin)
|
||||||
c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin,
|
c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin,
|
||||||
adaptors.WithPWMPinInitializer(pwmPinInitializer))
|
adaptors.WithPWMPinInitializer(pwmPinInitializer))
|
||||||
defI2cBusNr := defaultI2cBusNumber
|
defI2cBusNr := defaultI2cBusNumber
|
||||||
@ -82,6 +84,10 @@ func (c *Adaptor) Connect() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.AnalogPinsAdaptor.Connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.PWMPinsAdaptor.Connect(); err != nil {
|
if err := c.PWMPinsAdaptor.Connect(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -127,6 +133,10 @@ func (c *Adaptor) Finalize() error {
|
|||||||
err = multierror.Append(err, e)
|
err = multierror.Append(err, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e := c.AnalogPinsAdaptor.Finalize(); e != nil {
|
||||||
|
err = multierror.Append(err, e)
|
||||||
|
}
|
||||||
|
|
||||||
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
if e := c.I2cBusAdaptor.Finalize(); e != nil {
|
||||||
err = multierror.Append(err, e)
|
err = multierror.Append(err, e)
|
||||||
}
|
}
|
||||||
@ -165,14 +175,12 @@ func (c *Adaptor) DigitalPin(id string) (gobot.DigitalPinner, error) {
|
|||||||
|
|
||||||
// AnalogRead returns value from analog reading of specified pin
|
// AnalogRead returns value from analog reading of specified pin
|
||||||
func (c *Adaptor) AnalogRead(pin string) (int, error) {
|
func (c *Adaptor) AnalogRead(pin string) (int, error) {
|
||||||
buf, err := c.readFile("/sys/bus/iio/devices/iio:device1/in_voltage" + pin + "_raw")
|
rawRead, err := c.AnalogPinsAdaptor.AnalogRead(pin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := strconv.Atoi(string(buf[0 : len(buf)-1]))
|
return rawRead / 4, err
|
||||||
|
|
||||||
return val / 4, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Adaptor) validateAndSetupI2cBusNumber(busNr int) error {
|
func (c *Adaptor) validateAndSetupI2cBusNumber(busNr int) error {
|
||||||
@ -263,22 +271,6 @@ func (c *Adaptor) arduinoI2CSetup() error {
|
|||||||
return c.tristate.Write(system.HIGH)
|
return c.tristate.Write(system.HIGH)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Adaptor) readFile(path string) ([]byte, error) {
|
|
||||||
file, err := c.sys.OpenFile(path, os.O_RDONLY, 0o644)
|
|
||||||
defer file.Close() //nolint:staticcheck // for historical reasons
|
|
||||||
if err != nil {
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 200)
|
|
||||||
var i int
|
|
||||||
i, err = file.Read(buf)
|
|
||||||
if i == 0 {
|
|
||||||
return buf, err
|
|
||||||
}
|
|
||||||
return buf[:i], err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) {
|
func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) {
|
||||||
i := c.pinmap[id]
|
i := c.pinmap[id]
|
||||||
|
|
||||||
@ -347,6 +339,16 @@ func pwmPinInitializer(pin gobot.PWMPinner) error {
|
|||||||
return pin.SetEnabled(true)
|
return pin.SetEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, error) {
|
||||||
|
path := fmt.Sprintf("/sys/bus/iio/devices/iio:device1/in_voltage%s_raw", pin)
|
||||||
|
const (
|
||||||
|
read = true
|
||||||
|
write = false
|
||||||
|
readBufLen = 200
|
||||||
|
)
|
||||||
|
return path, read, write, readBufLen, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) {
|
func (c *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) {
|
||||||
sysPin, ok := c.pinmap[id]
|
sysPin, ok := c.pinmap[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -364,7 +364,7 @@ func TestFinalize(t *testing.T) {
|
|||||||
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
||||||
|
|
||||||
_ = a.DigitalWrite("3", 1)
|
_ = a.DigitalWrite("3", 1)
|
||||||
_ = a.PwmWrite("5", 100)
|
require.NoError(t, a.PwmWrite("5", 100))
|
||||||
|
|
||||||
_, _ = a.GetI2cConnection(0xff, 6)
|
_, _ = a.GetI2cConnection(0xff, 6)
|
||||||
require.NoError(t, a.Finalize())
|
require.NoError(t, a.Finalize())
|
||||||
@ -373,7 +373,7 @@ func TestFinalize(t *testing.T) {
|
|||||||
require.NoError(t, a.Finalize())
|
require.NoError(t, a.Finalize())
|
||||||
|
|
||||||
// assert that re-connect is working
|
// assert that re-connect is working
|
||||||
_ = a.Connect()
|
require.NoError(t, a.Connect())
|
||||||
// remove one file to force Finalize error
|
// remove one file to force Finalize error
|
||||||
delete(fs.Files, "/sys/class/gpio/unexport")
|
delete(fs.Files, "/sys/class/gpio/unexport")
|
||||||
err := a.Finalize()
|
err := a.Finalize()
|
||||||
@ -384,7 +384,7 @@ func TestFinalize(t *testing.T) {
|
|||||||
func TestFinalizeError(t *testing.T) {
|
func TestFinalizeError(t *testing.T) {
|
||||||
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
||||||
|
|
||||||
_ = a.PwmWrite("5", 100)
|
require.NoError(t, a.PwmWrite("5", 100))
|
||||||
|
|
||||||
fs.WithWriteError = true
|
fs.WithWriteError = true
|
||||||
err := a.Finalize()
|
err := a.Finalize()
|
||||||
@ -397,10 +397,10 @@ func TestFinalizeError(t *testing.T) {
|
|||||||
func TestDigitalIO(t *testing.T) {
|
func TestDigitalIO(t *testing.T) {
|
||||||
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
||||||
|
|
||||||
_ = a.DigitalWrite("13", 1)
|
require.NoError(t, a.DigitalWrite("13", 1))
|
||||||
assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio40/value"].Contents)
|
assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio40/value"].Contents)
|
||||||
|
|
||||||
_ = a.DigitalWrite("2", 0)
|
require.NoError(t, a.DigitalWrite("2", 0))
|
||||||
i, err := a.DigitalRead("2")
|
i, err := a.DigitalRead("2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 0, i)
|
assert.Equal(t, 0, i)
|
||||||
@ -411,7 +411,7 @@ func TestDigitalPinInFileError(t *testing.T) {
|
|||||||
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio40/value")
|
delete(fs.Files, "/sys/class/gpio/gpio40/value")
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio40/direction")
|
delete(fs.Files, "/sys/class/gpio/gpio40/direction")
|
||||||
_ = a.Connect()
|
_ = a.Connect() // we drop this error
|
||||||
|
|
||||||
_, err := a.DigitalPin("13")
|
_, err := a.DigitalPin("13")
|
||||||
require.ErrorContains(t, err, "no such file")
|
require.ErrorContains(t, err, "no such file")
|
||||||
@ -422,7 +422,7 @@ func TestDigitalPinInResistorFileError(t *testing.T) {
|
|||||||
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio229/value")
|
delete(fs.Files, "/sys/class/gpio/gpio229/value")
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio229/direction")
|
delete(fs.Files, "/sys/class/gpio/gpio229/direction")
|
||||||
_ = a.Connect()
|
_ = a.Connect() // we drop this error
|
||||||
|
|
||||||
_, err := a.DigitalPin("13")
|
_, err := a.DigitalPin("13")
|
||||||
require.ErrorContains(t, err, "no such file")
|
require.ErrorContains(t, err, "no such file")
|
||||||
@ -433,7 +433,7 @@ func TestDigitalPinInLevelShifterFileError(t *testing.T) {
|
|||||||
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio261/value")
|
delete(fs.Files, "/sys/class/gpio/gpio261/value")
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio261/direction")
|
delete(fs.Files, "/sys/class/gpio/gpio261/direction")
|
||||||
_ = a.Connect()
|
_ = a.Connect() // we drop this error
|
||||||
|
|
||||||
_, err := a.DigitalPin("13")
|
_, err := a.DigitalPin("13")
|
||||||
require.ErrorContains(t, err, "no such file")
|
require.ErrorContains(t, err, "no such file")
|
||||||
@ -444,7 +444,7 @@ func TestDigitalPinInMuxFileError(t *testing.T) {
|
|||||||
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
fs := a.sys.UseMockFilesystem(pwmMockPathsMux40)
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio243/value")
|
delete(fs.Files, "/sys/class/gpio/gpio243/value")
|
||||||
delete(fs.Files, "/sys/class/gpio/gpio243/direction")
|
delete(fs.Files, "/sys/class/gpio/gpio243/direction")
|
||||||
_ = a.Connect()
|
_ = a.Connect() // we drop this error
|
||||||
|
|
||||||
_, err := a.DigitalPin("13")
|
_, err := a.DigitalPin("13")
|
||||||
require.ErrorContains(t, err, "no such file")
|
require.ErrorContains(t, err, "no such file")
|
||||||
@ -492,7 +492,7 @@ func TestPwmEnableError(t *testing.T) {
|
|||||||
a := NewAdaptor()
|
a := NewAdaptor()
|
||||||
fs := a.sys.UseMockFilesystem(pwmMockPathsMux13)
|
fs := a.sys.UseMockFilesystem(pwmMockPathsMux13)
|
||||||
delete(fs.Files, "/sys/class/pwm/pwmchip0/pwm1/enable")
|
delete(fs.Files, "/sys/class/pwm/pwmchip0/pwm1/enable")
|
||||||
_ = a.Connect()
|
_ = a.Connect() // we drop this error
|
||||||
|
|
||||||
err := a.PwmWrite("5", 100)
|
err := a.PwmWrite("5", 100)
|
||||||
require.ErrorContains(t, err, "/sys/class/pwm/pwmchip0/pwm1/enable: no such file")
|
require.ErrorContains(t, err, "/sys/class/pwm/pwmchip0/pwm1/enable: no such file")
|
||||||
@ -526,7 +526,8 @@ func TestAnalog(t *testing.T) {
|
|||||||
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
a, fs := initTestAdaptorWithMockedFilesystem("arduino")
|
||||||
fs.Files["/sys/bus/iio/devices/iio:device1/in_voltage0_raw"].Contents = "1000\n"
|
fs.Files["/sys/bus/iio/devices/iio:device1/in_voltage0_raw"].Contents = "1000\n"
|
||||||
|
|
||||||
i, _ := a.AnalogRead("0")
|
i, err := a.AnalogRead("0")
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 250, i)
|
assert.Equal(t, 250, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,3 +93,5 @@ Now we should measure a value of around 2.3V with the meter, which is the differ
|
|||||||
If we have attached an oscilloscope we can play around with the values for period and duty_cycle and see what happen.
|
If we have attached an oscilloscope we can play around with the values for period and duty_cycle and see what happen.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
|
* <https://docs.kernel.org/driver-api/pwm.html>
|
||||||
|
37
system/analogpin_sysfs.go
Normal file
37
system/analogpin_sysfs.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type analogPinSysFs struct {
|
||||||
|
sysfsPath string
|
||||||
|
r, w bool
|
||||||
|
sfa *sysfsFileAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAnalogPinSysfs(sfa *sysfsFileAccess, path string, r, w bool) *analogPinSysFs {
|
||||||
|
p := &analogPinSysFs{
|
||||||
|
sysfsPath: path,
|
||||||
|
sfa: sfa,
|
||||||
|
r: r,
|
||||||
|
w: w,
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads a value from sysf path
|
||||||
|
func (p *analogPinSysFs) Read() (int, error) {
|
||||||
|
if !p.r {
|
||||||
|
return 0, fmt.Errorf("the pin '%s' is not allowed to read", p.sysfsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.sfa.readInteger(p.sysfsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes a value to sysf path
|
||||||
|
func (p *analogPinSysFs) Write(val int) error {
|
||||||
|
if !p.w {
|
||||||
|
return fmt.Errorf("the pin '%s' is not allowed to write (val: %v)", p.sysfsPath, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.sfa.writeInteger(p.sysfsPath, val)
|
||||||
|
}
|
121
system/analogpin_sysfs_test.go
Normal file
121
system/analogpin_sysfs_test.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newAnalogPinSysfs(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
m := &MockFilesystem{}
|
||||||
|
sfa := sysfsFileAccess{fs: m, readBufLen: 2}
|
||||||
|
const path = "/sys/whatever"
|
||||||
|
// act
|
||||||
|
pin := newAnalogPinSysfs(&sfa, path, true, false)
|
||||||
|
// assert
|
||||||
|
assert.Equal(t, path, pin.sysfsPath)
|
||||||
|
assert.Equal(t, &sfa, pin.sfa)
|
||||||
|
assert.True(t, pin.r)
|
||||||
|
assert.False(t, pin.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRead(t *testing.T) {
|
||||||
|
const (
|
||||||
|
sysfsPath = "/sys/testread"
|
||||||
|
value = "32"
|
||||||
|
)
|
||||||
|
tests := map[string]struct {
|
||||||
|
readable bool
|
||||||
|
simErr bool
|
||||||
|
wantVal int
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
"read_ok": {
|
||||||
|
readable: true,
|
||||||
|
wantVal: 32,
|
||||||
|
},
|
||||||
|
"error_not_readable": {
|
||||||
|
readable: false,
|
||||||
|
wantErr: "the pin '/sys/testread' is not allowed to read",
|
||||||
|
},
|
||||||
|
"error_on_read": {
|
||||||
|
readable: true,
|
||||||
|
simErr: true,
|
||||||
|
wantErr: "read error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
fs := newMockFilesystem([]string{sysfsPath})
|
||||||
|
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
|
||||||
|
pin := newAnalogPinSysfs(&sfa, sysfsPath, tc.readable, false)
|
||||||
|
fs.Files[sysfsPath].Contents = value
|
||||||
|
if tc.simErr {
|
||||||
|
fs.Files[sysfsPath].simulateReadError = fmt.Errorf("read error")
|
||||||
|
}
|
||||||
|
// act
|
||||||
|
got, err := pin.Read()
|
||||||
|
// assert
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tc.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.wantVal, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrite(t *testing.T) {
|
||||||
|
const (
|
||||||
|
sysfsPath = "/sys/testwrite"
|
||||||
|
oldVal = "old_value"
|
||||||
|
)
|
||||||
|
tests := map[string]struct {
|
||||||
|
writeable bool
|
||||||
|
simErr bool
|
||||||
|
wantVal string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
"write_ok": {
|
||||||
|
writeable: true,
|
||||||
|
wantVal: "23",
|
||||||
|
},
|
||||||
|
"error_not_writeable": {
|
||||||
|
writeable: false,
|
||||||
|
wantErr: "the pin '/sys/testwrite' is not allowed to write (val: 23)",
|
||||||
|
wantVal: oldVal,
|
||||||
|
},
|
||||||
|
"error_on_write": {
|
||||||
|
writeable: true,
|
||||||
|
simErr: true,
|
||||||
|
wantErr: "write error",
|
||||||
|
wantVal: "23", // the mock is implemented in this way
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// arrange
|
||||||
|
fs := newMockFilesystem([]string{sysfsPath})
|
||||||
|
sfa := sysfsFileAccess{fs: fs, readBufLen: 10}
|
||||||
|
pin := newAnalogPinSysfs(&sfa, sysfsPath, false, tc.writeable)
|
||||||
|
fs.Files[sysfsPath].Contents = oldVal
|
||||||
|
if tc.simErr {
|
||||||
|
fs.Files[sysfsPath].simulateWriteError = fmt.Errorf("write error")
|
||||||
|
}
|
||||||
|
// act
|
||||||
|
err := pin.Write(23)
|
||||||
|
// assert
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
require.EqualError(t, err, tc.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.wantVal, fs.Files[sysfsPath].Contents)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
// sysfsDitalPinHandler represents the sysfs implementation
|
// sysfsDitalPinHandler represents the sysfs implementation
|
||||||
type sysfsDigitalPinAccess struct {
|
type sysfsDigitalPinAccess struct {
|
||||||
fs filesystem
|
sfa *sysfsFileAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
// gpiodDigitalPinAccess represents the character device implementation
|
// gpiodDigitalPinAccess represents the character device implementation
|
||||||
@ -25,11 +25,11 @@ func (h *sysfsDigitalPinAccess) isSupported() bool {
|
|||||||
func (h *sysfsDigitalPinAccess) createPin(chip string, pin int,
|
func (h *sysfsDigitalPinAccess) createPin(chip string, pin int,
|
||||||
o ...func(gobot.DigitalPinOptioner) bool,
|
o ...func(gobot.DigitalPinOptioner) bool,
|
||||||
) gobot.DigitalPinner {
|
) gobot.DigitalPinner {
|
||||||
return newDigitalPinSysfs(h.fs, strconv.Itoa(pin), o...)
|
return newDigitalPinSysfs(h.sfa, strconv.Itoa(pin), o...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *sysfsDigitalPinAccess) setFs(fs filesystem) {
|
func (h *sysfsDigitalPinAccess) setFs(fs filesystem) {
|
||||||
h.fs = fs
|
h.sfa = &sysfsFileAccess{fs: fs, readBufLen: 2}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *gpiodDigitalPinAccess) isSupported() bool {
|
func (h *gpiodDigitalPinAccess) isSupported() bool {
|
||||||
|
@ -3,7 +3,6 @@ package system
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -24,21 +23,25 @@ var errNotExported = errors.New("pin has not been exported")
|
|||||||
type digitalPinSysfs struct {
|
type digitalPinSysfs struct {
|
||||||
pin string
|
pin string
|
||||||
*digitalPinConfig
|
*digitalPinConfig
|
||||||
fs filesystem
|
sfa *sysfsFileAccess
|
||||||
|
|
||||||
dirFile File
|
dirFile *sysfsFile
|
||||||
valFile File
|
valFile *sysfsFile
|
||||||
activeLowFile File
|
activeLowFile *sysfsFile
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDigitalPinSysfs returns a digital pin using for the given number. The name of the sysfs file will prepend "gpio"
|
// newDigitalPinSysfs returns a digital pin using for the given number. The name of the sysfs file will prepend "gpio"
|
||||||
// to the pin number, eg. a pin number of 10 will have a name of "gpio10". The pin is handled by the sysfs Kernel ABI.
|
// to the pin number, eg. a pin number of 10 will have a name of "gpio10". The pin is handled by the sysfs Kernel ABI.
|
||||||
func newDigitalPinSysfs(fs filesystem, pin string, options ...func(gobot.DigitalPinOptioner) bool) *digitalPinSysfs {
|
func newDigitalPinSysfs(
|
||||||
|
sfa *sysfsFileAccess,
|
||||||
|
pin string,
|
||||||
|
options ...func(gobot.DigitalPinOptioner) bool,
|
||||||
|
) *digitalPinSysfs {
|
||||||
cfg := newDigitalPinConfig("gpio"+pin, options...)
|
cfg := newDigitalPinConfig("gpio"+pin, options...)
|
||||||
d := &digitalPinSysfs{
|
d := &digitalPinSysfs{
|
||||||
pin: pin,
|
pin: pin,
|
||||||
digitalPinConfig: cfg,
|
digitalPinConfig: cfg,
|
||||||
fs: fs,
|
sfa: sfa,
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
@ -68,26 +71,26 @@ func (d *digitalPinSysfs) Export() error {
|
|||||||
|
|
||||||
// Unexport release the pin
|
// Unexport release the pin
|
||||||
func (d *digitalPinSysfs) Unexport() error {
|
func (d *digitalPinSysfs) Unexport() error {
|
||||||
unexport, err := d.fs.openFile(gpioPath+"/unexport", os.O_WRONLY, 0o644)
|
unexport, err := d.sfa.openWrite(gpioPath + "/unexport")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer unexport.Close()
|
defer unexport.close()
|
||||||
|
|
||||||
if d.dirFile != nil {
|
if d.dirFile != nil {
|
||||||
d.dirFile.Close()
|
d.dirFile.close()
|
||||||
d.dirFile = nil
|
d.dirFile = nil
|
||||||
}
|
}
|
||||||
if d.valFile != nil {
|
if d.valFile != nil {
|
||||||
d.valFile.Close()
|
d.valFile.close()
|
||||||
d.valFile = nil
|
d.valFile = nil
|
||||||
}
|
}
|
||||||
if d.activeLowFile != nil {
|
if d.activeLowFile != nil {
|
||||||
d.activeLowFile.Close()
|
d.activeLowFile.close()
|
||||||
d.activeLowFile = nil
|
d.activeLowFile = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writeFile(unexport, []byte(d.pin))
|
err = unexport.write([]byte(d.pin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If EINVAL then the pin is reserved in the system and can't be unexported, we suppress the error
|
// If EINVAL then the pin is reserved in the system and can't be unexported, we suppress the error
|
||||||
var pathError *os.PathError
|
var pathError *os.PathError
|
||||||
@ -101,13 +104,19 @@ func (d *digitalPinSysfs) Unexport() error {
|
|||||||
|
|
||||||
// Write writes the given value to the character device
|
// Write writes the given value to the character device
|
||||||
func (d *digitalPinSysfs) Write(b int) error {
|
func (d *digitalPinSysfs) Write(b int) error {
|
||||||
err := writeFile(d.valFile, []byte(strconv.Itoa(b)))
|
if d.valFile == nil {
|
||||||
|
return errNotExported
|
||||||
|
}
|
||||||
|
err := d.valFile.write([]byte(strconv.Itoa(b)))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read reads the given value from character device
|
// Read reads a value from character device
|
||||||
func (d *digitalPinSysfs) Read() (int, error) {
|
func (d *digitalPinSysfs) Read() (int, error) {
|
||||||
buf, err := readFile(d.valFile)
|
if d.valFile == nil {
|
||||||
|
return 0, errNotExported
|
||||||
|
}
|
||||||
|
buf, err := d.valFile.read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -115,13 +124,13 @@ func (d *digitalPinSysfs) Read() (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *digitalPinSysfs) reconfigure() error {
|
func (d *digitalPinSysfs) reconfigure() error {
|
||||||
exportFile, err := d.fs.openFile(gpioPath+"/export", os.O_WRONLY, 0o644)
|
exportFile, err := d.sfa.openWrite(gpioPath + "/export")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer exportFile.Close()
|
defer exportFile.close()
|
||||||
|
|
||||||
err = writeFile(exportFile, []byte(d.pin))
|
err = exportFile.write([]byte(d.pin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If EBUSY then the pin has already been exported, we suppress the error
|
// If EBUSY then the pin has already been exported, we suppress the error
|
||||||
var pathError *os.PathError
|
var pathError *os.PathError
|
||||||
@ -131,13 +140,13 @@ func (d *digitalPinSysfs) reconfigure() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if d.dirFile != nil {
|
if d.dirFile != nil {
|
||||||
d.dirFile.Close()
|
d.dirFile.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt := 0
|
attempt := 0
|
||||||
for {
|
for {
|
||||||
attempt++
|
attempt++
|
||||||
d.dirFile, err = d.fs.openFile(fmt.Sprintf("%s/%s/direction", gpioPath, d.label), os.O_RDWR, 0o644)
|
d.dirFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/direction", gpioPath, d.label))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -148,10 +157,10 @@ func (d *digitalPinSysfs) reconfigure() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if d.valFile != nil {
|
if d.valFile != nil {
|
||||||
d.valFile.Close()
|
d.valFile.close()
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
d.valFile, err = d.fs.openFile(fmt.Sprintf("%s/%s/value", gpioPath, d.label), os.O_RDWR, 0o644)
|
d.valFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/value", gpioPath, d.label))
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure direction
|
// configure direction
|
||||||
@ -162,9 +171,9 @@ func (d *digitalPinSysfs) reconfigure() error {
|
|||||||
// configure inverse logic
|
// configure inverse logic
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if d.activeLow {
|
if d.activeLow {
|
||||||
d.activeLowFile, err = d.fs.openFile(fmt.Sprintf("%s/%s/active_low", gpioPath, d.label), os.O_RDWR, 0o644)
|
d.activeLowFile, err = d.sfa.openReadWrite(fmt.Sprintf("%s/%s/active_low", gpioPath, d.label))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = writeFile(d.activeLowFile, []byte("1"))
|
err = d.activeLowFile.write([]byte("1"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -211,48 +220,16 @@ func (d *digitalPinSysfs) reconfigure() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *digitalPinSysfs) writeDirectionWithInitialOutput() error {
|
func (d *digitalPinSysfs) writeDirectionWithInitialOutput() error {
|
||||||
if err := writeFile(d.dirFile, []byte(d.direction)); err != nil || d.direction == IN {
|
if d.dirFile == nil {
|
||||||
|
return errNotExported
|
||||||
|
}
|
||||||
|
if err := d.dirFile.write([]byte(d.direction)); err != nil || d.direction == IN {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err := writeFile(d.valFile, []byte(strconv.Itoa(d.outInitialState)))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Linux sysfs / GPIO specific sysfs docs.
|
if d.valFile == nil {
|
||||||
// https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt
|
|
||||||
// https://www.kernel.org/doc/Documentation/gpio/sysfs.txt
|
|
||||||
|
|
||||||
var writeFile = func(f File, data []byte) error {
|
|
||||||
if f == nil {
|
|
||||||
return errNotExported
|
return errNotExported
|
||||||
}
|
}
|
||||||
|
|
||||||
// sysfs docs say:
|
return d.valFile.write([]byte(strconv.Itoa(d.outInitialState)))
|
||||||
// > When writing sysfs files, userspace processes should first read the
|
|
||||||
// > entire file, modify the values it wishes to change, then write the
|
|
||||||
// > entire buffer back.
|
|
||||||
// however, this seems outdated/inaccurate (docs are from back in the Kernel BitKeeper days).
|
|
||||||
|
|
||||||
// Write() returns already a non-nil error when n != len(b).
|
|
||||||
_, err := f.Write(data)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var readFile = func(f File) ([]byte, error) {
|
|
||||||
if f == nil {
|
|
||||||
return nil, errNotExported
|
|
||||||
}
|
|
||||||
|
|
||||||
// sysfs docs say:
|
|
||||||
// > If userspace seeks back to zero or does a pread(2) with an offset of '0' the [..] method will
|
|
||||||
// > be called again, rearmed, to fill the buffer.
|
|
||||||
|
|
||||||
// TODO: Examine if seek is needed if full buffer is read from sysfs file.
|
|
||||||
|
|
||||||
buf := make([]byte, 2)
|
|
||||||
_, err := f.Seek(0, io.SeekStart)
|
|
||||||
if err == nil {
|
|
||||||
_, err = f.Read(buf)
|
|
||||||
}
|
|
||||||
return buf, err
|
|
||||||
}
|
}
|
||||||
|
@ -21,19 +21,21 @@ var (
|
|||||||
|
|
||||||
func initTestDigitalPinSysfsWithMockedFilesystem(mockPaths []string) (*digitalPinSysfs, *MockFilesystem) {
|
func initTestDigitalPinSysfsWithMockedFilesystem(mockPaths []string) (*digitalPinSysfs, *MockFilesystem) {
|
||||||
fs := newMockFilesystem(mockPaths)
|
fs := newMockFilesystem(mockPaths)
|
||||||
pin := newDigitalPinSysfs(fs, "10")
|
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
|
||||||
|
pin := newDigitalPinSysfs(&sfa, "10")
|
||||||
return pin, fs
|
return pin, fs
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newDigitalPinSysfs(t *testing.T) {
|
func Test_newDigitalPinSysfs(t *testing.T) {
|
||||||
// arrange
|
// arrange
|
||||||
m := &MockFilesystem{}
|
m := &MockFilesystem{}
|
||||||
|
sfa := sysfsFileAccess{fs: m, readBufLen: 2}
|
||||||
const pinID = "1"
|
const pinID = "1"
|
||||||
// act
|
// act
|
||||||
pin := newDigitalPinSysfs(m, pinID, WithPinOpenDrain())
|
pin := newDigitalPinSysfs(&sfa, pinID, WithPinOpenDrain())
|
||||||
// assert
|
// assert
|
||||||
assert.Equal(t, pinID, pin.pin)
|
assert.Equal(t, pinID, pin.pin)
|
||||||
assert.Equal(t, m, pin.fs)
|
assert.Equal(t, &sfa, pin.sfa)
|
||||||
assert.Equal(t, "gpio"+pinID, pin.label)
|
assert.Equal(t, "gpio"+pinID, pin.label)
|
||||||
assert.Equal(t, "in", pin.direction)
|
assert.Equal(t, "in", pin.direction)
|
||||||
assert.Equal(t, 1, pin.drive)
|
assert.Equal(t, 1, pin.drive)
|
||||||
@ -134,7 +136,7 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
changeDebouncePeriod time.Duration
|
changeDebouncePeriod time.Duration
|
||||||
changeEdge int
|
changeEdge int
|
||||||
changePollInterval time.Duration
|
changePollInterval time.Duration
|
||||||
simEbusyOnWrite int
|
simEbusyOnPath string
|
||||||
wantWrites int
|
wantWrites int
|
||||||
wantExport string
|
wantExport string
|
||||||
wantUnexport string
|
wantUnexport string
|
||||||
@ -225,11 +227,11 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
wantValue: "0",
|
wantValue: "0",
|
||||||
},
|
},
|
||||||
"ok_already_exported": {
|
"ok_already_exported": {
|
||||||
mockPaths: allMockPaths,
|
mockPaths: allMockPaths,
|
||||||
wantWrites: 2,
|
wantWrites: 2,
|
||||||
wantExport: "10",
|
wantExport: "10",
|
||||||
wantDirection: "in",
|
wantDirection: "in",
|
||||||
simEbusyOnWrite: 1, // just means "already exported"
|
simEbusyOnPath: exportPath, // just means "already exported"
|
||||||
},
|
},
|
||||||
"error_no_eventhandler_for_polling": { // this only tests the call of function, all other is tested separately
|
"error_no_eventhandler_for_polling": { // this only tests the call of function, all other is tested separately
|
||||||
mockPaths: allMockPaths,
|
mockPaths: allMockPaths,
|
||||||
@ -250,11 +252,11 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
wantErr: "gpio10/direction: no such file",
|
wantErr: "gpio10/direction: no such file",
|
||||||
},
|
},
|
||||||
"error_write_direction_file": {
|
"error_write_direction_file": {
|
||||||
mockPaths: allMockPaths,
|
mockPaths: allMockPaths,
|
||||||
wantWrites: 3,
|
wantWrites: 3,
|
||||||
wantUnexport: "10",
|
wantUnexport: "10",
|
||||||
simEbusyOnWrite: 2,
|
simEbusyOnPath: dirPath,
|
||||||
wantErr: "device or resource busy",
|
wantErr: "device or resource busy",
|
||||||
},
|
},
|
||||||
"error_no_value_file": {
|
"error_no_value_file": {
|
||||||
mockPaths: []string{exportPath, dirPath, unexportPath},
|
mockPaths: []string{exportPath, dirPath, unexportPath},
|
||||||
@ -281,7 +283,8 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
// arrange
|
// arrange
|
||||||
fs := newMockFilesystem(tc.mockPaths)
|
fs := newMockFilesystem(tc.mockPaths)
|
||||||
pin := newDigitalPinSysfs(fs, "10")
|
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
|
||||||
|
pin := newDigitalPinSysfs(&sfa, "10")
|
||||||
if tc.changeDirection != "" {
|
if tc.changeDirection != "" {
|
||||||
pin.direction = tc.changeDirection
|
pin.direction = tc.changeDirection
|
||||||
}
|
}
|
||||||
@ -307,17 +310,9 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
pin.pollInterval = tc.changePollInterval
|
pin.pollInterval = tc.changePollInterval
|
||||||
}
|
}
|
||||||
// arrange write function
|
// arrange write function
|
||||||
oldWriteFunc := writeFile
|
if tc.simEbusyOnPath != "" {
|
||||||
numCallsWrite := 0
|
fs.Files[tc.simEbusyOnPath].simulateWriteError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
writeFile = func(f File, data []byte) error {
|
|
||||||
numCallsWrite++
|
|
||||||
require.NoError(t, oldWriteFunc(f, data))
|
|
||||||
if numCallsWrite == tc.simEbusyOnWrite {
|
|
||||||
return &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
defer func() { writeFile = oldWriteFunc }()
|
|
||||||
// act
|
// act
|
||||||
err := pin.Export()
|
err := pin.Export()
|
||||||
// assert
|
// assert
|
||||||
@ -333,7 +328,7 @@ func TestDigitalPinExportSysfs(t *testing.T) {
|
|||||||
assert.Equal(t, tc.wantInverse, fs.Files[inversePath].Contents)
|
assert.Equal(t, tc.wantInverse, fs.Files[inversePath].Contents)
|
||||||
}
|
}
|
||||||
assert.Equal(t, tc.wantUnexport, fs.Files[unexportPath].Contents)
|
assert.Equal(t, tc.wantUnexport, fs.Files[unexportPath].Contents)
|
||||||
assert.Equal(t, tc.wantWrites, numCallsWrite)
|
assert.Equal(t, tc.wantWrites, fs.numCallsWrite)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,7 +363,8 @@ func TestDigitalPinSysfs(t *testing.T) {
|
|||||||
data, _ := pin.Read()
|
data, _ := pin.Read()
|
||||||
assert.Equal(t, 1, data)
|
assert.Equal(t, 1, data)
|
||||||
|
|
||||||
pin2 := newDigitalPinSysfs(fs, "30")
|
sfa := sysfsFileAccess{fs: fs, readBufLen: 2}
|
||||||
|
pin2 := newDigitalPinSysfs(&sfa, "30")
|
||||||
err = pin2.Write(1)
|
err = pin2.Write(1)
|
||||||
require.ErrorContains(t, err, "pin has not been exported")
|
require.ErrorContains(t, err, "pin has not been exported")
|
||||||
|
|
||||||
@ -376,33 +372,47 @@ func TestDigitalPinSysfs(t *testing.T) {
|
|||||||
require.ErrorContains(t, err, "pin has not been exported")
|
require.ErrorContains(t, err, "pin has not been exported")
|
||||||
assert.Equal(t, 0, data)
|
assert.Equal(t, 0, data)
|
||||||
|
|
||||||
writeFile = func(File, []byte) error {
|
// arrange: unexport general write error, the error is not suppressed
|
||||||
return &os.PathError{Err: Syscall_EINVAL}
|
fs.Files["/sys/class/gpio/unexport"].simulateWriteError = &os.PathError{Err: errors.New("write error")}
|
||||||
}
|
// act: unexport
|
||||||
|
|
||||||
err = pin.Unexport()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
writeFile = func(File, []byte) error {
|
|
||||||
return &os.PathError{Err: errors.New("write error")}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = pin.Unexport()
|
err = pin.Unexport()
|
||||||
|
// assert: the error is not suppressed
|
||||||
var pathError *os.PathError
|
var pathError *os.PathError
|
||||||
require.ErrorAs(t, err, &pathError)
|
require.ErrorAs(t, err, &pathError)
|
||||||
require.ErrorContains(t, err, "write error")
|
require.ErrorContains(t, err, "write error")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDigitalPinUnexportErrorSysfs(t *testing.T) {
|
func TestDigitalPinUnexportErrorSysfs(t *testing.T) {
|
||||||
mockPaths := []string{
|
tests := map[string]struct {
|
||||||
"/sys/class/gpio/unexport",
|
simulateError error
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
"reserved_pin": {
|
||||||
|
// simulation of reserved pin, the internal error is suppressed
|
||||||
|
simulateError: &os.PathError{Err: Syscall_EINVAL},
|
||||||
|
wantErr: "",
|
||||||
|
},
|
||||||
|
"error_busy": {
|
||||||
|
simulateError: &os.PathError{Err: Syscall_EBUSY},
|
||||||
|
wantErr: " : device or resource busy",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
pin, _ := initTestDigitalPinSysfsWithMockedFilesystem(mockPaths)
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
writeFile = func(File, []byte) error {
|
// arrange
|
||||||
return &os.PathError{Err: Syscall_EBUSY}
|
mockPaths := []string{
|
||||||
|
"/sys/class/gpio/unexport",
|
||||||
|
}
|
||||||
|
pin, fs := initTestDigitalPinSysfsWithMockedFilesystem(mockPaths)
|
||||||
|
fs.Files["/sys/class/gpio/unexport"].simulateWriteError = tc.simulateError
|
||||||
|
// act
|
||||||
|
err := pin.Unexport()
|
||||||
|
// assert
|
||||||
|
if tc.wantErr != "" {
|
||||||
|
require.ErrorContains(t, err, tc.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err := pin.Unexport()
|
|
||||||
require.ErrorContains(t, err, " : device or resource busy")
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -21,16 +22,20 @@ type MockFilesystem struct {
|
|||||||
WithReadError bool
|
WithReadError bool
|
||||||
WithWriteError bool
|
WithWriteError bool
|
||||||
WithCloseError bool
|
WithCloseError bool
|
||||||
|
numCallsWrite int
|
||||||
|
numCallsRead int
|
||||||
}
|
}
|
||||||
|
|
||||||
// A MockFile represents a mock file that contains a single string. Any write
|
// A MockFile represents a mock file that contains a single string. Any write
|
||||||
// overwrites, and any read returns from the start.
|
// overwrites, and any read returns from the start.
|
||||||
type MockFile struct {
|
type MockFile struct {
|
||||||
Contents string
|
Contents string
|
||||||
Seq int // When this file was last written or read.
|
Seq int // When this file was last written or read.
|
||||||
Opened bool
|
Opened bool
|
||||||
Closed bool
|
Closed bool
|
||||||
fd uintptr
|
fd uintptr
|
||||||
|
simulateWriteError error
|
||||||
|
simulateReadError error
|
||||||
|
|
||||||
fs *MockFilesystem
|
fs *MockFilesystem
|
||||||
}
|
}
|
||||||
@ -46,6 +51,7 @@ func (f *MockFile) Write(b []byte) (int, error) {
|
|||||||
if f.fs.WithWriteError {
|
if f.fs.WithWriteError {
|
||||||
return 0, errWrite
|
return 0, errWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.WriteString(string(b))
|
return f.WriteString(string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +64,8 @@ func (f *MockFile) Seek(offset int64, whence int) (int64, error) {
|
|||||||
func (f *MockFile) WriteString(s string) (int, error) {
|
func (f *MockFile) WriteString(s string) (int, error) {
|
||||||
f.Contents = s
|
f.Contents = s
|
||||||
f.Seq = f.fs.next()
|
f.Seq = f.fs.next()
|
||||||
return len(s), nil
|
f.fs.numCallsWrite++
|
||||||
|
return len(s), f.simulateWriteError
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync implements the File interface Sync function
|
// Sync implements the File interface Sync function
|
||||||
@ -79,7 +86,8 @@ func (f *MockFile) Read(b []byte) (int, error) {
|
|||||||
copy(b, []byte(f.Contents)[:count])
|
copy(b, []byte(f.Contents)[:count])
|
||||||
f.Seq = f.fs.next()
|
f.Seq = f.fs.next()
|
||||||
|
|
||||||
return count, nil
|
f.fs.numCallsRead++
|
||||||
|
return count, f.simulateReadError
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadAt calls MockFile.Read
|
// ReadAt calls MockFile.Read
|
||||||
@ -135,9 +143,10 @@ func (fs *MockFilesystem) openFile(name string, _ int, _ os.FileMode) (File, err
|
|||||||
func (fs *MockFilesystem) stat(name string) (os.FileInfo, error) {
|
func (fs *MockFilesystem) stat(name string) (os.FileInfo, error) {
|
||||||
_, ok := fs.Files[name]
|
_, ok := fs.Files[name]
|
||||||
if ok {
|
if ok {
|
||||||
// return file based mock FileInfo
|
// return file based mock FileInfo, CreateTemp don't like "/" in between
|
||||||
tmpFile, err := os.CreateTemp("", name)
|
tmpFile, err := os.CreateTemp("", strings.ReplaceAll(name, "/", "_"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Println("A")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer os.Remove(tmpFile.Name())
|
defer os.Remove(tmpFile.Name())
|
||||||
@ -194,6 +203,8 @@ func (fs *MockFilesystem) readFile(name string) ([]byte, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, &os.PathError{Err: fmt.Errorf("%s: no such file", name)}
|
return nil, &os.PathError{Err: fmt.Errorf("%s: no such file", name)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f.fs.numCallsRead++
|
||||||
return []byte(f.Contents), nil
|
return []byte(f.Contents), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,30 +24,25 @@ type pwmPinSysFs struct {
|
|||||||
polarityNormalIdentifier string
|
polarityNormalIdentifier string
|
||||||
polarityInvertedIdentifier string
|
polarityInvertedIdentifier string
|
||||||
|
|
||||||
write func(fs filesystem, path string, data []byte) (i int, err error)
|
sfa *sysfsFileAccess
|
||||||
read func(fs filesystem, path string) ([]byte, error)
|
|
||||||
fs filesystem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newPWMPinSysfs returns a new pwmPin, working with sysfs file access.
|
// newPWMPinSysfs returns a new pwmPin, working with sysfs file access.
|
||||||
func newPWMPinSysfs(fs filesystem, path string, pin int, polNormIdent string, polInvIdent string) *pwmPinSysFs {
|
func newPWMPinSysfs(sfa *sysfsFileAccess, path string, pin int, polNormIdent string, polInvIdent string) *pwmPinSysFs {
|
||||||
p := &pwmPinSysFs{
|
p := &pwmPinSysFs{
|
||||||
path: path,
|
path: path,
|
||||||
pin: strconv.Itoa(pin),
|
pin: strconv.Itoa(pin),
|
||||||
polarityNormalIdentifier: polNormIdent,
|
polarityNormalIdentifier: polNormIdent,
|
||||||
polarityInvertedIdentifier: polInvIdent,
|
polarityInvertedIdentifier: polInvIdent,
|
||||||
|
|
||||||
read: readPwmFile,
|
sfa: sfa,
|
||||||
write: writePwmFile,
|
|
||||||
fs: fs,
|
|
||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export exports the pin for use by the operating system by writing the pin to the "Export" path
|
// Export exports the pin for use by the operating system by writing the pin to the "Export" path
|
||||||
func (p *pwmPinSysFs) Export() error {
|
func (p *pwmPinSysFs) Export() error {
|
||||||
_, err := p.write(p.fs, p.pwmExportPath(), []byte(p.pin))
|
if err := p.sfa.write(p.pwmExportPath(), []byte(p.pin)); err != nil {
|
||||||
if err != nil {
|
|
||||||
// If EBUSY then the pin has already been exported, we suppress the error
|
// If EBUSY then the pin has already been exported, we suppress the error
|
||||||
var pathError *os.PathError
|
var pathError *os.PathError
|
||||||
if !(errors.As(err, &pathError) && errors.Is(err, Syscall_EBUSY)) {
|
if !(errors.As(err, &pathError) && errors.Is(err, Syscall_EBUSY)) {
|
||||||
@ -65,7 +60,7 @@ func (p *pwmPinSysFs) Export() error {
|
|||||||
|
|
||||||
// Unexport releases the pin from the operating system by writing the pin to the "Unexport" path
|
// Unexport releases the pin from the operating system by writing the pin to the "Unexport" path
|
||||||
func (p *pwmPinSysFs) Unexport() error {
|
func (p *pwmPinSysFs) Unexport() error {
|
||||||
if _, err := p.write(p.fs, p.pwmUnexportPath(), []byte(p.pin)); err != nil {
|
if err := p.sfa.write(p.pwmUnexportPath(), []byte(p.pin)); err != nil {
|
||||||
return fmt.Errorf(pwmPinErrorPattern, "Unexport", p.pin, err)
|
return fmt.Errorf(pwmPinErrorPattern, "Unexport", p.pin, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -73,16 +68,12 @@ func (p *pwmPinSysFs) Unexport() error {
|
|||||||
|
|
||||||
// Enabled reads and returns the enabled state of the pin
|
// Enabled reads and returns the enabled state of the pin
|
||||||
func (p *pwmPinSysFs) Enabled() (bool, error) {
|
func (p *pwmPinSysFs) Enabled() (bool, error) {
|
||||||
buf, err := p.read(p.fs, p.pwmEnablePath())
|
val, err := p.sfa.readInteger(p.pwmEnablePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf(pwmPinErrorPattern, "Enabled", p.pin, err)
|
return false, fmt.Errorf(pwmPinErrorPattern, "Enabled", p.pin, err)
|
||||||
}
|
}
|
||||||
if len(buf) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
val, e := strconv.Atoi(string(bytes.TrimRight(buf, "\n")))
|
return val > 0, nil
|
||||||
return val > 0, e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetEnabled writes enable(1) or disable(0) status. For most platforms this is only possible if period was
|
// SetEnabled writes enable(1) or disable(0) status. For most platforms this is only possible if period was
|
||||||
@ -92,7 +83,7 @@ func (p *pwmPinSysFs) SetEnabled(enable bool) error {
|
|||||||
if enable {
|
if enable {
|
||||||
enableVal = 1
|
enableVal = 1
|
||||||
}
|
}
|
||||||
if _, err := p.write(p.fs, p.pwmEnablePath(), []byte(strconv.Itoa(enableVal))); err != nil {
|
if err := p.sfa.writeInteger(p.pwmEnablePath(), enableVal); err != nil {
|
||||||
if pwmDebug {
|
if pwmDebug {
|
||||||
p.printState()
|
p.printState()
|
||||||
}
|
}
|
||||||
@ -104,7 +95,7 @@ func (p *pwmPinSysFs) SetEnabled(enable bool) error {
|
|||||||
|
|
||||||
// Polarity reads and returns false if the polarity is inverted, otherwise true
|
// Polarity reads and returns false if the polarity is inverted, otherwise true
|
||||||
func (p *pwmPinSysFs) Polarity() (bool, error) {
|
func (p *pwmPinSysFs) Polarity() (bool, error) {
|
||||||
buf, err := p.read(p.fs, p.pwmPolarityPath())
|
buf, err := p.sfa.read(p.pwmPolarityPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, fmt.Errorf(pwmPinErrorPattern, "Polarity", p.pin, err)
|
return true, fmt.Errorf(pwmPinErrorPattern, "Polarity", p.pin, err)
|
||||||
}
|
}
|
||||||
@ -133,7 +124,7 @@ func (p *pwmPinSysFs) SetPolarity(normal bool) error {
|
|||||||
if !normal {
|
if !normal {
|
||||||
value = p.polarityInvertedIdentifier
|
value = p.polarityInvertedIdentifier
|
||||||
}
|
}
|
||||||
if _, err := p.write(p.fs, p.pwmPolarityPath(), []byte(value)); err != nil {
|
if err := p.sfa.write(p.pwmPolarityPath(), []byte(value)); err != nil {
|
||||||
if pwmDebug {
|
if pwmDebug {
|
||||||
p.printState()
|
p.printState()
|
||||||
}
|
}
|
||||||
@ -144,23 +135,17 @@ func (p *pwmPinSysFs) SetPolarity(normal bool) error {
|
|||||||
|
|
||||||
// Period returns the current period
|
// Period returns the current period
|
||||||
func (p *pwmPinSysFs) Period() (uint32, error) {
|
func (p *pwmPinSysFs) Period() (uint32, error) {
|
||||||
buf, err := p.read(p.fs, p.pwmPeriodPath())
|
val, err := p.sfa.readInteger(p.pwmPeriodPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf(pwmPinErrorPattern, "Period", p.pin, err)
|
return 0, fmt.Errorf(pwmPinErrorPattern, "Period", p.pin, err)
|
||||||
}
|
}
|
||||||
if len(buf) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v := bytes.TrimRight(buf, "\n")
|
return uint32(val), nil
|
||||||
val, e := strconv.Atoi(string(v))
|
|
||||||
return uint32(val), e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPeriod writes the current period in nanoseconds
|
// SetPeriod writes the current period in nanoseconds
|
||||||
func (p *pwmPinSysFs) SetPeriod(period uint32) error {
|
func (p *pwmPinSysFs) SetPeriod(period uint32) error {
|
||||||
//nolint:perfsprint // ok here
|
if err := p.sfa.writeInteger(p.pwmPeriodPath(), int(period)); err != nil {
|
||||||
if _, err := p.write(p.fs, p.pwmPeriodPath(), []byte(fmt.Sprintf("%v", period))); err != nil {
|
|
||||||
|
|
||||||
if pwmDebug {
|
if pwmDebug {
|
||||||
p.printState()
|
p.printState()
|
||||||
@ -172,19 +157,17 @@ func (p *pwmPinSysFs) SetPeriod(period uint32) error {
|
|||||||
|
|
||||||
// DutyCycle reads and returns the duty cycle in nanoseconds
|
// DutyCycle reads and returns the duty cycle in nanoseconds
|
||||||
func (p *pwmPinSysFs) DutyCycle() (uint32, error) {
|
func (p *pwmPinSysFs) DutyCycle() (uint32, error) {
|
||||||
buf, err := p.read(p.fs, p.pwmDutyCyclePath())
|
val, err := p.sfa.readInteger(p.pwmDutyCyclePath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf(pwmPinErrorPattern, "DutyCycle", p.pin, err)
|
return 0, fmt.Errorf(pwmPinErrorPattern, "DutyCycle", p.pin, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
val, err := strconv.Atoi(string(bytes.TrimRight(buf, "\n")))
|
|
||||||
return uint32(val), err
|
return uint32(val), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDutyCycle writes the duty cycle in nanoseconds
|
// SetDutyCycle writes the duty cycle in nanoseconds
|
||||||
func (p *pwmPinSysFs) SetDutyCycle(duty uint32) error {
|
func (p *pwmPinSysFs) SetDutyCycle(duty uint32) error {
|
||||||
//nolint:perfsprint // ok here
|
if err := p.sfa.writeInteger(p.pwmDutyCyclePath(), int(duty)); err != nil {
|
||||||
if _, err := p.write(p.fs, p.pwmDutyCyclePath(), []byte(fmt.Sprintf("%v", duty))); err != nil {
|
|
||||||
if pwmDebug {
|
if pwmDebug {
|
||||||
p.printState()
|
p.printState()
|
||||||
}
|
}
|
||||||
@ -223,32 +206,6 @@ func (p *pwmPinSysFs) pwmPolarityPath() string {
|
|||||||
return path.Join(p.path, "pwm"+p.pin, "polarity")
|
return path.Join(p.path, "pwm"+p.pin, "polarity")
|
||||||
}
|
}
|
||||||
|
|
||||||
func writePwmFile(fs filesystem, path string, data []byte) (int, error) {
|
|
||||||
file, err := fs.openFile(path, os.O_WRONLY, 0o644)
|
|
||||||
defer file.Close() //nolint:staticcheck // for historical reasons
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readPwmFile(fs filesystem, path string) ([]byte, error) {
|
|
||||||
file, err := fs.openFile(path, os.O_RDONLY, 0o644)
|
|
||||||
defer file.Close() //nolint:staticcheck // for historical reasons
|
|
||||||
if err != nil {
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 200)
|
|
||||||
var i int
|
|
||||||
i, err = file.Read(buf)
|
|
||||||
if i == 0 {
|
|
||||||
return []byte{}, err
|
|
||||||
}
|
|
||||||
return buf[:i], err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pwmPinSysFs) printState() {
|
func (p *pwmPinSysFs) printState() {
|
||||||
enabled, _ := p.Enabled()
|
enabled, _ := p.Enabled()
|
||||||
polarity, _ := p.Polarity()
|
polarity, _ := p.Polarity()
|
||||||
|
@ -19,7 +19,8 @@ const (
|
|||||||
|
|
||||||
func initTestPWMPinSysFsWithMockedFilesystem(mockPaths []string) (*pwmPinSysFs, *MockFilesystem) {
|
func initTestPWMPinSysFsWithMockedFilesystem(mockPaths []string) (*pwmPinSysFs, *MockFilesystem) {
|
||||||
fs := newMockFilesystem(mockPaths)
|
fs := newMockFilesystem(mockPaths)
|
||||||
pin := newPWMPinSysfs(fs, "/sys/class/pwm/pwmchip0", 10, normal, inverted)
|
sfa := &sysfsFileAccess{fs: fs, readBufLen: 200}
|
||||||
|
pin := newPWMPinSysfs(sfa, "/sys/class/pwm/pwmchip0", 10, normal, inverted)
|
||||||
return pin, fs
|
return pin, fs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +77,7 @@ func TestPwmPin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinAlreadyExported(t *testing.T) {
|
func TestPwmPinAlreadyExported(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
@ -83,17 +85,14 @@ func TestPwmPinAlreadyExported(t *testing.T) {
|
|||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/export"].simulateWriteError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
pin.write = func(filesystem, string, []byte) (int, error) {
|
// act & assert: no error indicates that the pin was already exported
|
||||||
return 0, &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no error indicates that the pin was already exported
|
|
||||||
require.NoError(t, pin.Export())
|
require.NoError(t, pin.Export())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinExportError(t *testing.T) {
|
func TestPwmPinExportError(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
@ -101,18 +100,16 @@ func TestPwmPinExportError(t *testing.T) {
|
|||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/export"].simulateWriteError = &os.PathError{Err: Syscall_EFAULT}
|
||||||
pin.write = func(filesystem, string, []byte) (int, error) {
|
// act
|
||||||
return 0, &os.PathError{Err: Syscall_EFAULT}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no error indicates that the pin was already exported
|
|
||||||
err := pin.Export()
|
err := pin.Export()
|
||||||
|
// assert
|
||||||
require.ErrorContains(t, err, "Export() failed for id 10 with : bad address")
|
require.ErrorContains(t, err, "Export() failed for id 10 with : bad address")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinUnxportError(t *testing.T) {
|
func TestPwmPinUnxportError(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
@ -120,17 +117,16 @@ func TestPwmPinUnxportError(t *testing.T) {
|
|||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/unexport"].simulateWriteError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
pin.write = func(filesystem, string, []byte) (int, error) {
|
// act
|
||||||
return 0, &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := pin.Unexport()
|
err := pin.Unexport()
|
||||||
|
// assert
|
||||||
require.ErrorContains(t, err, "Unexport() failed for id 10 with : device or resource busy")
|
require.ErrorContains(t, err, "Unexport() failed for id 10 with : device or resource busy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinPeriodError(t *testing.T) {
|
func TestPwmPinPeriodError(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
@ -138,35 +134,34 @@ func TestPwmPinPeriodError(t *testing.T) {
|
|||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/pwm10/period"].simulateReadError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
pin.read = func(filesystem, string) ([]byte, error) {
|
// act
|
||||||
return nil, &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := pin.Period()
|
_, err := pin.Period()
|
||||||
|
// assert
|
||||||
require.ErrorContains(t, err, "Period() failed for id 10 with : device or resource busy")
|
require.ErrorContains(t, err, "Period() failed for id 10 with : device or resource busy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinPolarityError(t *testing.T) {
|
func TestPwmPinPolarityError(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/enable",
|
"/sys/class/pwm/pwmchip0/pwm10/enable",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
|
"/sys/class/pwm/pwmchip0/pwm10/polarity",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/pwm10/polarity"].simulateReadError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
pin.read = func(filesystem, string) ([]byte, error) {
|
// act
|
||||||
return nil, &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := pin.Polarity()
|
_, err := pin.Polarity()
|
||||||
|
// assert
|
||||||
require.ErrorContains(t, err, "Polarity() failed for id 10 with : device or resource busy")
|
require.ErrorContains(t, err, "Polarity() failed for id 10 with : device or resource busy")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPwmPinDutyCycleError(t *testing.T) {
|
func TestPwmPinDutyCycleError(t *testing.T) {
|
||||||
|
// arrange
|
||||||
mockedPaths := []string{
|
mockedPaths := []string{
|
||||||
"/sys/class/pwm/pwmchip0/export",
|
"/sys/class/pwm/pwmchip0/export",
|
||||||
"/sys/class/pwm/pwmchip0/unexport",
|
"/sys/class/pwm/pwmchip0/unexport",
|
||||||
@ -174,12 +169,10 @@ func TestPwmPinDutyCycleError(t *testing.T) {
|
|||||||
"/sys/class/pwm/pwmchip0/pwm10/period",
|
"/sys/class/pwm/pwmchip0/pwm10/period",
|
||||||
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
"/sys/class/pwm/pwmchip0/pwm10/duty_cycle",
|
||||||
}
|
}
|
||||||
pin, _ := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
pin, fs := initTestPWMPinSysFsWithMockedFilesystem(mockedPaths)
|
||||||
|
fs.Files["/sys/class/pwm/pwmchip0/pwm10/duty_cycle"].simulateReadError = &os.PathError{Err: Syscall_EBUSY}
|
||||||
pin.read = func(filesystem, string) ([]byte, error) {
|
// act
|
||||||
return nil, &os.PathError{Err: Syscall_EBUSY}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := pin.DutyCycle()
|
_, err := pin.DutyCycle()
|
||||||
|
// assert
|
||||||
require.ErrorContains(t, err, "DutyCycle() failed for id 10 with : device or resource busy")
|
require.ErrorContains(t, err, "DutyCycle() failed for id 10 with : device or resource busy")
|
||||||
}
|
}
|
||||||
|
146
system/sysfsfile_access.go
Normal file
146
system/sysfsfile_access.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sysfsFileAccess struct {
|
||||||
|
fs filesystem
|
||||||
|
readBufLen uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux sysfs / GPIO specific sysfs docs.
|
||||||
|
//
|
||||||
|
// https://www.kernel.org/doc/Documentation/filesystems/sysfs.txt
|
||||||
|
// https://www.kernel.org/doc/Documentation/gpio/sysfs.txt
|
||||||
|
// https://www.kernel.org/doc/Documentation/thermal/sysfs-api.txt
|
||||||
|
// see also PWM.md
|
||||||
|
type sysfsFile struct {
|
||||||
|
sfa sysfsFileAccess
|
||||||
|
file File
|
||||||
|
sysfsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) readInteger(path string) (int, error) {
|
||||||
|
sf, err := sfa.openRead(path)
|
||||||
|
defer func() { _ = sf.close() }()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.readInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) read(path string) ([]byte, error) {
|
||||||
|
sf, err := sfa.openRead(path)
|
||||||
|
defer func() { _ = sf.close() }()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) writeInteger(path string, val int) error {
|
||||||
|
sf, err := sfa.openWrite(path)
|
||||||
|
defer func() { _ = sf.close() }()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.writeInteger(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) write(path string, data []byte) error {
|
||||||
|
sf, err := sfa.openWrite(path)
|
||||||
|
defer func() { _ = sf.close() }()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sf.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) openRead(path string) (*sysfsFile, error) {
|
||||||
|
f, err := sfa.fs.openFile(path, os.O_RDONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sysfsFile{sfa: sfa, file: f, sysfsPath: path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) openWrite(path string) (*sysfsFile, error) {
|
||||||
|
f, err := sfa.fs.openFile(path, os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sysfsFile{sfa: sfa, file: f, sysfsPath: path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sfa sysfsFileAccess) openReadWrite(path string) (*sysfsFile, error) {
|
||||||
|
f, err := sfa.fs.openFile(path, os.O_RDWR, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sysfsFile{sfa: sfa, file: f, sysfsPath: path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *sysfsFile) close() error {
|
||||||
|
if sf == nil || sf.file == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return sf.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *sysfsFile) readInteger() (int, error) {
|
||||||
|
buf, err := sf.read()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(strings.Split(string(buf), "\n")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *sysfsFile) read() ([]byte, error) {
|
||||||
|
// sysfs docs say:
|
||||||
|
// > If userspace seeks back to zero or does a pread(2) with an offset of '0' the [..] method will
|
||||||
|
// > be called again, rearmed, to fill the buffer.
|
||||||
|
// > The buffer will always be PAGE_SIZE bytes in length. On i386, this is 4096.
|
||||||
|
|
||||||
|
// TODO: Examine if seek is needed if full buffer is read from sysfs file.
|
||||||
|
|
||||||
|
buf := make([]byte, sf.sfa.readBufLen)
|
||||||
|
if _, err := sf.file.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := sf.file.Read(buf)
|
||||||
|
if i == 0 {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf[:i], err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *sysfsFile) writeInteger(val int) error {
|
||||||
|
return sf.write([]byte(strconv.Itoa(val)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf *sysfsFile) write(data []byte) error {
|
||||||
|
// sysfs docs say:
|
||||||
|
// > When writing sysfs files, userspace processes should first read the
|
||||||
|
// > entire file, modify the values it wishes to change, then write the
|
||||||
|
// > entire buffer back.
|
||||||
|
// however, this seems outdated/inaccurate (docs are from back in the Kernel BitKeeper days).
|
||||||
|
|
||||||
|
// Write() returns already a non-nil error when n != len(b).
|
||||||
|
_, err := sf.file.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
@ -72,7 +72,7 @@ func NewAccesser(options ...func(Optioner)) *Accesser {
|
|||||||
fs: &nativeFilesystem{},
|
fs: &nativeFilesystem{},
|
||||||
}
|
}
|
||||||
s.spiAccess = &periphioSpiAccess{fs: s.fs}
|
s.spiAccess = &periphioSpiAccess{fs: s.fs}
|
||||||
s.digitalPinAccess = &sysfsDigitalPinAccess{fs: s.fs}
|
s.digitalPinAccess = &sysfsDigitalPinAccess{sfa: &sysfsFileAccess{fs: s.fs, readBufLen: 2}}
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(s)
|
option(s)
|
||||||
}
|
}
|
||||||
@ -85,7 +85,7 @@ func (a *Accesser) UseDigitalPinAccessWithMockFs(digitalPinAccess string, files
|
|||||||
var dph digitalPinAccesser
|
var dph digitalPinAccesser
|
||||||
switch digitalPinAccess {
|
switch digitalPinAccess {
|
||||||
case "sysfs":
|
case "sysfs":
|
||||||
dph = &sysfsDigitalPinAccess{fs: fs}
|
dph = &sysfsDigitalPinAccess{sfa: &sysfsFileAccess{fs: fs, readBufLen: 2}}
|
||||||
case "cdev":
|
case "cdev":
|
||||||
dph = &gpiodDigitalPinAccess{fs: fs}
|
dph = &gpiodDigitalPinAccess{fs: fs}
|
||||||
default:
|
default:
|
||||||
@ -135,7 +135,15 @@ func (a *Accesser) IsSysfsDigitalPinAccess() bool {
|
|||||||
|
|
||||||
// NewPWMPin returns a new system PWM pin, according to the given pin number.
|
// NewPWMPin returns a new system PWM pin, according to the given pin number.
|
||||||
func (a *Accesser) NewPWMPin(path string, pin int, polNormIdent string, polInvIdent string) gobot.PWMPinner {
|
func (a *Accesser) NewPWMPin(path string, pin int, polNormIdent string, polInvIdent string) gobot.PWMPinner {
|
||||||
return newPWMPinSysfs(a.fs, path, pin, polNormIdent, polInvIdent)
|
sfa := &sysfsFileAccess{fs: a.fs, readBufLen: 200}
|
||||||
|
return newPWMPinSysfs(sfa, path, pin, polNormIdent, polInvIdent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Accesser) NewAnalogPin(path string, r, w bool, readBufLen uint16) gobot.AnalogPinner {
|
||||||
|
if readBufLen == 0 {
|
||||||
|
readBufLen = 32 // max. count of characters for int value is 20
|
||||||
|
}
|
||||||
|
return newAnalogPinSysfs(&sysfsFileAccess{fs: a.fs, readBufLen: readBufLen}, path, r, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSpiDevice returns a new connection to SPI with the given parameters.
|
// NewSpiDevice returns a new connection to SPI with the given parameters.
|
||||||
|
@ -82,7 +82,7 @@ func TestNewAccesser_IsSysfsDigitalPinAccess(t *testing.T) {
|
|||||||
assert.True(t, got)
|
assert.True(t, got)
|
||||||
dpaSys := a.digitalPinAccess.(*sysfsDigitalPinAccess)
|
dpaSys := a.digitalPinAccess.(*sysfsDigitalPinAccess)
|
||||||
assert.NotNil(t, dpaSys)
|
assert.NotNil(t, dpaSys)
|
||||||
assert.Equal(t, a.fs.(*nativeFilesystem), dpaSys.fs)
|
assert.Equal(t, a.fs.(*nativeFilesystem), dpaSys.sfa.fs)
|
||||||
} else {
|
} else {
|
||||||
assert.False(t, got)
|
assert.False(t, got)
|
||||||
dpaGpiod := a.digitalPinAccess.(*gpiodDigitalPinAccess)
|
dpaGpiod := a.digitalPinAccess.(*gpiodDigitalPinAccess)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user