From daff1dd51d1658c3e93c4e48a6db0dc2655b6d7e Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Sun, 3 Dec 2023 20:09:31 +0100 Subject: [PATCH] adaptors(pwm): introduce scale option for servo --- examples/tinkerboard_servo.go | 16 +- platforms/adaptors/digitalpinsadaptor.go | 269 +++--------------- platforms/adaptors/digitalpinsadaptor_test.go | 108 ------- .../adaptors/digitalpinsadaptoroptions.go | 157 ++++++++++ .../digitalpinsadaptoroptions_test.go | 120 ++++++++ platforms/adaptors/options.go | 7 - platforms/adaptors/pwmpinsadaptor.go | 206 ++++++++------ platforms/adaptors/pwmpinsadaptor_test.go | 57 +--- platforms/adaptors/pwmpinsadaptoroptions.go | 118 ++++++++ .../adaptors/pwmpinsadaptoroptions_test.go | 148 ++++++++++ platforms/beaglebone/beaglebone_adaptor.go | 108 ++++--- .../beaglebone/beaglebone_adaptor_test.go | 134 ++++++--- platforms/beaglebone/pocketbeagle_adaptor.go | 9 +- platforms/chip/chip_adaptor.go | 77 +++-- platforms/chip/chip_adaptor_test.go | 46 +-- platforms/dragonboard/dragonboard_adaptor.go | 2 +- platforms/intel-iot/edison/edison_adaptor.go | 201 +++++++------ .../intel-iot/edison/edison_adaptor_test.go | 17 +- platforms/intel-iot/joule/joule_adaptor.go | 65 +++-- platforms/jetson/jetson_adaptor.go | 2 +- platforms/nanopi/nanopi_adaptor.go | 89 +++--- platforms/nanopi/nanopi_adaptor_test.go | 38 ++- platforms/raspi/raspi_adaptor.go | 2 +- platforms/rockpi/rockpi_adaptor.go | 2 +- platforms/tinkerboard/adaptor.go | 94 +++--- platforms/tinkerboard/adaptor_test.go | 80 ++++-- platforms/upboard/up2/adaptor.go | 86 +++--- platforms/upboard/up2/adaptor_test.go | 40 ++- 28 files changed, 1395 insertions(+), 903 deletions(-) create mode 100644 platforms/adaptors/digitalpinsadaptoroptions.go create mode 100644 platforms/adaptors/digitalpinsadaptoroptions_test.go delete mode 100644 platforms/adaptors/options.go create mode 100644 platforms/adaptors/pwmpinsadaptoroptions.go create mode 100644 platforms/adaptors/pwmpinsadaptoroptions_test.go diff --git a/examples/tinkerboard_servo.go b/examples/tinkerboard_servo.go index 5be3fce63..4e81503b7 100644 --- a/examples/tinkerboard_servo.go +++ b/examples/tinkerboard_servo.go @@ -12,6 +12,7 @@ import ( "gobot.io/x/gobot/v2" "gobot.io/x/gobot/v2/drivers/gpio" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/platforms/tinkerboard" ) @@ -25,15 +26,16 @@ func main() { fiftyHzNanos = 20 * 1000 * 1000 // 50Hz = 0.02 sec = 20 ms ) - adaptor := tinkerboard.NewAdaptor() + // usually a frequency of 50Hz is used for servos, most servos have 0.5 ms..2.5 ms for 0-180°, + // however the mapping can be changed with options: + adaptor := tinkerboard.NewAdaptor( + adaptors.WithPWMDefaultPeriodForPin(pwmPin, fiftyHzNanos), + adaptors.WithPWMServoDutyCycleRangeForPin(pwmPin, time.Millisecond, 2*time.Millisecond), + adaptors.WithPWMServoAngleRangeForPin(pwmPin, 0, 270), + ) servo := gpio.NewServoDriver(adaptor, pwmPin) work := func() { - // usually a frequency of 50Hz is used for servos - if err := adaptor.SetPeriod(pwmPin, fiftyHzNanos); err != nil { - log.Println(err) - } - fmt.Printf("first move to minimal position for %s...\n", wait) if err := servo.ToMin(); err != nil { log.Println(err) @@ -55,7 +57,7 @@ func main() { time.Sleep(wait) - fmt.Println("finally move 0-180° and back forever...") + fmt.Println("finally move 0-180° (or what your servo do for the new mapping) and back forever...") angle := 0 fadeAmount := 45 diff --git a/platforms/adaptors/digitalpinsadaptor.go b/platforms/adaptors/digitalpinsadaptor.go index e62d91c70..d60aa9be6 100644 --- a/platforms/adaptors/digitalpinsadaptor.go +++ b/platforms/adaptors/digitalpinsadaptor.go @@ -16,25 +16,6 @@ type ( digitalPinInitializer func(gobot.DigitalPinner) error ) -type digitalPinsOptioner interface { - setDigitalPinInitializer(initializer digitalPinInitializer) - setDigitalPinsForSystemGpiod() - setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin string) - prepareDigitalPinsActiveLow(pin string, otherPins ...string) - prepareDigitalPinsPullDown(pin string, otherPins ...string) - prepareDigitalPinsPullUp(pin string, otherPins ...string) - prepareDigitalPinsOpenDrain(pin string, otherPins ...string) - prepareDigitalPinsOpenSource(pin string, otherPins ...string) - prepareDigitalPinDebounce(pin string, period time.Duration) - prepareDigitalPinEventOnFallingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, - detectedEdge string, seqno uint32, lseqno uint32)) - prepareDigitalPinEventOnRisingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, - detectedEdge string, seqno uint32, lseqno uint32)) - prepareDigitalPinEventOnBothEdges(pin string, handler func(lineOffset int, timestamp time.Duration, - detectedEdge string, seqno uint32, lseqno uint32)) - prepareDigitalPinPollForEdgeDetection(pin string, pollInterval time.Duration, pollQuitChan chan struct{}) -} - // DigitalPinsAdaptor is a adaptor for digital pins, normally used for composition in platforms. type DigitalPinsAdaptor struct { sys *system.Accesser @@ -53,7 +34,7 @@ type DigitalPinsAdaptor struct { func NewDigitalPinsAdaptor( sys *system.Accesser, t digitalPinTranslator, - options ...func(Optioner), + options ...func(DigitalPinsOptioner), ) *DigitalPinsAdaptor { a := &DigitalPinsAdaptor{ sys: sys, @@ -67,99 +48,72 @@ func NewDigitalPinsAdaptor( } // WithDigitalPinInitializer can be used to substitute the default initializer. -func WithDigitalPinInitializer(pc digitalPinInitializer) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.setDigitalPinInitializer(pc) - } +func WithDigitalPinInitializer(pc digitalPinInitializer) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.setDigitalPinInitializer(pc) } } // WithGpiodAccess can be used to change the default sysfs implementation to the character device Kernel ABI. // The access is provided by the gpiod package. -func WithGpiodAccess() func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.setDigitalPinsForSystemGpiod() - } +func WithGpiodAccess() func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.setDigitalPinsForSystemGpiod() } } // WithSpiGpioAccess can be used to switch the default SPI implementation to GPIO usage. -func WithSpiGpioAccess(sclkPin, nssPin, mosiPin, misoPin string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin) - } +func WithSpiGpioAccess(sclkPin, nssPin, mosiPin, misoPin string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin) } } // WithGpiosActiveLow prepares the given pins for inverse reaction on next initialize. // This is working for inputs and outputs. -func WithGpiosActiveLow(pin string, otherPins ...string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinsActiveLow(pin, otherPins...) - } +func WithGpiosActiveLow(pin string, otherPins ...string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinsActiveLow(pin, otherPins...) } } // WithGpiosPullDown prepares the given pins to be pulled down (high impedance to GND) on next initialize. // This is working for inputs and outputs since Kernel 5.5, but will be ignored with sysfs ABI. -func WithGpiosPullDown(pin string, otherPins ...string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinsPullDown(pin, otherPins...) - } +func WithGpiosPullDown(pin string, otherPins ...string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinsPullDown(pin, otherPins...) } } // WithGpiosPullUp prepares the given pins to be pulled up (high impedance to VDD) on next initialize. // This is working for inputs and outputs since Kernel 5.5, but will be ignored with sysfs ABI. -func WithGpiosPullUp(pin string, otherPins ...string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinsPullUp(pin, otherPins...) - } +func WithGpiosPullUp(pin string, otherPins ...string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinsPullUp(pin, otherPins...) } } // WithGpiosOpenDrain prepares the given output pins to be driven with open drain/collector on next initialize. // This will be ignored for inputs or with sysfs ABI. -func WithGpiosOpenDrain(pin string, otherPins ...string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinsOpenDrain(pin, otherPins...) - } +func WithGpiosOpenDrain(pin string, otherPins ...string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinsOpenDrain(pin, otherPins...) } } // WithGpiosOpenSource prepares the given output pins to be driven with open source/emitter on next initialize. // This will be ignored for inputs or with sysfs ABI. -func WithGpiosOpenSource(pin string, otherPins ...string) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinsOpenSource(pin, otherPins...) - } +func WithGpiosOpenSource(pin string, otherPins ...string) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinsOpenSource(pin, otherPins...) } } // WithGpioDebounce prepares the given input pin to be debounced on next initialize. // This is working for inputs since Kernel 5.10, but will be ignored for outputs or with sysfs ABI. -func WithGpioDebounce(pin string, period time.Duration) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinDebounce(pin, period) - } +func WithGpioDebounce(pin string, period time.Duration) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinDebounce(pin, period) } } @@ -167,12 +121,9 @@ func WithGpioDebounce(pin string, period time.Duration) func(Optioner) { // This is working for inputs since Kernel 5.10, but will be ignored for outputs or with sysfs ABI. func WithGpioEventOnFallingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32), -) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinEventOnFallingEdge(pin, handler) - } +) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinEventOnFallingEdge(pin, handler) } } @@ -180,12 +131,9 @@ func WithGpioEventOnFallingEdge(pin string, handler func(lineOffset int, timesta // This is working for inputs since Kernel 5.10, but will be ignored for outputs or with sysfs ABI. func WithGpioEventOnRisingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32), -) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinEventOnRisingEdge(pin, handler) - } +) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinEventOnRisingEdge(pin, handler) } } @@ -193,23 +141,21 @@ func WithGpioEventOnRisingEdge(pin string, handler func(lineOffset int, timestam // This is working for inputs since Kernel 5.10, but will be ignored for outputs or with sysfs ABI. func WithGpioEventOnBothEdges(pin string, handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32), -) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinEventOnBothEdges(pin, handler) - } +) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinEventOnBothEdges(pin, handler) } } // WithGpioPollForEdgeDetection prepares the given input pin to use a discrete input pin polling function together with // edge detection. -func WithGpioPollForEdgeDetection(pin string, pollInterval time.Duration, pollQuitChan chan struct{}) func(Optioner) { - return func(o Optioner) { - a, ok := o.(digitalPinsOptioner) - if ok { - a.prepareDigitalPinPollForEdgeDetection(pin, pollInterval, pollQuitChan) - } +func WithGpioPollForEdgeDetection( + pin string, + pollInterval time.Duration, + pollQuitChan chan struct{}, +) func(DigitalPinsOptioner) { + return func(o DigitalPinsOptioner) { + o.prepareDigitalPinPollForEdgeDetection(pin, pollInterval, pollQuitChan) } } @@ -273,133 +219,6 @@ func (a *DigitalPinsAdaptor) DigitalWrite(id string, val byte) error { return pin.Write(int(val)) } -func (a *DigitalPinsAdaptor) setDigitalPinInitializer(pinInit digitalPinInitializer) { - a.initialize = pinInit -} - -func (a *DigitalPinsAdaptor) setDigitalPinsForSystemGpiod() { - system.WithDigitalPinGpiodAccess()(a.sys) -} - -func (a *DigitalPinsAdaptor) setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin string) { - system.WithSpiGpioAccess(a, sclkPin, nssPin, mosiPin, misoPin)(a.sys) -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinsActiveLow(id string, otherIDs ...string) { - ids := []string{id} - ids = append(ids, otherIDs...) - - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - for _, i := range ids { - a.pinOptions[i] = append(a.pinOptions[i], system.WithPinActiveLow()) - } -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinsPullDown(id string, otherIDs ...string) { - ids := []string{id} - ids = append(ids, otherIDs...) - - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - for _, i := range ids { - a.pinOptions[i] = append(a.pinOptions[i], system.WithPinPullDown()) - } -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinsPullUp(id string, otherIDs ...string) { - ids := []string{id} - ids = append(ids, otherIDs...) - - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - for _, i := range ids { - a.pinOptions[i] = append(a.pinOptions[i], system.WithPinPullUp()) - } -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinsOpenDrain(id string, otherIDs ...string) { - ids := []string{id} - ids = append(ids, otherIDs...) - - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - for _, i := range ids { - a.pinOptions[i] = append(a.pinOptions[i], system.WithPinOpenDrain()) - } -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinsOpenSource(id string, otherIDs ...string) { - ids := []string{id} - ids = append(ids, otherIDs...) - - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - for _, i := range ids { - a.pinOptions[i] = append(a.pinOptions[i], system.WithPinOpenSource()) - } -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinDebounce(id string, period time.Duration) { - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - a.pinOptions[id] = append(a.pinOptions[id], system.WithPinDebounce(period)) -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnFallingEdge(id string, handler func(int, time.Duration, string, - uint32, uint32), -) { - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnFallingEdge(handler)) -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnRisingEdge(id string, handler func(int, time.Duration, string, - uint32, uint32), -) { - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnRisingEdge(handler)) -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnBothEdges(id string, handler func(int, time.Duration, string, - uint32, uint32), -) { - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnBothEdges(handler)) -} - -func (a *DigitalPinsAdaptor) prepareDigitalPinPollForEdgeDetection( - id string, - pollInterval time.Duration, - pollQuitChan chan struct{}, -) { - if a.pinOptions == nil { - a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) - } - - a.pinOptions[id] = append(a.pinOptions[id], system.WithPinPollForEdgeDetection(pollInterval, pollQuitChan)) -} - func (a *DigitalPinsAdaptor) digitalPin( id string, opts ...func(gobot.DigitalPinOptioner) bool, diff --git a/platforms/adaptors/digitalpinsadaptor_test.go b/platforms/adaptors/digitalpinsadaptor_test.go index ae0785df8..55e16393b 100644 --- a/platforms/adaptors/digitalpinsadaptor_test.go +++ b/platforms/adaptors/digitalpinsadaptor_test.go @@ -7,7 +7,6 @@ import ( "strconv" "sync" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -43,18 +42,6 @@ func testDigitalPinTranslator(pin string) (string, int, error) { return "", line, err } -func TestDigitalPinsWithGpiosActiveLow(t *testing.T) { - // This is a general test, that options are applied in constructor. Further tests for options - // can also be done by call of "WithOption(val)(d)". - // arrange - translate := func(pin string) (chip string, line int, err error) { return } - sys := system.NewAccesser() - // act - a := NewDigitalPinsAdaptor(sys, translate, WithGpiosActiveLow("1", "12", "33")) - // assert - assert.Len(t, a.pinOptions, 3) -} - func TestDigitalPinsConnect(t *testing.T) { translate := func(pin string) (chip string, line int, err error) { return } sys := system.NewAccesser() @@ -170,96 +157,6 @@ func TestDigitalRead(t *testing.T) { require.ErrorContains(t, err, "write error") } -func TestDigitalReadWithGpiosActiveLow(t *testing.T) { - mockedPaths := []string{ - "/sys/class/gpio/export", - "/sys/class/gpio/unexport", - "/sys/class/gpio/gpio25/value", - "/sys/class/gpio/gpio25/direction", - "/sys/class/gpio/gpio25/active_low", - "/sys/class/gpio/gpio26/value", - "/sys/class/gpio/gpio26/direction", - } - a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) - fs.Files["/sys/class/gpio/gpio25/value"].Contents = "1" - fs.Files["/sys/class/gpio/gpio25/active_low"].Contents = "5" - fs.Files["/sys/class/gpio/gpio26/value"].Contents = "0" - WithGpiosActiveLow("14")(a) - // creates a new pin without inverted logic - if _, err := a.DigitalRead("15"); err != nil { - panic(err) - } - fs.Add("/sys/class/gpio/gpio26/active_low") - fs.Files["/sys/class/gpio/gpio26/active_low"].Contents = "6" - WithGpiosActiveLow("15")(a) - // act - got1, err1 := a.DigitalRead("14") // for a new pin - got2, err2 := a.DigitalRead("15") // for an existing pin (calls ApplyOptions()) - // assert - require.NoError(t, err1) - require.NoError(t, err2) - assert.Equal(t, 1, got1) // there is no mechanism to negate mocked values - assert.Equal(t, 0, got2) - assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio25/active_low"].Contents) - assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio26/active_low"].Contents) -} - -func TestDigitalWrite(t *testing.T) { - // arrange - mockedPaths := []string{ - "/sys/class/gpio/export", - "/sys/class/gpio/unexport", - "/sys/class/gpio/gpio18/value", - "/sys/class/gpio/gpio18/direction", - } - a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) - - // assert write correct value without error and just ignore unsupported options - WithGpiosPullUp("7")(a) - WithGpiosOpenDrain("7")(a) - WithGpioEventOnFallingEdge("7", gpioEventHandler)(a) - WithGpioPollForEdgeDetection("7", 0, nil)(a) - err := a.DigitalWrite("7", 1) - require.NoError(t, err) - assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio18/value"].Contents) - - // assert second write to same pin without error and just ignore unsupported options - WithGpiosPullDown("7")(a) - WithGpiosOpenSource("7")(a) - WithGpioDebounce("7", 2*time.Second)(a) - WithGpioEventOnRisingEdge("7", gpioEventHandler)(a) - err = a.DigitalWrite("7", 1) - require.NoError(t, err) - - // assert error on bad id - require.ErrorContains(t, a.DigitalWrite("notexist", 1), "not a valid pin") - - // assert error bubbling - fs.WithWriteError = true - err = a.DigitalWrite("7", 0) - require.ErrorContains(t, err, "write error") -} - -func TestDigitalWriteWithGpiosActiveLow(t *testing.T) { - // arrange - mockedPaths := []string{ - "/sys/class/gpio/export", - "/sys/class/gpio/unexport", - "/sys/class/gpio/gpio19/value", - "/sys/class/gpio/gpio19/direction", - "/sys/class/gpio/gpio19/active_low", - } - a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) - fs.Files["/sys/class/gpio/gpio19/active_low"].Contents = "5" - WithGpiosActiveLow("8")(a) - // act - err := a.DigitalWrite("8", 2) - // assert - require.NoError(t, err) - assert.Equal(t, "2", fs.Files["/sys/class/gpio/gpio19/value"].Contents) - assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio19/active_low"].Contents) -} - func TestDigitalPinConcurrency(t *testing.T) { oldProcs := runtime.GOMAXPROCS(0) runtime.GOMAXPROCS(8) @@ -286,8 +183,3 @@ func TestDigitalPinConcurrency(t *testing.T) { wg.Wait() } } - -func gpioEventHandler(o int, t time.Duration, et string, sn uint32, lsn uint32) { - // the handler should never execute, because used in outputs and not supported by sysfs - panic(fmt.Sprintf("event handler was called (%d, %d) unexpected for line %d with '%s' at %s!", sn, lsn, o, t, et)) -} diff --git a/platforms/adaptors/digitalpinsadaptoroptions.go b/platforms/adaptors/digitalpinsadaptoroptions.go new file mode 100644 index 000000000..6aefbf700 --- /dev/null +++ b/platforms/adaptors/digitalpinsadaptoroptions.go @@ -0,0 +1,157 @@ +package adaptors + +import ( + "time" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/system" +) + +// DigitalPinsOptioner is the interface for digital adaptors options. This provides the possibility for change the +// platform behavior by the user when creating the platform, e.g. by "NewAdaptor()". +// TODO: change to applier-architecture, see options of pwmpinsadaptor.go +type DigitalPinsOptioner interface { + setDigitalPinInitializer(initializer digitalPinInitializer) + setDigitalPinsForSystemGpiod() + setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin string) + prepareDigitalPinsActiveLow(pin string, otherPins ...string) + prepareDigitalPinsPullDown(pin string, otherPins ...string) + prepareDigitalPinsPullUp(pin string, otherPins ...string) + prepareDigitalPinsOpenDrain(pin string, otherPins ...string) + prepareDigitalPinsOpenSource(pin string, otherPins ...string) + prepareDigitalPinDebounce(pin string, period time.Duration) + prepareDigitalPinEventOnFallingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, + detectedEdge string, seqno uint32, lseqno uint32)) + prepareDigitalPinEventOnRisingEdge(pin string, handler func(lineOffset int, timestamp time.Duration, + detectedEdge string, seqno uint32, lseqno uint32)) + prepareDigitalPinEventOnBothEdges(pin string, handler func(lineOffset int, timestamp time.Duration, + detectedEdge string, seqno uint32, lseqno uint32)) + prepareDigitalPinPollForEdgeDetection(pin string, pollInterval time.Duration, pollQuitChan chan struct{}) +} + +func (a *DigitalPinsAdaptor) setDigitalPinInitializer(pinInit digitalPinInitializer) { + a.initialize = pinInit +} + +func (a *DigitalPinsAdaptor) setDigitalPinsForSystemGpiod() { + system.WithDigitalPinGpiodAccess()(a.sys) +} + +func (a *DigitalPinsAdaptor) setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin, misoPin string) { + system.WithSpiGpioAccess(a, sclkPin, nssPin, mosiPin, misoPin)(a.sys) +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinsActiveLow(id string, otherIDs ...string) { + ids := []string{id} + ids = append(ids, otherIDs...) + + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + for _, i := range ids { + a.pinOptions[i] = append(a.pinOptions[i], system.WithPinActiveLow()) + } +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinsPullDown(id string, otherIDs ...string) { + ids := []string{id} + ids = append(ids, otherIDs...) + + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + for _, i := range ids { + a.pinOptions[i] = append(a.pinOptions[i], system.WithPinPullDown()) + } +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinsPullUp(id string, otherIDs ...string) { + ids := []string{id} + ids = append(ids, otherIDs...) + + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + for _, i := range ids { + a.pinOptions[i] = append(a.pinOptions[i], system.WithPinPullUp()) + } +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinsOpenDrain(id string, otherIDs ...string) { + ids := []string{id} + ids = append(ids, otherIDs...) + + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + for _, i := range ids { + a.pinOptions[i] = append(a.pinOptions[i], system.WithPinOpenDrain()) + } +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinsOpenSource(id string, otherIDs ...string) { + ids := []string{id} + ids = append(ids, otherIDs...) + + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + for _, i := range ids { + a.pinOptions[i] = append(a.pinOptions[i], system.WithPinOpenSource()) + } +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinDebounce(id string, period time.Duration) { + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + a.pinOptions[id] = append(a.pinOptions[id], system.WithPinDebounce(period)) +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnFallingEdge(id string, handler func(int, time.Duration, string, + uint32, uint32), +) { + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnFallingEdge(handler)) +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnRisingEdge(id string, handler func(int, time.Duration, string, + uint32, uint32), +) { + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnRisingEdge(handler)) +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinEventOnBothEdges(id string, handler func(int, time.Duration, string, + uint32, uint32), +) { + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + a.pinOptions[id] = append(a.pinOptions[id], system.WithPinEventOnBothEdges(handler)) +} + +func (a *DigitalPinsAdaptor) prepareDigitalPinPollForEdgeDetection( + id string, + pollInterval time.Duration, + pollQuitChan chan struct{}, +) { + if a.pinOptions == nil { + a.pinOptions = make(map[string][]func(gobot.DigitalPinOptioner) bool) + } + + a.pinOptions[id] = append(a.pinOptions[id], system.WithPinPollForEdgeDetection(pollInterval, pollQuitChan)) +} diff --git a/platforms/adaptors/digitalpinsadaptoroptions_test.go b/platforms/adaptors/digitalpinsadaptoroptions_test.go new file mode 100644 index 000000000..a458de37e --- /dev/null +++ b/platforms/adaptors/digitalpinsadaptoroptions_test.go @@ -0,0 +1,120 @@ +//nolint:nonamedreturns // ok for tests +package adaptors + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gobot.io/x/gobot/v2/system" +) + +func TestDigitalPinsWithGpiosActiveLow(t *testing.T) { + // This is a general test, that options are applied in constructor. Further tests for options + // can also be done by call of "WithOption(val)(d)". + // arrange + translate := func(pin string) (chip string, line int, err error) { return } + sys := system.NewAccesser() + // act + a := NewDigitalPinsAdaptor(sys, translate, WithGpiosActiveLow("1", "12", "33")) + // assert + assert.Len(t, a.pinOptions, 3) +} + +func TestDigitalReadWithGpiosActiveLow(t *testing.T) { + mockedPaths := []string{ + "/sys/class/gpio/export", + "/sys/class/gpio/unexport", + "/sys/class/gpio/gpio25/value", + "/sys/class/gpio/gpio25/direction", + "/sys/class/gpio/gpio25/active_low", + "/sys/class/gpio/gpio26/value", + "/sys/class/gpio/gpio26/direction", + } + a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) + fs.Files["/sys/class/gpio/gpio25/value"].Contents = "1" + fs.Files["/sys/class/gpio/gpio25/active_low"].Contents = "5" + fs.Files["/sys/class/gpio/gpio26/value"].Contents = "0" + WithGpiosActiveLow("14")(a) + // creates a new pin without inverted logic + if _, err := a.DigitalRead("15"); err != nil { + panic(err) + } + fs.Add("/sys/class/gpio/gpio26/active_low") + fs.Files["/sys/class/gpio/gpio26/active_low"].Contents = "6" + WithGpiosActiveLow("15")(a) + // act + got1, err1 := a.DigitalRead("14") // for a new pin + got2, err2 := a.DigitalRead("15") // for an existing pin (calls ApplyOptions()) + // assert + require.NoError(t, err1) + require.NoError(t, err2) + assert.Equal(t, 1, got1) // there is no mechanism to negate mocked values + assert.Equal(t, 0, got2) + assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio25/active_low"].Contents) + assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio26/active_low"].Contents) +} + +func TestDigitalWriteWithOptions(t *testing.T) { + // arrange + mockedPaths := []string{ + "/sys/class/gpio/export", + "/sys/class/gpio/unexport", + "/sys/class/gpio/gpio18/value", + "/sys/class/gpio/gpio18/direction", + } + a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) + + // assert write correct value without error and just ignore unsupported options + WithGpiosPullUp("7")(a) + WithGpiosOpenDrain("7")(a) + WithGpioEventOnFallingEdge("7", gpioEventHandler)(a) + WithGpioPollForEdgeDetection("7", 0, nil)(a) + err := a.DigitalWrite("7", 1) + require.NoError(t, err) + assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio18/value"].Contents) + + // assert second write to same pin without error and just ignore unsupported options + WithGpiosPullDown("7")(a) + WithGpiosOpenSource("7")(a) + WithGpioDebounce("7", 2*time.Second)(a) + WithGpioEventOnRisingEdge("7", gpioEventHandler)(a) + err = a.DigitalWrite("7", 1) + require.NoError(t, err) + + // assert error on bad id + require.ErrorContains(t, a.DigitalWrite("notexist", 1), "not a valid pin") + + // assert error bubbling + fs.WithWriteError = true + err = a.DigitalWrite("7", 0) + require.ErrorContains(t, err, "write error") +} + +func TestDigitalWriteWithGpiosActiveLow(t *testing.T) { + // arrange + mockedPaths := []string{ + "/sys/class/gpio/export", + "/sys/class/gpio/unexport", + "/sys/class/gpio/gpio19/value", + "/sys/class/gpio/gpio19/direction", + "/sys/class/gpio/gpio19/active_low", + } + a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) + fs.Files["/sys/class/gpio/gpio19/active_low"].Contents = "5" + WithGpiosActiveLow("8")(a) + // act + err := a.DigitalWrite("8", 2) + // assert + require.NoError(t, err) + assert.Equal(t, "2", fs.Files["/sys/class/gpio/gpio19/value"].Contents) + assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio19/active_low"].Contents) +} + +func gpioEventHandler(o int, t time.Duration, et string, sn uint32, lsn uint32) { + // the handler should never execute, because used in outputs and not supported by sysfs + panic(fmt.Sprintf("event handler was called (%d, %d) unexpected for line %d with '%s' at %s!", sn, lsn, o, t, et)) +} diff --git a/platforms/adaptors/options.go b/platforms/adaptors/options.go deleted file mode 100644 index 11b48afa4..000000000 --- a/platforms/adaptors/options.go +++ /dev/null @@ -1,7 +0,0 @@ -package adaptors - -// Optioner is the interface for adaptors options. This provides the possibility for change the platform behavior -// by the user when creating the platform, e.g. by "NewAdaptor()". -type Optioner interface { - digitalPinsOptioner -} diff --git a/platforms/adaptors/pwmpinsadaptor.go b/platforms/adaptors/pwmpinsadaptor.go index 962c7dd4b..baa8bc0dd 100644 --- a/platforms/adaptors/pwmpinsadaptor.go +++ b/platforms/adaptors/pwmpinsadaptor.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "sync" + "time" multierror "github.com/hashicorp/go-multierror" @@ -17,69 +18,112 @@ import ( // 100ns = 10MHz, 10ns = 100MHz, 1ns = 1GHz const pwmPeriodDefault = 10000000 // 10 ms = 100 Hz +// 50Hz = 0.02 sec = 20 ms +const fiftyHzNanos = 20 * 1000 * 1000 + type ( pwmPinTranslator func(pin string) (path string, channel int, err error) - pwmPinInitializer func(gobot.PWMPinner) error + pwmPinInitializer func(id string, pin gobot.PWMPinner) error ) -type pwmPinsOption interface { - setInitializer(initializer pwmPinInitializer) - setDefaultPeriod(period uint32) - setPolarityInvertedIdentifier(id string) +type pwmPinServoScale struct { + minDegree, maxDegree float64 + minDuty, maxDuty time.Duration } -// PWMPinsAdaptor is a adaptor for PWM pins, normally used for composition in platforms. -type PWMPinsAdaptor struct { - sys *system.Accesser - translate pwmPinTranslator +// pwmPinConfiguration contains all changeable attributes of the adaptor. +type pwmPinsConfiguration struct { initialize pwmPinInitializer periodDefault uint32 polarityNormalIdentifier string polarityInvertedIdentifier string adjustDutyOnSetPeriod bool - pins map[string]gobot.PWMPinner - mutex sync.Mutex + pinsDefaultPeriod map[string]uint32 // the key is the pin id + pinsServoScale map[string]pwmPinServoScale // the key is the pin id +} + +// PWMPinsAdaptor is a adaptor for PWM pins, normally used for composition in platforms. +type PWMPinsAdaptor struct { + sys *system.Accesser + translate pwmPinTranslator + pwmPinsCfg *pwmPinsConfiguration + pins map[string]gobot.PWMPinner + mutex sync.Mutex } // NewPWMPinsAdaptor provides the access to PWM pins of the board. It uses sysfs system drivers. 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. If for some reasons the default initializer is not suitable, it can be given by the option // "WithPWMPinInitializer()". -func NewPWMPinsAdaptor(sys *system.Accesser, t pwmPinTranslator, options ...func(pwmPinsOption)) *PWMPinsAdaptor { +// +// Further options: +// +// "WithPWMDefaultPeriod" +// "WithPWMPolarityInvertedIdentifier" +// "WithPWMNoDutyCycleAdjustment" +// "WithPWMDefaultPeriodForPin" +// "WithPWMServoDutyCycleRangeForPin" +// "WithPWMServoAngleRangeForPin" +func NewPWMPinsAdaptor(sys *system.Accesser, t pwmPinTranslator, opts ...PwmPinsOptionApplier) *PWMPinsAdaptor { a := &PWMPinsAdaptor{ - sys: sys, - translate: t, - periodDefault: pwmPeriodDefault, - polarityNormalIdentifier: "normal", - polarityInvertedIdentifier: "inverted", - adjustDutyOnSetPeriod: true, + sys: sys, + translate: t, + pwmPinsCfg: &pwmPinsConfiguration{ + periodDefault: pwmPeriodDefault, + pinsDefaultPeriod: make(map[string]uint32), + pinsServoScale: make(map[string]pwmPinServoScale), + polarityNormalIdentifier: "normal", + polarityInvertedIdentifier: "inverted", + adjustDutyOnSetPeriod: true, + }, } - a.initialize = a.getDefaultInitializer() - for _, option := range options { - option(a) + a.pwmPinsCfg.initialize = a.getDefaultInitializer() + + for _, o := range opts { + o.apply(a.pwmPinsCfg) } + return a } // WithPWMPinInitializer substitute the default initializer. -func WithPWMPinInitializer(pc pwmPinInitializer) func(pwmPinsOption) { - return func(a pwmPinsOption) { - a.setInitializer(pc) - } +func WithPWMPinInitializer(pc pwmPinInitializer) pwmPinsInitializeOption { + return pwmPinsInitializeOption(pc) } -// WithPWMPinDefaultPeriod substitute the default period of 10 ms (100 Hz) for all created pins. -func WithPWMPinDefaultPeriod(periodNanoSec uint32) func(pwmPinsOption) { - return func(a pwmPinsOption) { - a.setDefaultPeriod(periodNanoSec) - } +// WithPWMDefaultPeriod substitute the default period of 10 ms (100 Hz) for all created pins. +func WithPWMDefaultPeriod(periodNanoSec uint32) pwmPinsPeriodDefaultOption { + return pwmPinsPeriodDefaultOption(periodNanoSec) } -// WithPolarityInvertedIdentifier use the given identifier, which will replace the default "inverted". -func WithPolarityInvertedIdentifier(identifier string) func(pwmPinsOption) { - return func(a pwmPinsOption) { - a.setPolarityInvertedIdentifier(identifier) - } +// WithPWMPolarityInvertedIdentifier use the given identifier, which will replace the default "inverted". +func WithPWMPolarityInvertedIdentifier(identifier string) pwmPinsPolarityInvertedIdentifierOption { + return pwmPinsPolarityInvertedIdentifierOption(identifier) +} + +// WithPWMNoDutyCycleAdjustment switch off the automatic adjustment of duty cycle on setting the period. +func WithPWMNoDutyCycleAdjustment() pwmPinsAdjustDutyOnSetPeriodOption { + return pwmPinsAdjustDutyOnSetPeriodOption(false) +} + +// WithPWMDefaultPeriodForPin substitute the default period of 10 ms (100 Hz) for the given pin. +// This option also overrides a default period given by the WithPWMDefaultPeriod() option. +// This is often needed for servo applications, where the default period is 50Hz (20.000.000 ns). +func WithPWMDefaultPeriodForPin(pin string, periodNanoSec uint32) pwmPinsDefaultPeriodForPinOption { + o := pwmPinsDefaultPeriodForPinOption{id: pin, period: periodNanoSec} + return o +} + +// WithPWMServoDutyCycleRangeForPin set new values for range of duty cycle for servo calls, which replaces the default +// 0.5-2.5 ms range. The given duration values will be internally converted to nanoseconds. +func WithPWMServoDutyCycleRangeForPin(pin string, min, max time.Duration) pwmPinsServoDutyScaleForPinOption { + return pwmPinsServoDutyScaleForPinOption{id: pin, min: min, max: max} +} + +// WithPWMServoAngleRangeForPin set new values for range of angle for servo calls, which replaces +// the default 0.0-180.0° range. +func WithPWMServoAngleRangeForPin(pin string, min, max float64) pwmPinsServoAngleScaleForPinOption { + return pwmPinsServoAngleScaleForPinOption{id: pin, minDegree: min, maxDegree: max} } // Connect prepare new connection to PWM pins. @@ -142,40 +186,19 @@ func (a *PWMPinsAdaptor) ServoWrite(id string, angle byte) error { return err } - // 50Hz = 0.02 sec = 20 ms - const fiftyHzNanos = 20 * 1000 * 1000 - if period != fiftyHzNanos { log.Printf("WARNING: the PWM acts with a period of %d, but should use %d (50Hz) for servos\n", period, fiftyHzNanos) } - // TODO: implement an option to give another min-max range - // TODO: allow usage of adaptors.WithPWMPinDefaultPeriod() from main.go - // - // for some older servos, this can happen - // 0.5 ms => -90 (1/40 part of period at 50Hz) - // 1.5 ms => 0 - // 2.0 ms => 90 (1/10 part of period at 50Hz) - - // some servos have profiles below/above the 0-180° value - // SG90: 1/90: after position a small smooth movement is done - // SG90: >1/100: endless movement clock wise - // SG90: <1/8: endless movement counter clock wise - - // usually for the most servos (at 50Hz) for 90° - // 1.0 ms => 0 (1/20 part of period at 50Hz) - // 1.5 ms => 45 - // 2.0 ms => 90 (1/10 part of period at 50Hz) - - // usually for the most servos (at 50Hz) for 180° (SG90, AD002) - // 0.5 ms => 0 (1/40 part of period at 50Hz) - // 1.5 ms => 90 - // 2.5 ms => 180 (1/8 part of period at 50Hz) - - minDuty := float64(period) / 40 - maxDuty := float64(period) / 8 - duty := uint32(gobot.ToScale(gobot.FromScale(float64(angle), 0, 180), minDuty, maxDuty)) - return pin.SetDutyCycle(duty) + scale, ok := a.pwmPinsCfg.pinsServoScale[id] + if !ok { + return fmt.Errorf("no scaler found for servo pin '%s'", id) + } + + duty := gobot.ToScale(gobot.FromScale(float64(angle), + scale.minDegree, scale.maxDegree), + float64(scale.minDuty), float64(scale.maxDuty)) + return pin.SetDutyCycle(uint32(duty)) } // SetPeriod adjusts the period of the specified PWM pin immediately. @@ -188,7 +211,7 @@ func (a *PWMPinsAdaptor) SetPeriod(id string, period uint32) error { if err != nil { return err } - return setPeriod(pin, period, a.adjustDutyOnSetPeriod) + return setPeriod(pin, period, a.pwmPinsCfg.adjustDutyOnSetPeriod) } // PWMPin initializes the pin for PWM and returns matched pwmPin for specified pin number. @@ -200,20 +223,8 @@ func (a *PWMPinsAdaptor) PWMPin(id string) (gobot.PWMPinner, error) { return a.pwmPin(id) } -func (a *PWMPinsAdaptor) setInitializer(pinInit pwmPinInitializer) { - a.initialize = pinInit -} - -func (a *PWMPinsAdaptor) setDefaultPeriod(periodNanoSec uint32) { - a.periodDefault = periodNanoSec -} - -func (a *PWMPinsAdaptor) setPolarityInvertedIdentifier(identifier string) { - a.polarityInvertedIdentifier = identifier -} - -func (a *PWMPinsAdaptor) getDefaultInitializer() func(gobot.PWMPinner) error { - return func(pin gobot.PWMPinner) error { +func (a *PWMPinsAdaptor) getDefaultInitializer() func(string, gobot.PWMPinner) error { + return func(id string, pin gobot.PWMPinner) error { if err := pin.Export(); err != nil { return err } @@ -223,9 +234,38 @@ func (a *PWMPinsAdaptor) getDefaultInitializer() func(gobot.PWMPinner) error { return err } } - if err := setPeriod(pin, a.periodDefault, a.adjustDutyOnSetPeriod); err != nil { + + // looking for a pin specific period + defaultPeriod, ok := a.pwmPinsCfg.pinsDefaultPeriod[id] + if !ok { + defaultPeriod = a.pwmPinsCfg.periodDefault + } + + if err := setPeriod(pin, defaultPeriod, a.pwmPinsCfg.adjustDutyOnSetPeriod); err != nil { return err } + + // ensure servo scaler is present + // + // usually for the most servos (at 50Hz) for 180° (SG90, AD002) + // 0.5 ms => 0 (1/40 part of period at 50Hz) + // 1.5 ms => 90 + // 2.5 ms => 180 (1/8 part of period at 50Hz) + scale, ok := a.pwmPinsCfg.pinsServoScale[id] + if !ok { + scale = pwmPinServoScale{ + minDegree: 0, + maxDegree: 180, + } + } + if scale.minDuty == 0 { + scale.minDuty = time.Duration(defaultPeriod / 40) + } + if scale.maxDuty == 0 { + scale.maxDuty = time.Duration(defaultPeriod / 8) + } + a.pwmPinsCfg.pinsServoScale[id] = scale + // period needs to be set >1 before all next statements if err := pin.SetPolarity(true); err != nil { return err @@ -246,8 +286,8 @@ func (a *PWMPinsAdaptor) pwmPin(id string) (gobot.PWMPinner, error) { if err != nil { return nil, err } - pin = a.sys.NewPWMPin(path, channel, a.polarityNormalIdentifier, a.polarityInvertedIdentifier) - if err := a.initialize(pin); err != nil { + pin = a.sys.NewPWMPin(path, channel, a.pwmPinsCfg.polarityNormalIdentifier, a.pwmPinsCfg.polarityInvertedIdentifier) + if err := a.pwmPinsCfg.initialize(id, pin); err != nil { return nil, err } a.pins[id] = pin diff --git a/platforms/adaptors/pwmpinsadaptor_test.go b/platforms/adaptors/pwmpinsadaptor_test.go index ad0397be5..4635c90fb 100644 --- a/platforms/adaptors/pwmpinsadaptor_test.go +++ b/platforms/adaptors/pwmpinsadaptor_test.go @@ -50,7 +50,7 @@ func initTestPWMPinsAdaptorWithMockedFilesystem(mockPaths []string) (*PWMPinsAda fs.Files[pwmEnablePath].Contents = "0" fs.Files[pwmPeriodPath].Contents = "0" fs.Files[pwmDutyCyclePath].Contents = "0" - fs.Files[pwmPolarityPath].Contents = a.polarityInvertedIdentifier + fs.Files[pwmPolarityPath].Contents = a.pwmPinsCfg.polarityInvertedIdentifier if err := a.Connect(); err != nil { panic(err) } @@ -72,44 +72,10 @@ func TestNewPWMPinsAdaptor(t *testing.T) { // act a := NewPWMPinsAdaptor(system.NewAccesser(), translate) // assert - assert.Equal(t, uint32(pwmPeriodDefault), a.periodDefault) - assert.Equal(t, "normal", a.polarityNormalIdentifier) - assert.Equal(t, "inverted", a.polarityInvertedIdentifier) - assert.True(t, a.adjustDutyOnSetPeriod) -} - -func TestWithPWMPinInitializer(t *testing.T) { - // This is a general test, that options are applied by using the WithPWMPinInitializer() option. - // All other configuration options can also be tested by With..(val)(a). - // arrange - wantErr := fmt.Errorf("new_initializer") - newInitializer := func(gobot.PWMPinner) error { return wantErr } - // act - a := NewPWMPinsAdaptor(system.NewAccesser(), func(pin string) (c string, l int, e error) { return }, - WithPWMPinInitializer(newInitializer)) - // assert - err := a.initialize(nil) - assert.Equal(t, wantErr, err) -} - -func TestWithPWMPinDefaultPeriod(t *testing.T) { - // arrange - const newPeriod = uint32(10) - a := NewPWMPinsAdaptor(system.NewAccesser(), func(string) (c string, l int, e error) { return }) - // act - WithPWMPinDefaultPeriod(newPeriod)(a) - // assert - assert.Equal(t, newPeriod, a.periodDefault) -} - -func TestWithPolarityInvertedIdentifier(t *testing.T) { - // arrange - const newPolarityIdent = "pwm_invers" - a := NewPWMPinsAdaptor(system.NewAccesser(), func(pin string) (c string, l int, e error) { return }) - // act - WithPolarityInvertedIdentifier(newPolarityIdent)(a) - // assert - assert.Equal(t, newPolarityIdent, a.polarityInvertedIdentifier) + assert.Equal(t, uint32(pwmPeriodDefault), a.pwmPinsCfg.periodDefault) + assert.Equal(t, "normal", a.pwmPinsCfg.polarityNormalIdentifier) + assert.Equal(t, "inverted", a.pwmPinsCfg.polarityInvertedIdentifier) + assert.True(t, a.pwmPinsCfg.adjustDutyOnSetPeriod) } func TestPWMPinsConnect(t *testing.T) { @@ -182,7 +148,8 @@ func TestPwmWrite(t *testing.T) { assert.Equal(t, "44", fs.Files[pwmExportPath].Contents) assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) - assert.Equal(t, fmt.Sprintf("%d", a.periodDefault), fs.Files[pwmPeriodPath].Contents) //nolint:perfsprint // ok here + //nolint:perfsprint // ok here + assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) @@ -203,12 +170,13 @@ func TestServoWrite(t *testing.T) { a, fs := initTestPWMPinsAdaptorWithMockedFilesystem(pwmMockPaths) err := a.ServoWrite("33", 0) + require.NoError(t, err) assert.Equal(t, "44", fs.Files[pwmExportPath].Contents) assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) - assert.Equal(t, fmt.Sprintf("%d", a.periodDefault), fs.Files[pwmPeriodPath].Contents) //nolint:perfsprint // ok here + //nolint:perfsprint // ok here + assert.Equal(t, fmt.Sprintf("%d", a.pwmPinsCfg.periodDefault), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "250000", fs.Files[pwmDutyCyclePath].Contents) assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) - require.NoError(t, err) err = a.ServoWrite("33", 180) require.NoError(t, err) @@ -225,6 +193,11 @@ func TestServoWrite(t *testing.T) { fs.WithReadError = true err = a.ServoWrite("33", 100) require.ErrorContains(t, err, "read error") + fs.WithReadError = false + + delete(a.pwmPinsCfg.pinsServoScale, "33") + err = a.ServoWrite("33", 42) + require.EqualError(t, err, "no scaler found for servo pin '33'") } func TestSetPeriod(t *testing.T) { diff --git a/platforms/adaptors/pwmpinsadaptoroptions.go b/platforms/adaptors/pwmpinsadaptoroptions.go new file mode 100644 index 000000000..c6b336d92 --- /dev/null +++ b/platforms/adaptors/pwmpinsadaptoroptions.go @@ -0,0 +1,118 @@ +package adaptors + +import "time" + +// pwmPinOptionApplier needs to be implemented by each configurable option type +type PwmPinsOptionApplier interface { + apply(cfg *pwmPinsConfiguration) +} + +// pwmPinInitializeOption is the type for applying another than the default initializer. +type pwmPinsInitializeOption pwmPinInitializer + +// pwmPinPeriodDefaultOption is the type for applying another than the default period of 10 ms (100 Hz) for all +// created pins. +type pwmPinsPeriodDefaultOption uint32 + +// pwmPinPolarityInvertedIdentifierOption is the type for applying another identifier, which will replace the default +// "inverted". +type pwmPinsPolarityInvertedIdentifierOption string + +// pwmPinsAdjustDutyOnSetPeriodOption is the type for applying the automatic adjustment of duty cycle on setting +// the period to on/off. +type pwmPinsAdjustDutyOnSetPeriodOption bool + +// pwmPinsDefaultPeriodForPinOption is the type for applying another than the default period of 10 ms (100 Hz) only for +// the given pin id. +type pwmPinsDefaultPeriodForPinOption struct { + id string + period uint32 +} + +// pwmPinsServoDutyScaleForPinOption is the type for applying another than the default 0.5-2.5 ms range of duty cycle +// for servo calls on the specified pin id. +type pwmPinsServoDutyScaleForPinOption struct { + id string + min time.Duration + max time.Duration +} + +// pwmPinsServoAngleScaleForPinOption is the type for applying another than the default 0.0-180.0° range of angle for +// servo calls on the specified pin id. +type pwmPinsServoAngleScaleForPinOption struct { + id string + minDegree float64 + maxDegree float64 +} + +func (o pwmPinsInitializeOption) String() string { + return "pin initializer option for PWM's" +} + +func (o pwmPinsPeriodDefaultOption) String() string { + return "default period option for PWM's" +} + +func (o pwmPinsPolarityInvertedIdentifierOption) String() string { + return "inverted identifier option for PWM's" +} + +func (o pwmPinsAdjustDutyOnSetPeriodOption) String() string { + return "adjust duty cycle on set period option for PWM's" +} + +func (o pwmPinsDefaultPeriodForPinOption) String() string { + return "default period for the pin option for PWM's" +} + +func (o pwmPinsServoDutyScaleForPinOption) String() string { + return "duty cycle min-max range for a servo pin option for PWM's" +} + +func (o pwmPinsServoAngleScaleForPinOption) String() string { + return "angle min-max range for a servo pin option for PWM's" +} + +func (o pwmPinsInitializeOption) apply(cfg *pwmPinsConfiguration) { + cfg.initialize = pwmPinInitializer(o) +} + +func (o pwmPinsPeriodDefaultOption) apply(cfg *pwmPinsConfiguration) { + cfg.periodDefault = uint32(o) +} + +func (o pwmPinsPolarityInvertedIdentifierOption) apply(cfg *pwmPinsConfiguration) { + cfg.polarityInvertedIdentifier = string(o) +} + +func (o pwmPinsAdjustDutyOnSetPeriodOption) apply(cfg *pwmPinsConfiguration) { + cfg.adjustDutyOnSetPeriod = bool(o) +} + +func (o pwmPinsDefaultPeriodForPinOption) apply(cfg *pwmPinsConfiguration) { + cfg.pinsDefaultPeriod[o.id] = o.period +} + +func (o pwmPinsServoDutyScaleForPinOption) apply(cfg *pwmPinsConfiguration) { + scale, ok := cfg.pinsServoScale[o.id] + if !ok { + scale = pwmPinServoScale{minDegree: 0, maxDegree: 180} + } + + scale.minDuty = o.min + scale.maxDuty = o.max + + cfg.pinsServoScale[o.id] = scale +} + +func (o pwmPinsServoAngleScaleForPinOption) apply(cfg *pwmPinsConfiguration) { + scale, ok := cfg.pinsServoScale[o.id] + if !ok { + scale = pwmPinServoScale{} // default values for duty cycle will be set on initialize, if zero + } + + scale.minDegree = o.minDegree + scale.maxDegree = o.maxDegree + + cfg.pinsServoScale[o.id] = scale +} diff --git a/platforms/adaptors/pwmpinsadaptoroptions_test.go b/platforms/adaptors/pwmpinsadaptoroptions_test.go new file mode 100644 index 000000000..64341272d --- /dev/null +++ b/platforms/adaptors/pwmpinsadaptoroptions_test.go @@ -0,0 +1,148 @@ +//nolint:nonamedreturns // ok for tests +package adaptors + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "gobot.io/x/gobot/v2" + "gobot.io/x/gobot/v2/system" +) + +func TestWithPWMPinInitializer(t *testing.T) { + // This is a general test, that options are applied by using the WithPWMPinInitializer() option. + // All other configuration options can also be tested by With..(val).apply(cfg). + // arrange + wantErr := fmt.Errorf("new_initializer") + newInitializer := func(string, gobot.PWMPinner) error { return wantErr } + // act + a := NewPWMPinsAdaptor(system.NewAccesser(), func(pin string) (c string, l int, e error) { return }, + WithPWMPinInitializer(newInitializer)) + // assert + err := a.pwmPinsCfg.initialize("1", nil) + assert.Equal(t, wantErr, err) +} + +func TestWithPWMDefaultPeriod(t *testing.T) { + // arrange + const newPeriod = uint32(10) + cfg := &pwmPinsConfiguration{periodDefault: 123} + // act + WithPWMDefaultPeriod(newPeriod).apply(cfg) + // assert + assert.Equal(t, newPeriod, cfg.periodDefault) +} + +func TestWithPWMPolarityInvertedIdentifier(t *testing.T) { + // arrange + const newPolarityIdent = "pwm_invers" + cfg := &pwmPinsConfiguration{polarityInvertedIdentifier: "old_inverted"} + // act + WithPWMPolarityInvertedIdentifier(newPolarityIdent).apply(cfg) + // assert + assert.Equal(t, newPolarityIdent, cfg.polarityInvertedIdentifier) +} + +func TestWithPWMNoDutyCycleAdjustment(t *testing.T) { + // arrange + cfg := &pwmPinsConfiguration{adjustDutyOnSetPeriod: true} + // act + WithPWMNoDutyCycleAdjustment().apply(cfg) + // assert + assert.False(t, cfg.adjustDutyOnSetPeriod) +} + +func TestWithPWMDefaultPeriodForPin(t *testing.T) { + // arrange + const ( + pin = "pin4test" + newPeriod = 123456 + ) + cfg := &pwmPinsConfiguration{pinsDefaultPeriod: map[string]uint32{pin: 54321}} + // act + WithPWMDefaultPeriodForPin(pin, newPeriod).apply(cfg) + // assert + assert.Equal(t, uint32(newPeriod), cfg.pinsDefaultPeriod[pin]) +} + +func TestWithPWMServoDutyCycleRangeForPin(t *testing.T) { + const ( + pin = "pin4test" + newMin = 19 + newMax = 99 + ) + + tests := map[string]struct { + scaleMap map[string]pwmPinServoScale + wantScaleMap map[string]pwmPinServoScale + }{ + "empty_scale_map": { + scaleMap: make(map[string]pwmPinServoScale), + wantScaleMap: map[string]pwmPinServoScale{ + pin: {minDuty: newMin, maxDuty: newMax, minDegree: 0, maxDegree: 180}, + }, + }, + "scale_exists_for_set_pin": { + scaleMap: map[string]pwmPinServoScale{ + "other": {minDuty: 123, maxDuty: 234, minDegree: 11, maxDegree: 22}, + pin: {minDuty: newMin - 2, maxDuty: newMax + 2, minDegree: 1, maxDegree: 2}, + }, + wantScaleMap: map[string]pwmPinServoScale{ + "other": {minDuty: 123, maxDuty: 234, minDegree: 11, maxDegree: 22}, + pin: {minDuty: newMin, maxDuty: newMax, minDegree: 1, maxDegree: 2}, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + cfg := &pwmPinsConfiguration{pinsServoScale: tc.scaleMap} + // act + WithPWMServoDutyCycleRangeForPin(pin, newMin, newMax).apply(cfg) + // assert + assert.Equal(t, tc.wantScaleMap, cfg.pinsServoScale) + }) + } +} + +func TestWithPWMServoAngleRangeForPin(t *testing.T) { + const ( + pin = "pin4test" + newMin = 30 + newMax = 90 + ) + + tests := map[string]struct { + scaleMap map[string]pwmPinServoScale + wantScaleMap map[string]pwmPinServoScale + }{ + "empty_scale_map": { + scaleMap: make(map[string]pwmPinServoScale), + wantScaleMap: map[string]pwmPinServoScale{ + pin: {minDuty: 0.0, maxDuty: 0.0, minDegree: newMin, maxDegree: newMax}, + }, + }, + "scale_exists_for_set_pin": { + scaleMap: map[string]pwmPinServoScale{ + "other": {minDuty: 123, maxDuty: 234, minDegree: 11, maxDegree: 22}, + pin: {minDuty: 4, maxDuty: 5, minDegree: newMin - 2, maxDegree: newMax + 2}, + }, + wantScaleMap: map[string]pwmPinServoScale{ + "other": {minDuty: 123, maxDuty: 234, minDegree: 11, maxDegree: 22}, + pin: {minDuty: 4, maxDuty: 5, minDegree: newMin, maxDegree: newMax}, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + cfg := &pwmPinsConfiguration{pinsServoScale: tc.scaleMap} + // act + WithPWMServoAngleRangeForPin(pin, newMin, newMax).apply(cfg) + // assert + assert.Equal(t, tc.wantScaleMap, cfg.pinsServoScale) + }) + } +} diff --git a/platforms/beaglebone/beaglebone_adaptor.go b/platforms/beaglebone/beaglebone_adaptor.go index 87269eae4..3355497e4 100644 --- a/platforms/beaglebone/beaglebone_adaptor.go +++ b/platforms/beaglebone/beaglebone_adaptor.go @@ -43,7 +43,7 @@ const ( type Adaptor struct { name string sys *system.Accesser - mutex sync.Mutex + mutex *sync.Mutex *adaptors.AnalogPinsAdaptor *adaptors.DigitalPinsAdaptor *adaptors.PWMPinsAdaptor @@ -61,75 +61,91 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("BeagleboneBlack"), sys: sys, + mutex: &sync.Mutex{}, pinMap: bbbPinMap, pwmPinMap: bbbPwmPinMap, analogPinMap: bbbAnalogPinMap, usrLed: "/sys/class/leds/beaglebone:green:", } - c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin) - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateAndMuxDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin, - adaptors.WithPWMPinDefaultPeriod(pwmPeriodDefault)) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMDefaultPeriod(pwmPeriodDefault)} + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin) + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateAndMuxDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translateAndMuxPWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the Adaptor name -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the Adaptor name -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.AnalogPinsAdaptor.Connect(); err != nil { + if err := a.AnalogPinsAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize releases all i2c devices and exported analog, digital, pwm pins. -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.AnalogPinsAdaptor.Finalize(); e != nil { + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err @@ -137,12 +153,12 @@ func (c *Adaptor) Finalize() error { // DigitalWrite writes a digital value to specified pin. // valid usr pin values are usr0, usr1, usr2 and usr3 -func (c *Adaptor) DigitalWrite(id string, val byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) DigitalWrite(id string, val byte) error { + a.mutex.Lock() + defer a.mutex.Unlock() if strings.Contains(id, "usr") { - fi, e := c.sys.OpenFile(c.usrLed+id+"/brightness", os.O_WRONLY|os.O_APPEND, 0o666) + fi, e := a.sys.OpenFile(a.usrLed+id+"/brightness", os.O_WRONLY|os.O_APPEND, 0o666) defer fi.Close() //nolint:staticcheck // for historical reasons if e != nil { return e @@ -151,10 +167,10 @@ func (c *Adaptor) DigitalWrite(id string, val byte) error { return err } - return c.DigitalPinsAdaptor.DigitalWrite(id, val) + return a.DigitalPinsAdaptor.DigitalWrite(id, val) } -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x. // x is the chip number <255 if (busNr < 0) || (busNr > 1) { @@ -163,7 +179,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is either 0 or 2 which corresponds to /dev/i2c-0 or /dev/i2c-2. if (busNr != 0) && (busNr != 2) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -172,8 +188,8 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { } // translateAnalogPin converts analog pin name to pin position -func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, error) { - pinInfo, ok := c.analogPinMap[pin] +func (a *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, error) { + pinInfo, ok := a.analogPinMap[pin] if !ok { return "", false, false, 0, fmt.Errorf("Not a valid analog pin") } @@ -182,30 +198,30 @@ func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, er } // translatePin converts digital pin name to pin position -func (c *Adaptor) translateAndMuxDigitalPin(id string) (string, int, error) { - line, ok := c.pinMap[id] +func (a *Adaptor) translateAndMuxDigitalPin(id string) (string, int, error) { + line, ok := a.pinMap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } // mux is done by id, not by line - if err := c.muxPin(id, "gpio"); err != nil { + if err := a.muxPin(id, "gpio"); err != nil { return "", -1, err } return "", line, nil } -func (c *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) { - pinInfo, ok := c.pwmPinMap[id] +func (a *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) { + pinInfo, ok := a.pwmPinMap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a PWM pin", id) } - path, err := pinInfo.findPWMDir(c.sys) + path, err := pinInfo.findPWMDir(a.sys) if err != nil { return "", -1, err } - if err := c.muxPin(id, "pwm"); err != nil { + if err := a.muxPin(id, "pwm"); err != nil { return "", -1, err } @@ -230,9 +246,9 @@ func (p pwmPinDefinition) findPWMDir(sys *system.Accesser) (string, error) { return dir, nil } -func (c *Adaptor) muxPin(pin, cmd string) error { +func (a *Adaptor) muxPin(pin, cmd string) error { path := fmt.Sprintf("/sys/devices/platform/ocp/ocp:%s_pinmux/state", pin) - fi, e := c.sys.OpenFile(path, os.O_WRONLY, 0o666) + fi, e := a.sys.OpenFile(path, os.O_WRONLY, 0o666) defer fi.Close() //nolint:staticcheck // for historical reasons if e != nil { return e diff --git a/platforms/beaglebone/beaglebone_adaptor_test.go b/platforms/beaglebone/beaglebone_adaptor_test.go index 879999e3b..e94de6971 100644 --- a/platforms/beaglebone/beaglebone_adaptor_test.go +++ b/platforms/beaglebone/beaglebone_adaptor_test.go @@ -2,6 +2,7 @@ package beaglebone import ( "fmt" + "strconv" "strings" "testing" @@ -13,6 +14,7 @@ import ( "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" "gobot.io/x/gobot/v2/drivers/spi" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) @@ -45,47 +47,115 @@ const ( pwmChip0ExportPath = pwmChip0Dir + "export" pwmChip0UnexportPath = pwmChip0Dir + "unexport" pwmChip0Pwm0Dir = pwmChip0Dir + "pwm0/" - pwm0EnablePath = pwmChip0Pwm0Dir + "enable" - pwm0PeriodPath = pwmChip0Pwm0Dir + "period" - pwm0DutyCyclePath = pwmChip0Pwm0Dir + "duty_cycle" - pwm0PolarityPath = pwmChip0Pwm0Dir + "polarity" + pwmChip0Pwm1Dir = pwmChip0Dir + "pwm1/" + + pwm0EnablePath = pwmChip0Pwm0Dir + "enable" + pwm0PeriodPath = pwmChip0Pwm0Dir + "period" + pwm0DutyCyclePath = pwmChip0Pwm0Dir + "duty_cycle" + pwm0PolarityPath = pwmChip0Pwm0Dir + "polarity" - pwmChip0Pwm1Dir = pwmChip0Dir + "pwm1/" pwm1EnablePath = pwmChip0Pwm1Dir + "enable" pwm1PeriodPath = pwmChip0Pwm1Dir + "period" pwm1DutyCyclePath = pwmChip0Pwm1Dir + "duty_cycle" pwm1PolarityPath = pwmChip0Pwm1Dir + "polarity" ) -func TestPWM(t *testing.T) { - mockPaths := []string{ - "/sys/devices/platform/ocp/ocp:P9_22_pinmux/state", - "/sys/devices/platform/ocp/ocp:P9_21_pinmux/state", - "/sys/bus/iio/devices/iio:device0/in_voltage1_raw", - pwmChip0ExportPath, - pwmChip0UnexportPath, - pwm0EnablePath, - pwm0PeriodPath, - pwm0DutyCyclePath, - pwm0PolarityPath, - pwm1EnablePath, - pwm1PeriodPath, - pwm1DutyCyclePath, - pwm1PolarityPath, - } +var pwmMockPaths = []string{ + "/sys/devices/platform/ocp/ocp:P9_22_pinmux/state", + "/sys/devices/platform/ocp/ocp:P9_21_pinmux/state", + "/sys/bus/iio/devices/iio:device0/in_voltage1_raw", + pwmChip0ExportPath, + pwmChip0UnexportPath, + pwm0EnablePath, + pwm0PeriodPath, + pwm0DutyCyclePath, + pwm0PolarityPath, + pwm1EnablePath, + pwm1PeriodPath, + pwm1DutyCyclePath, + pwm1PolarityPath, +} - a, fs := initTestAdaptorWithMockedFilesystem(mockPaths) +func TestNewAdaptor(t *testing.T) { + // arrange & act + a := NewAdaptor() + // assert + assert.IsType(t, &Adaptor{}, a) + assert.True(t, strings.HasPrefix(a.Name(), "Beaglebone")) + assert.NotNil(t, a.sys) + assert.NotNil(t, a.mutex) + assert.NotNil(t, a.AnalogPinsAdaptor) + assert.NotNil(t, a.DigitalPinsAdaptor) + assert.NotNil(t, a.PWMPinsAdaptor) + assert.NotNil(t, a.I2cBusAdaptor) + assert.NotNil(t, a.SpiBusAdaptor) + assert.Equal(t, bbbPinMap, a.pinMap) + assert.Equal(t, bbbPwmPinMap, a.pwmPinMap) + assert.Equal(t, bbbAnalogPinMap, a.analogPinMap) + assert.Equal(t, "/sys/class/leds/beaglebone:green:", a.usrLed) + // act & assert + a.SetName("NewName") + assert.Equal(t, "NewName", a.Name()) +} + +func TestNewPocketBeagleAdaptor(t *testing.T) { + // arrange & act + a := NewPocketBeagleAdaptor() + // assert + assert.IsType(t, &PocketBeagleAdaptor{}, a) + assert.True(t, strings.HasPrefix(a.Name(), "PocketBeagle")) + assert.NotNil(t, a.sys) + assert.Equal(t, pocketBeaglePinMap, a.pinMap) + assert.Equal(t, pocketBeaglePwmPinMap, a.pwmPinMap) + assert.Equal(t, pocketBeagleAnalogPinMap, a.analogPinMap) + assert.Equal(t, "/sys/class/leds/beaglebone:green:", a.usrLed) +} + +func TestNewPocketBeagleAdaptorWithOption(t *testing.T) { + // arrange & act + a := NewPocketBeagleAdaptor(adaptors.WithGpiodAccess()) + // assert + require.NoError(t, a.Connect()) +} + +func TestPWMWrite(t *testing.T) { + // arrange + a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) fs.Files[pwm1DutyCyclePath].Contents = "0" fs.Files[pwm1PeriodPath].Contents = "0" - + // act & assert wrong pin require.ErrorContains(t, a.PwmWrite("P9_99", 175), "'P9_99' is not a valid id for a PWM pin") + + // act & assert values _ = a.PwmWrite("P9_21", 175) assert.Equal(t, "500000", fs.Files[pwm1PeriodPath].Contents) assert.Equal(t, "343137", fs.Files[pwm1DutyCyclePath].Contents) - _ = a.ServoWrite("P9_21", 100) - assert.Equal(t, "500000", fs.Files[pwm1PeriodPath].Contents) - assert.Equal(t, "40277", fs.Files[pwm1DutyCyclePath].Contents) + require.NoError(t, a.Finalize()) +} + +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "P9_21" + fiftyHzNano = 20000000 + ) + a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(pwmMockPaths) + require.NoError(t, a.Connect()) + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwm1PeriodPath].Contents) + assert.Equal(t, "500000", fs.Files[pwm1DutyCyclePath].Contents) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwm1PeriodPath].Contents) + assert.Equal(t, "2500000", fs.Files[pwm1DutyCyclePath].Contents) + // act & assert invalid pins + err = a.ServoWrite("3", 120) + require.ErrorContains(t, err, "'3' is not a valid id for a PWM pin") require.NoError(t, a.Finalize()) } @@ -160,13 +230,6 @@ func TestDigitalIO(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestName(t *testing.T) { - a := NewAdaptor() - assert.True(t, strings.HasPrefix(a.Name(), "Beaglebone")) - a.SetName("NewName") - assert.Equal(t, "NewName", a.Name()) -} - func TestAnalogReadFileError(t *testing.T) { mockPaths := []string{ "/sys/devices/platform/whatever", @@ -212,11 +275,6 @@ func TestDigitalPinFinalizeFileError(t *testing.T) { require.ErrorContains(t, err, "/sys/class/gpio/unexport: no such file") } -func TestPocketName(t *testing.T) { - a := NewPocketBeagleAdaptor() - assert.True(t, strings.HasPrefix(a.Name(), "PocketBeagle")) -} - func TestSpiDefaultValues(t *testing.T) { a := NewAdaptor() diff --git a/platforms/beaglebone/pocketbeagle_adaptor.go b/platforms/beaglebone/pocketbeagle_adaptor.go index 68c6db49e..5ef366970 100644 --- a/platforms/beaglebone/pocketbeagle_adaptor.go +++ b/platforms/beaglebone/pocketbeagle_adaptor.go @@ -2,7 +2,6 @@ package beaglebone import ( "gobot.io/x/gobot/v2" - "gobot.io/x/gobot/v2/platforms/adaptors" ) // PocketBeagleAdaptor is the Gobot Adaptor for the PocketBeagle @@ -14,7 +13,13 @@ type PocketBeagleAdaptor struct { } // NewPocketBeagleAdaptor creates a new Adaptor for the PocketBeagle -func NewPocketBeagleAdaptor(opts ...func(adaptors.Optioner)) *PocketBeagleAdaptor { +// Optional parameters: +// +// adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs +// adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewPocketBeagleAdaptor(opts ...interface{}) *PocketBeagleAdaptor { a := NewAdaptor(opts...) a.SetName(gobot.DefaultName("PocketBeagle")) a.pinMap = pocketBeaglePinMap diff --git a/platforms/chip/chip_adaptor.go b/platforms/chip/chip_adaptor.go index c7dd3a21a..7f9d999ce 100644 --- a/platforms/chip/chip_adaptor.go +++ b/platforms/chip/chip_adaptor.go @@ -42,67 +42,82 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("CHIP"), sys: sys, } - c.pinmap = chipPins + a.pinmap = chipPins baseAddr, _ := getXIOBase() for i := 0; i < 8; i++ { pin := fmt.Sprintf("XIO-P%d", i) - c.pinmap[pin] = sysfsPin{pin: baseAddr + i, pwmPin: -1} + a.pinmap[pin] = sysfsPin{pin: baseAddr + i, pwmPin: -1} + } + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + var pwmPinsOpts []adaptors.PwmPinsOptionApplier + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } } - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - return c + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + return a } // NewProAdaptor creates a C.H.I.P. Pro Adaptor func NewProAdaptor() *Adaptor { - c := NewAdaptor() - c.name = gobot.DefaultName("CHIP Pro") - c.pinmap = chipProPins - return c + a := NewAdaptor() + a.name = gobot.DefaultName("CHIP Pro") + a.pinmap = chipProPins + return a } // Name returns the name of the Adaptor -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the name of the Adaptor -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board and pins -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } @@ -139,7 +154,7 @@ func getXIOBase() (int, error) { return baseAddr, nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..2] which corresponds to /dev/i2c-0 through /dev/i2c-2. if (busNr < 0) || (busNr > 2) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -147,15 +162,15 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { - if val, ok := c.pinmap[id]; ok { +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { + if val, ok := a.pinmap[id]; ok { return "", val.pin, nil } return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } -func (c *Adaptor) translatePWMPin(id string) (string, int, error) { - sysPin, ok := c.pinmap[id] +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { + sysPin, ok := a.pinmap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a pin", id) } diff --git a/platforms/chip/chip_adaptor_test.go b/platforms/chip/chip_adaptor_test.go index 0a3d275ad..61aad0c58 100644 --- a/platforms/chip/chip_adaptor_test.go +++ b/platforms/chip/chip_adaptor_test.go @@ -2,6 +2,7 @@ package chip import ( "fmt" + "strconv" "strings" "testing" @@ -11,6 +12,7 @@ import ( "gobot.io/x/gobot/v2" "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) @@ -73,7 +75,6 @@ func TestNewProAdaptor(t *testing.T) { func TestFinalizeErrorAfterGPIO(t *testing.T) { a, fs := initTestAdaptorWithMockedFilesystem() - require.NoError(t, a.Connect()) require.NoError(t, a.DigitalWrite("CSID7", 1)) fs.WithWriteError = true @@ -87,7 +88,6 @@ func TestFinalizeErrorAfterPWM(t *testing.T) { fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents = "0" fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents = "0" - require.NoError(t, a.Connect()) require.NoError(t, a.PwmWrite("PWM0", 100)) fs.WithWriteError = true @@ -98,9 +98,8 @@ func TestFinalizeErrorAfterPWM(t *testing.T) { func TestDigitalIO(t *testing.T) { a, fs := initTestAdaptorWithMockedFilesystem() - _ = a.Connect() - _ = a.DigitalWrite("CSID7", 1) + require.NoError(t, a.DigitalWrite("CSID7", 1)) assert.Equal(t, "1", fs.Files["/sys/class/gpio/gpio139/value"].Contents) fs.Files["/sys/class/gpio/gpio50/value"].Contents = "1" @@ -126,36 +125,45 @@ func TestProDigitalIO(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestPWM(t *testing.T) { +func TestPWMWrite(t *testing.T) { + // arrange a, fs := initTestAdaptorWithMockedFilesystem() fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents = "0" fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents = "0" - - _ = a.Connect() - + // act err := a.PwmWrite("PWM0", 100) + // assert require.NoError(t, err) - assert.Equal(t, "0", fs.Files["/sys/class/pwm/pwmchip0/export"].Contents) assert.Equal(t, "1", fs.Files["/sys/class/pwm/pwmchip0/pwm0/enable"].Contents) assert.Equal(t, "3921568", fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents) assert.Equal(t, "10000000", fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents) // pwmPeriodDefault assert.Equal(t, "normal", fs.Files["/sys/class/pwm/pwmchip0/pwm0/polarity"].Contents) - // prepare 50Hz for servos - const fiftyHzNano = "20000000" - fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents = fiftyHzNano - err = a.ServoWrite("PWM0", 0) - require.NoError(t, err) + require.NoError(t, a.Finalize()) +} +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "PWM0" + fiftyHzNano = 20000000 + ) + a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(mockPaths) + require.NoError(t, a.Connect()) + fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents = "0" + fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents = "0" + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents) assert.Equal(t, "500000", fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents) - assert.Equal(t, fiftyHzNano, fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents) - - err = a.ServoWrite("PWM0", 180) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) require.NoError(t, err) - + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents) assert.Equal(t, "2500000", fs.Files["/sys/class/pwm/pwmchip0/pwm0/duty_cycle"].Contents) - assert.Equal(t, fiftyHzNano, fs.Files["/sys/class/pwm/pwmchip0/pwm0/period"].Contents) // pwmPeriodDefault require.NoError(t, a.Finalize()) } diff --git a/platforms/dragonboard/dragonboard_adaptor.go b/platforms/dragonboard/dragonboard_adaptor.go index 0f75eac41..5941b995f 100644 --- a/platforms/dragonboard/dragonboard_adaptor.go +++ b/platforms/dragonboard/dragonboard_adaptor.go @@ -50,7 +50,7 @@ var fixedPins = map[string]int{ // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { sys := system.NewAccesser() c := &Adaptor{ name: gobot.DefaultName("DragonBoard"), diff --git a/platforms/intel-iot/edison/edison_adaptor.go b/platforms/intel-iot/edison/edison_adaptor.go index 07a1ca76c..87c2c33c2 100644 --- a/platforms/intel-iot/edison/edison_adaptor.go +++ b/platforms/intel-iot/edison/edison_adaptor.go @@ -48,108 +48,121 @@ type Adaptor struct { // NewAdaptor returns a new Edison Adaptor of the given type. // Supported types are: "arduino", "miniboard", "sparkfun", an empty string defaults to "arduino" -func NewAdaptor(boardType ...string) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("Edison"), board: "arduino", sys: sys, pinmap: arduinoPinMap, } - if len(boardType) > 0 && boardType[0] != "" { - c.board = boardType[0] + + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPinInitializer(pwmPinInitializer)} + for _, opt := range opts { + switch o := opt.(type) { + case string: + if o != "" { + a.board = o + } + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } } - c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translateAndMuxPWMPin, - adaptors.WithPWMPinInitializer(pwmPinInitializer)) + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translateAndMuxPWMPin, pwmPinsOpts...) defI2cBusNr := defaultI2cBusNumber - if c.board != "arduino" { + if a.board != "arduino" { defI2cBusNr = defaultI2cBusNumberOther } - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateAndSetupI2cBusNumber, defI2cBusNr) - return c + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateAndSetupI2cBusNumber, defI2cBusNr) + return a } // Name returns the Adaptors name -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the Adaptors name -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect initializes the Edison for use with the Arduino breakout board -func (c *Adaptor) Connect() error { - c.digitalPins = make(map[int]gobot.DigitalPinner) +func (a *Adaptor) Connect() error { + a.digitalPins = make(map[int]gobot.DigitalPinner) - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.AnalogPinsAdaptor.Connect(); err != nil { + if err := a.AnalogPinsAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - switch c.board { + switch a.board { case "sparkfun": - c.pinmap = sparkfunPinMap + a.pinmap = sparkfunPinMap case "arduino": - c.board = "arduino" - c.pinmap = arduinoPinMap - if err := c.arduinoSetup(); err != nil { + a.board = "arduino" + a.pinmap = arduinoPinMap + if err := a.arduinoSetup(); err != nil { return err } case "miniboard": - c.pinmap = miniboardPinMap + a.pinmap = miniboardPinMap default: - return fmt.Errorf("Unknown board type: %s", c.board) + return fmt.Errorf("Unknown board type: %s", a.board) } return nil } // Finalize releases all i2c devices and exported analog, digital, pwm pins. -func (c *Adaptor) Finalize() error { +func (a *Adaptor) Finalize() error { var err error - if c.tristate != nil { - if errs := c.tristate.Unexport(); errs != nil { + if a.tristate != nil { + if errs := a.tristate.Unexport(); errs != nil { err = multierror.Append(err, errs) } } - c.tristate = nil + a.tristate = nil - for _, pin := range c.digitalPins { + for _, pin := range a.digitalPins { if pin != nil { if errs := pin.Unexport(); errs != nil { err = multierror.Append(err, errs) } } } - c.digitalPins = nil + a.digitalPins = nil - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.AnalogPinsAdaptor.Finalize(); e != nil { + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - c.arduinoI2cInitialized = false + a.arduinoI2cInitialized = false return err } // DigitalRead reads digital value from pin -func (c *Adaptor) DigitalRead(pin string) (int, error) { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) DigitalRead(pin string) (int, error) { + a.mutex.Lock() + defer a.mutex.Unlock() - sysPin, err := c.digitalPin(pin, system.WithPinDirectionInput()) + sysPin, err := a.digitalPin(pin, system.WithPinDirectionInput()) if err != nil { return 0, err } @@ -157,25 +170,25 @@ func (c *Adaptor) DigitalRead(pin string) (int, error) { } // DigitalWrite writes a value to the pin. Acceptable values are 1 or 0. -func (c *Adaptor) DigitalWrite(pin string, val byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) DigitalWrite(pin string, val byte) error { + a.mutex.Lock() + defer a.mutex.Unlock() - return c.digitalWrite(pin, val) + return a.digitalWrite(pin, val) } // DigitalPin returns a digital pin. If the pin is initially acquired, it is an input. // Pin direction and other options can be changed afterwards by pin.ApplyOptions() at any time. -func (c *Adaptor) DigitalPin(id string) (gobot.DigitalPinner, error) { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) DigitalPin(id string) (gobot.DigitalPinner, error) { + a.mutex.Lock() + defer a.mutex.Unlock() - return c.digitalPin(id) + return a.digitalPin(id) } // AnalogRead returns value from analog reading of specified pin -func (c *Adaptor) AnalogRead(pin string) (int, error) { - rawRead, err := c.AnalogPinsAdaptor.AnalogRead(pin) +func (a *Adaptor) AnalogRead(pin string) (int, error) { + rawRead, err := a.AnalogPinsAdaptor.AnalogRead(pin) if err != nil { return 0, err } @@ -183,20 +196,20 @@ func (c *Adaptor) AnalogRead(pin string) (int, error) { return rawRead / 4, err } -func (c *Adaptor) validateAndSetupI2cBusNumber(busNr int) error { +func (a *Adaptor) validateAndSetupI2cBusNumber(busNr int) error { // Valid bus number is 6 for "arduino", otherwise 1. - if busNr == 6 && c.board == "arduino" { - if !c.arduinoI2cInitialized { - if err := c.arduinoI2CSetup(); err != nil { + if busNr == 6 && a.board == "arduino" { + if !a.arduinoI2cInitialized { + if err := a.arduinoI2CSetup(); err != nil { return err } - c.arduinoI2cInitialized = true + a.arduinoI2cInitialized = true return nil } return nil } - if busNr == 1 && c.board != "arduino" { + if busNr == 1 && a.board != "arduino" { return nil } @@ -204,81 +217,81 @@ func (c *Adaptor) validateAndSetupI2cBusNumber(busNr int) error { } // arduinoSetup does needed setup for the Arduino compatible breakout board -func (c *Adaptor) arduinoSetup() error { +func (a *Adaptor) arduinoSetup() error { // TODO: also check to see if device labels for // /sys/class/gpio/gpiochip{200,216,232,248}/label == "pcal9555a" - tpin, err := c.newExportedDigitalPin(214, system.WithPinDirectionOutput(system.LOW)) + tpin, err := a.newExportedDigitalPin(214, system.WithPinDirectionOutput(system.LOW)) if err != nil { return err } - c.tristate = tpin + a.tristate = tpin for _, i := range []int{263, 262} { - if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.HIGH)); err != nil { + if err := a.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.HIGH)); err != nil { return err } } for _, i := range []int{240, 241, 242, 243} { - if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { + if err := a.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { return err } } for _, i := range []string{"111", "115", "114", "109"} { - if err := c.changePinMode(i, "1"); err != nil { + if err := a.changePinMode(i, "1"); err != nil { return err } } for _, i := range []string{"131", "129", "40"} { - if err := c.changePinMode(i, "0"); err != nil { + if err := a.changePinMode(i, "0"); err != nil { return err } } - return c.tristate.Write(system.HIGH) + return a.tristate.Write(system.HIGH) } -func (c *Adaptor) arduinoI2CSetup() error { - if c.tristate == nil { +func (a *Adaptor) arduinoI2CSetup() error { + if a.tristate == nil { return fmt.Errorf("not connected") } - if err := c.tristate.Write(system.LOW); err != nil { + if err := a.tristate.Write(system.LOW); err != nil { return err } for _, i := range []int{14, 165, 212, 213} { - if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionInput()); err != nil { + if err := a.newUnexportedDigitalPin(i, system.WithPinDirectionInput()); err != nil { return err } } for _, i := range []int{236, 237, 204, 205} { - if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { + if err := a.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { return err } } for _, i := range []string{"28", "27"} { - if err := c.changePinMode(i, "1"); err != nil { + if err := a.changePinMode(i, "1"); err != nil { return err } } - return c.tristate.Write(system.HIGH) + return a.tristate.Write(system.HIGH) } -func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) { - i := c.pinmap[id] +func (a *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) { + i := a.pinmap[id] - err := c.ensureDigitalPin(i.pin, o...) + err := a.ensureDigitalPin(i.pin, o...) if err != nil { return nil, err } - pin := c.digitalPins[i.pin] + pin := a.digitalPins[i.pin] vpin, ok := pin.(gobot.DigitalPinValuer) if !ok { return nil, fmt.Errorf("can not determine the direction behavior") @@ -289,7 +302,7 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool if dir == system.OUT { rop = system.WithPinDirectionInput() } - if err := c.ensureDigitalPin(i.resistor, rop); err != nil { + if err := a.ensureDigitalPin(i.resistor, rop); err != nil { return nil, err } } @@ -299,14 +312,14 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool if dir == system.OUT { lop = system.WithPinDirectionOutput(system.HIGH) } - if err := c.ensureDigitalPin(i.levelShifter, lop); err != nil { + if err := a.ensureDigitalPin(i.levelShifter, lop); err != nil { return nil, err } } if len(i.mux) > 0 { for _, mux := range i.mux { - if err := c.ensureDigitalPin(mux.pin, system.WithPinDirectionOutput(mux.value)); err != nil { + if err := a.ensureDigitalPin(mux.pin, system.WithPinDirectionOutput(mux.value)); err != nil { return nil, err } } @@ -315,15 +328,15 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool return pin, nil } -func (c *Adaptor) ensureDigitalPin(idx int, o ...func(gobot.DigitalPinOptioner) bool) error { - pin := c.digitalPins[idx] +func (a *Adaptor) ensureDigitalPin(idx int, o ...func(gobot.DigitalPinOptioner) bool) error { + pin := a.digitalPins[idx] var err error if pin == nil { - pin, err = c.newExportedDigitalPin(idx, o...) + pin, err = a.newExportedDigitalPin(idx, o...) if err != nil { return err } - c.digitalPins[idx] = pin + a.digitalPins[idx] = pin } else { if err := pin.ApplyOptions(o...); err != nil { return err @@ -332,14 +345,14 @@ func (c *Adaptor) ensureDigitalPin(idx int, o ...func(gobot.DigitalPinOptioner) return nil } -func pwmPinInitializer(pin gobot.PWMPinner) error { +func pwmPinInitializer(_ string, pin gobot.PWMPinner) error { if err := pin.Export(); err != nil { return err } return pin.SetEnabled(true) } -func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, error) { +func (a *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 @@ -349,43 +362,43 @@ func (c *Adaptor) translateAnalogPin(pin string) (string, bool, bool, uint16, er return path, read, write, readBufLen, nil } -func (c *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) { - sysPin, ok := c.pinmap[id] +func (a *Adaptor) translateAndMuxPWMPin(id string) (string, int, error) { + sysPin, ok := a.pinmap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a pin", id) } if sysPin.pwmPin == -1 { return "", -1, fmt.Errorf("'%s' is not a valid id for a PWM pin", id) } - if err := c.digitalWrite(id, 1); err != nil { + if err := a.digitalWrite(id, 1); err != nil { return "", -1, err } - if err := c.changePinMode(strconv.Itoa(sysPin.pin), "1"); err != nil { + if err := a.changePinMode(strconv.Itoa(sysPin.pin), "1"); err != nil { return "", -1, err } return "/sys/class/pwm/pwmchip0", sysPin.pwmPin, nil } -func (c *Adaptor) newUnexportedDigitalPin(i int, o ...func(gobot.DigitalPinOptioner) bool) error { - io := c.sys.NewDigitalPin("", i, o...) +func (a *Adaptor) newUnexportedDigitalPin(i int, o ...func(gobot.DigitalPinOptioner) bool) error { + io := a.sys.NewDigitalPin("", i, o...) if err := io.Export(); err != nil { return err } return io.Unexport() } -func (c *Adaptor) newExportedDigitalPin( +func (a *Adaptor) newExportedDigitalPin( pin int, o ...func(gobot.DigitalPinOptioner) bool, ) (gobot.DigitalPinner, error) { - sysPin := c.sys.NewDigitalPin("", pin, o...) + sysPin := a.sys.NewDigitalPin("", pin, o...) err := sysPin.Export() return sysPin, err } // changePinMode writes pin mode to current_pinmux file -func (c *Adaptor) changePinMode(pin, mode string) error { - file, err := c.sys.OpenFile("/sys/kernel/debug/gpio_debug/gpio"+pin+"/current_pinmux", os.O_WRONLY, 0o644) +func (a *Adaptor) changePinMode(pin, mode string) error { + file, err := a.sys.OpenFile("/sys/kernel/debug/gpio_debug/gpio"+pin+"/current_pinmux", os.O_WRONLY, 0o644) defer file.Close() //nolint:staticcheck // for historical reasons if err != nil { return err @@ -394,8 +407,8 @@ func (c *Adaptor) changePinMode(pin, mode string) error { return err } -func (c *Adaptor) digitalWrite(pin string, val byte) error { - sysPin, err := c.digitalPin(pin, system.WithPinDirectionOutput(int(val))) +func (a *Adaptor) digitalWrite(pin string, val byte) error { + sysPin, err := a.digitalPin(pin, system.WithPinDirectionOutput(int(val))) if err != nil { return err } diff --git a/platforms/intel-iot/edison/edison_adaptor_test.go b/platforms/intel-iot/edison/edison_adaptor_test.go index 8451d949e..05bb4bad9 100644 --- a/platforms/intel-iot/edison/edison_adaptor_test.go +++ b/platforms/intel-iot/edison/edison_adaptor_test.go @@ -1,7 +1,6 @@ package edison import ( - "fmt" "strings" "testing" @@ -579,15 +578,15 @@ func Test_validateI2cBusNumber(t *testing.T) { tests := map[string]struct { board string busNr int - wantErr error + wantErr string }{ "arduino_number_negative_error": { busNr: -1, - wantErr: fmt.Errorf("Unsupported I2C bus '-1'"), + wantErr: "Unsupported I2C bus '-1'", }, "arduino_number_1_error": { busNr: 1, - wantErr: fmt.Errorf("Unsupported I2C bus '1'"), + wantErr: "Unsupported I2C bus '1'", }, "arduino_number_6_ok": { busNr: 6, @@ -595,7 +594,7 @@ func Test_validateI2cBusNumber(t *testing.T) { "sparkfun_number_negative_error": { board: "sparkfun", busNr: -1, - wantErr: fmt.Errorf("Unsupported I2C bus '-1'"), + wantErr: "Unsupported I2C bus '-1'", }, "sparkfun_number_1_ok": { board: "sparkfun", @@ -604,7 +603,7 @@ func Test_validateI2cBusNumber(t *testing.T) { "miniboard_number_6_error": { board: "miniboard", busNr: 6, - wantErr: fmt.Errorf("Unsupported I2C bus '6'"), + wantErr: "Unsupported I2C bus '6'", }, } for name, tc := range tests { @@ -616,7 +615,11 @@ func Test_validateI2cBusNumber(t *testing.T) { // act err := a.validateAndSetupI2cBusNumber(tc.busNr) // assert - assert.Equal(t, tc.wantErr, err) + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + } else { + require.NoError(t, err) + } }) } } diff --git a/platforms/intel-iot/joule/joule_adaptor.go b/platforms/intel-iot/joule/joule_adaptor.go index 0afdc83d5..5706ac233 100644 --- a/platforms/intel-iot/joule/joule_adaptor.go +++ b/platforms/intel-iot/joule/joule_adaptor.go @@ -34,59 +34,74 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("Joule"), sys: sys, } - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin, - adaptors.WithPWMPinInitializer(pwmPinInitializer)) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - return c + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPinInitializer(pwmPinInitializer)} + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + return a } // Name returns the Adaptors name -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the Adaptors name -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize releases all i2c devices and exported digital and pwm pins. -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..2] which corresponds to /dev/i2c-0 through /dev/i2c-2. if (busNr < 0) || (busNr > 2) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -94,14 +109,14 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { if val, ok := sysfsPinMap[id]; ok { return "", val.pin, nil } return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } -func (c *Adaptor) translatePWMPin(id string) (string, int, error) { +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { sysPin, ok := sysfsPinMap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a pin", id) @@ -112,7 +127,7 @@ func (c *Adaptor) translatePWMPin(id string) (string, int, error) { return "/sys/class/pwm/pwmchip0", sysPin.pwmPin, nil } -func pwmPinInitializer(pin gobot.PWMPinner) error { +func pwmPinInitializer(_ string, pin gobot.PWMPinner) error { if err := pin.Export(); err != nil { return err } diff --git a/platforms/jetson/jetson_adaptor.go b/platforms/jetson/jetson_adaptor.go index ce336ed34..dacd9ede4 100644 --- a/platforms/jetson/jetson_adaptor.go +++ b/platforms/jetson/jetson_adaptor.go @@ -41,7 +41,7 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { sys := system.NewAccesser() c := &Adaptor{ name: gobot.DefaultName("JetsonNano"), diff --git a/platforms/nanopi/nanopi_adaptor.go b/platforms/nanopi/nanopi_adaptor.go index 0ef45cffb..ab78758d8 100644 --- a/platforms/nanopi/nanopi_adaptor.go +++ b/platforms/nanopi/nanopi_adaptor.go @@ -71,79 +71,94 @@ type Adaptor struct { // adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior // adaptors.WithGpioDebounce(pin, period): sets the input debouncer // adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection -func NewNeoAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewNeoAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("NanoPi NEO Board"), sys: sys, gpioPinMap: neoGpioPins, pwmPinMap: neoPwmPins, } - c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin) - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin, - adaptors.WithPolarityInvertedIdentifier(pwmInvertedIdentifier)) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)} + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin) + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the name of the Adaptor -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the name of the Adaptor -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.AnalogPinsAdaptor.Connect(); err != nil { + if err := a.AnalogPinsAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board, pins and bus -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.AnalogPinsAdaptor.Finalize(); e != nil { + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err } -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0] which corresponds to /dev/spidev0.x // x is the chip number <255 if busNr != 0 { @@ -152,7 +167,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..2] which corresponds to /dev/i2c-0 through /dev/i2c-2. if (busNr < 0) || (busNr > 2) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -160,14 +175,14 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { +func (a *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { pinInfo, ok := analogPinDefinitions[id] if !ok { return "", false, false, 0, fmt.Errorf("'%s' is not a valid id for a analog pin", id) } path := pinInfo.path - info, err := c.sys.Stat(path) + info, err := a.sys.Stat(path) if err != nil { return "", false, false, 0, fmt.Errorf("Error (%v) on access '%s'", err, path) } @@ -178,12 +193,12 @@ func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, err return path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { - pindef, ok := c.gpioPinMap[id] +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { + pindef, ok := a.gpioPinMap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } - if c.sys.IsSysfsDigitalPinAccess() { + if a.sys.IsSysfsDigitalPinAccess() { return "", pindef.sysfs, nil } chip := fmt.Sprintf("gpiochip%d", pindef.cdev.chip) @@ -191,12 +206,12 @@ func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { return chip, line, nil } -func (c *Adaptor) translatePWMPin(id string) (string, int, error) { - pinInfo, ok := c.pwmPinMap[id] +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { + pinInfo, ok := a.pwmPinMap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a PWM pin", id) } - path, err := pinInfo.findPWMDir(c.sys) + path, err := pinInfo.findPWMDir(a.sys) if err != nil { return "", -1, err } diff --git a/platforms/nanopi/nanopi_adaptor_test.go b/platforms/nanopi/nanopi_adaptor_test.go index 8c44ce240..5410323bc 100644 --- a/platforms/nanopi/nanopi_adaptor_test.go +++ b/platforms/nanopi/nanopi_adaptor_test.go @@ -13,6 +13,7 @@ import ( "gobot.io/x/gobot/v2/drivers/aio" "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) @@ -30,8 +31,6 @@ const ( pwmPeriodPath = pwmPwmDir + "period" pwmDutyCyclePath = pwmPwmDir + "duty_cycle" pwmPolarityPath = pwmPwmDir + "polarity" - - fiftyHzNano = "20000000" ) var pwmMockPaths = []string{ @@ -144,29 +143,46 @@ func TestInvalidPWMPin(t *testing.T) { } func TestPwmWrite(t *testing.T) { + // arrange a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) preparePwmFs(fs) - + // act err := a.PwmWrite("PWM", 100) + // assert require.NoError(t, err) - assert.Equal(t, "0", fs.Files[pwmExportPath].Contents) assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) assert.Equal(t, strconv.Itoa(10000000), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) - // prepare 50Hz for servos - fs.Files[pwmPeriodPath].Contents = fiftyHzNano - err = a.ServoWrite("PWM", 0) - require.NoError(t, err) + require.NoError(t, a.Finalize()) +} +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "PWM" + fiftyHzNano = 20000000 + ) + a := NewNeoAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(pwmMockPaths) + preparePwmFs(fs) + require.NoError(t, a.Connect()) + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "500000", fs.Files[pwmDutyCyclePath].Contents) - - err = a.ServoWrite("PWM", 180) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) require.NoError(t, err) - + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "2500000", fs.Files[pwmDutyCyclePath].Contents) + // act & assert invalid pins + err = a.ServoWrite("3", 120) + require.ErrorContains(t, err, "'3' is not a valid id for a PWM pin") + require.NoError(t, a.Finalize()) } diff --git a/platforms/raspi/raspi_adaptor.go b/platforms/raspi/raspi_adaptor.go index 75e3fe5e0..a98ff9fe1 100644 --- a/platforms/raspi/raspi_adaptor.go +++ b/platforms/raspi/raspi_adaptor.go @@ -56,7 +56,7 @@ type Adaptor struct { // adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior // adaptors.WithGpioDebounce(pin, period): sets the input debouncer // adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) c := &Adaptor{ name: gobot.DefaultName("RaspberryPi"), diff --git a/platforms/rockpi/rockpi_adaptor.go b/platforms/rockpi/rockpi_adaptor.go index 13418f86c..cf159a716 100644 --- a/platforms/rockpi/rockpi_adaptor.go +++ b/platforms/rockpi/rockpi_adaptor.go @@ -44,7 +44,7 @@ type Adaptor struct { // adaptors.WithGpiodAccess(): use character device gpiod driver instead of the default sysfs (NOT work on RockPi4C+!) // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# // adaptors.WithGpiosActiveLow(pin's): invert the pin behavior -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +func NewAdaptor(opts ...func(adaptors.DigitalPinsOptioner)) *Adaptor { sys := system.NewAccesser() c := &Adaptor{ name: gobot.DefaultName("RockPi"), diff --git a/platforms/tinkerboard/adaptor.go b/platforms/tinkerboard/adaptor.go index f63ce80c7..01f926660 100644 --- a/platforms/tinkerboard/adaptor.go +++ b/platforms/tinkerboard/adaptor.go @@ -50,7 +50,7 @@ type pwmPinDefinition struct { type Adaptor struct { name string sys *system.Accesser - mutex sync.Mutex + mutex *sync.Mutex *adaptors.AnalogPinsAdaptor *adaptors.DigitalPinsAdaptor *adaptors.PWMPinsAdaptor @@ -67,79 +67,95 @@ type Adaptor struct { // adaptors.WithGpiosActiveLow(pin's): invert the pin behavior // adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor // +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +// // note from RK3288 datasheet: "The pull direction (pullup or pulldown) for all of GPIOs are software-programmable", but // the latter is not working for any pin (armbian 22.08.7) -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) - c := &Adaptor{ - name: gobot.DefaultName("Tinker Board"), - sys: sys, - } - c.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, c.translateAnalogPin) - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin, - adaptors.WithPolarityInvertedIdentifier(pwmInvertedIdentifier)) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + a := &Adaptor{ + name: gobot.DefaultName("Tinker Board"), + sys: sys, + mutex: &sync.Mutex{}, + } + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + pwmPinsOpts := []adaptors.PwmPinsOptionApplier{adaptors.WithPWMPolarityInvertedIdentifier(pwmInvertedIdentifier)} + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + a.AnalogPinsAdaptor = adaptors.NewAnalogPinsAdaptor(sys, a.translateAnalogPin) + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the name of the Adaptor -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the name of the Adaptor -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.AnalogPinsAdaptor.Connect(); err != nil { + if err := a.AnalogPinsAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board, pins and bus -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.AnalogPinsAdaptor.Finalize(); e != nil { + if e := a.AnalogPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err } -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0,2] which corresponds to /dev/spidev0.x, /dev/spidev2.x // x is the chip number <255 if (busNr != 0) && (busNr != 2) { @@ -148,7 +164,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [0..4] which corresponds to /dev/i2c-0 through /dev/i2c-4. // We don't support "/dev/i2c-6 DesignWare HDMI". if (busNr < 0) || (busNr > 4) { @@ -157,14 +173,14 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { +func (a *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, error) { pinInfo, ok := analogPinDefinitions[id] if !ok { return "", false, false, 0, fmt.Errorf("'%s' is not a valid id for a analog pin", id) } path := pinInfo.path - info, err := c.sys.Stat(path) + info, err := a.sys.Stat(path) if err != nil { return "", false, false, 0, fmt.Errorf("Error (%v) on access '%s'", err, path) } @@ -175,12 +191,12 @@ func (c *Adaptor) translateAnalogPin(id string) (string, bool, bool, uint16, err return path, pinInfo.r, pinInfo.w, pinInfo.bufLen, nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { pindef, ok := gpioPinDefinitions[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } - if c.sys.IsSysfsDigitalPinAccess() { + if a.sys.IsSysfsDigitalPinAccess() { return "", pindef.sysfs, nil } chip := fmt.Sprintf("gpiochip%d", pindef.cdev.chip) @@ -188,12 +204,12 @@ func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { return chip, line, nil } -func (c *Adaptor) translatePWMPin(id string) (string, int, error) { +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { pinInfo, ok := pwmPinDefinitions[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a PWM pin", id) } - path, err := pinInfo.findPWMDir(c.sys) + path, err := pinInfo.findPWMDir(a.sys) if err != nil { return "", -1, err } diff --git a/platforms/tinkerboard/adaptor_test.go b/platforms/tinkerboard/adaptor_test.go index 71b95b39d..0b6a5fe6d 100644 --- a/platforms/tinkerboard/adaptor_test.go +++ b/platforms/tinkerboard/adaptor_test.go @@ -13,6 +13,7 @@ import ( "gobot.io/x/gobot/v2/drivers/aio" "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) @@ -79,13 +80,31 @@ func initTestAdaptorWithMockedFilesystem(mockPaths []string) (*Adaptor, *system. return a, fs } -func TestName(t *testing.T) { +func TestNewAdaptor(t *testing.T) { + // arrange & act a := NewAdaptor() + // assert + assert.IsType(t, &Adaptor{}, a) assert.True(t, strings.HasPrefix(a.Name(), "Tinker Board")) + assert.NotNil(t, a.sys) + assert.NotNil(t, a.mutex) + assert.NotNil(t, a.AnalogPinsAdaptor) + assert.NotNil(t, a.DigitalPinsAdaptor) + assert.NotNil(t, a.PWMPinsAdaptor) + assert.NotNil(t, a.I2cBusAdaptor) + assert.NotNil(t, a.SpiBusAdaptor) + // act & assert a.SetName("NewName") assert.Equal(t, "NewName", a.Name()) } +func TestNewAdaptorWithOption(t *testing.T) { + // arrange & act + a := NewAdaptor(adaptors.WithGpiosActiveLow("1")) + // assert + require.NoError(t, a.Connect()) +} + func TestDigitalIO(t *testing.T) { // only basic tests needed, further tests are done in "digitalpinsadaptor.go" a, fs := initTestAdaptorWithMockedFilesystem(gpioMockPaths) @@ -101,7 +120,7 @@ func TestDigitalIO(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestAnalog(t *testing.T) { +func TestAnalogRead(t *testing.T) { mockPaths := []string{ "/sys/class/thermal/thermal_zone0/temp", } @@ -124,47 +143,50 @@ func TestAnalog(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestInvalidPWMPin(t *testing.T) { - a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) - preparePwmFs(fs) - - err := a.PwmWrite("666", 42) - require.ErrorContains(t, err, "'666' is not a valid id for a PWM pin") - - err = a.ServoWrite("666", 120) - require.ErrorContains(t, err, "'666' is not a valid id for a PWM pin") - - err = a.PwmWrite("3", 42) - require.ErrorContains(t, err, "'3' is not a valid id for a PWM pin") - - err = a.ServoWrite("3", 120) - require.ErrorContains(t, err, "'3' is not a valid id for a PWM pin") -} - func TestPwmWrite(t *testing.T) { + // arrange a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) preparePwmFs(fs) - + // act err := a.PwmWrite("33", 100) + // assert require.NoError(t, err) - assert.Equal(t, "0", fs.Files[pwmExportPath].Contents) assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) - assert.Equal(t, strconv.Itoa(10000000), fs.Files[pwmPeriodPath].Contents) + assert.Equal(t, "10000000", fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) + // act & assert invalid pin + err = a.PwmWrite("666", 42) + require.ErrorContains(t, err, "'666' is not a valid id for a PWM pin") - // prepare 50Hz for servos - fs.Files[pwmPeriodPath].Contents = strconv.Itoa(20000000) - err = a.ServoWrite("33", 0) - require.NoError(t, err) + require.NoError(t, a.Finalize()) +} +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "33" + fiftyHzNano = 20000000 + ) + a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(pwmMockPaths) + preparePwmFs(fs) + require.NoError(t, a.Connect()) + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "500000", fs.Files[pwmDutyCyclePath].Contents) - - err = a.ServoWrite("33", 180) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) require.NoError(t, err) - + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "2500000", fs.Files[pwmDutyCyclePath].Contents) + // act & assert invalid pins + err = a.ServoWrite("3", 120) + require.ErrorContains(t, err, "'3' is not a valid id for a PWM pin") + require.NoError(t, a.Finalize()) } diff --git a/platforms/upboard/up2/adaptor.go b/platforms/upboard/up2/adaptor.go index d4a80660c..b58d1258d 100644 --- a/platforms/upboard/up2/adaptor.go +++ b/platforms/upboard/up2/adaptor.go @@ -56,77 +56,93 @@ type Adaptor struct { // // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# -func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { +// +// Optional parameters for PWM, see [adaptors.NewPWMPinsAdaptor] +func NewAdaptor(opts ...interface{}) *Adaptor { sys := system.NewAccesser() - c := &Adaptor{ + a := &Adaptor{ name: gobot.DefaultName("UP2"), sys: sys, ledPath: "/sys/class/leds/upboard:%s:/brightness", pinmap: fixedPins, } - c.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, c.translateDigitalPin, opts...) - c.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, c.translatePWMPin) - c.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, c.validateI2cBusNumber, defaultI2cBusNumber) - c.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, c.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, + + var digitalPinsOpts []func(adaptors.DigitalPinsOptioner) + var pwmPinsOpts []adaptors.PwmPinsOptionApplier + for _, opt := range opts { + switch o := opt.(type) { + case func(adaptors.DigitalPinsOptioner): + digitalPinsOpts = append(digitalPinsOpts, o) + case adaptors.PwmPinsOptionApplier: + pwmPinsOpts = append(pwmPinsOpts, o) + default: + panic(fmt.Sprintf("'%s' can not be applied on adaptor '%s'", opt, a.name)) + } + } + + a.DigitalPinsAdaptor = adaptors.NewDigitalPinsAdaptor(sys, a.translateDigitalPin, digitalPinsOpts...) + a.PWMPinsAdaptor = adaptors.NewPWMPinsAdaptor(sys, a.translatePWMPin, pwmPinsOpts...) + a.I2cBusAdaptor = adaptors.NewI2cBusAdaptor(sys, a.validateI2cBusNumber, defaultI2cBusNumber) + a.SpiBusAdaptor = adaptors.NewSpiBusAdaptor(sys, a.validateSpiBusNumber, defaultSpiBusNumber, defaultSpiChipNumber, defaultSpiMode, defaultSpiBitsNumber, defaultSpiMaxSpeed) - return c + return a } // Name returns the name of the Adaptor -func (c *Adaptor) Name() string { return c.name } +func (a *Adaptor) Name() string { return a.name } // SetName sets the name of the Adaptor -func (c *Adaptor) SetName(n string) { c.name = n } +func (a *Adaptor) SetName(n string) { a.name = n } // Connect create new connection to board and pins. -func (c *Adaptor) Connect() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Connect() error { + a.mutex.Lock() + defer a.mutex.Unlock() - if err := c.SpiBusAdaptor.Connect(); err != nil { + if err := a.SpiBusAdaptor.Connect(); err != nil { return err } - if err := c.I2cBusAdaptor.Connect(); err != nil { + if err := a.I2cBusAdaptor.Connect(); err != nil { return err } - if err := c.PWMPinsAdaptor.Connect(); err != nil { + if err := a.PWMPinsAdaptor.Connect(); err != nil { return err } - return c.DigitalPinsAdaptor.Connect() + return a.DigitalPinsAdaptor.Connect() } // Finalize closes connection to board and pins -func (c *Adaptor) Finalize() error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) Finalize() error { + a.mutex.Lock() + defer a.mutex.Unlock() - err := c.DigitalPinsAdaptor.Finalize() + err := a.DigitalPinsAdaptor.Finalize() - if e := c.PWMPinsAdaptor.Finalize(); e != nil { + if e := a.PWMPinsAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.I2cBusAdaptor.Finalize(); e != nil { + if e := a.I2cBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } - if e := c.SpiBusAdaptor.Finalize(); e != nil { + if e := a.SpiBusAdaptor.Finalize(); e != nil { err = multierror.Append(err, e) } return err } // DigitalWrite writes digital value to the specified pin. -func (c *Adaptor) DigitalWrite(id string, val byte) error { - c.mutex.Lock() - defer c.mutex.Unlock() +func (a *Adaptor) DigitalWrite(id string, val byte) error { + a.mutex.Lock() + defer a.mutex.Unlock() // is it one of the built-in LEDs? if id == LEDRed || id == LEDBlue || id == LEDGreen || id == LEDYellow { - pinPath := fmt.Sprintf(c.ledPath, id) - fi, err := c.sys.OpenFile(pinPath, os.O_WRONLY|os.O_APPEND, 0o666) + pinPath := fmt.Sprintf(a.ledPath, id) + fi, err := a.sys.OpenFile(pinPath, os.O_WRONLY|os.O_APPEND, 0o666) defer fi.Close() //nolint:staticcheck // for historical reasons if err != nil { return err @@ -135,10 +151,10 @@ func (c *Adaptor) DigitalWrite(id string, val byte) error { return err } - return c.DigitalPinsAdaptor.DigitalWrite(id, val) + return a.DigitalPinsAdaptor.DigitalWrite(id, val) } -func (c *Adaptor) validateSpiBusNumber(busNr int) error { +func (a *Adaptor) validateSpiBusNumber(busNr int) error { // Valid bus numbers are [0,1] which corresponds to /dev/spidev0.x through /dev/spidev1.x. // x is the chip number <255 if (busNr < 0) || (busNr > 1) { @@ -147,7 +163,7 @@ func (c *Adaptor) validateSpiBusNumber(busNr int) error { return nil } -func (c *Adaptor) validateI2cBusNumber(busNr int) error { +func (a *Adaptor) validateI2cBusNumber(busNr int) error { // Valid bus number is [5..6] which corresponds to /dev/i2c-5 through /dev/i2c-6. if (busNr < 5) || (busNr > 6) { return fmt.Errorf("Bus number %d out of range", busNr) @@ -155,15 +171,15 @@ func (c *Adaptor) validateI2cBusNumber(busNr int) error { return nil } -func (c *Adaptor) translateDigitalPin(id string) (string, int, error) { - if val, ok := c.pinmap[id]; ok { +func (a *Adaptor) translateDigitalPin(id string) (string, int, error) { + if val, ok := a.pinmap[id]; ok { return "", val.pin, nil } return "", -1, fmt.Errorf("'%s' is not a valid id for a digital pin", id) } -func (c *Adaptor) translatePWMPin(id string) (string, int, error) { - sysPin, ok := c.pinmap[id] +func (a *Adaptor) translatePWMPin(id string) (string, int, error) { + sysPin, ok := a.pinmap[id] if !ok { return "", -1, fmt.Errorf("'%s' is not a valid id for a pin", id) } diff --git a/platforms/upboard/up2/adaptor_test.go b/platforms/upboard/up2/adaptor_test.go index 610af5b2c..6993bb5b1 100644 --- a/platforms/upboard/up2/adaptor_test.go +++ b/platforms/upboard/up2/adaptor_test.go @@ -2,6 +2,7 @@ package up2 import ( "fmt" + "strconv" "strings" "testing" @@ -12,6 +13,7 @@ import ( "gobot.io/x/gobot/v2/drivers/gpio" "gobot.io/x/gobot/v2/drivers/i2c" "gobot.io/x/gobot/v2/drivers/spi" + "gobot.io/x/gobot/v2/platforms/adaptors" "gobot.io/x/gobot/v2/system" ) @@ -36,8 +38,6 @@ const ( pwmDutyCyclePath = pwmDir + "pwm0/duty_cycle" pwmPeriodPath = pwmDir + "pwm0/period" pwmPolarityPath = pwmDir + "pwm0/polarity" - - fiftyHzNano = "20000000" ) var pwmMockPaths = []string{ @@ -95,33 +95,45 @@ func TestDigitalIO(t *testing.T) { require.NoError(t, a.Finalize()) } -func TestPWM(t *testing.T) { +func TestPWMWrite(t *testing.T) { + // arrange a, fs := initTestAdaptorWithMockedFilesystem(pwmMockPaths) fs.Files[pwmDutyCyclePath].Contents = "0" fs.Files[pwmPeriodPath].Contents = "0" - + // act err := a.PwmWrite("32", 100) + // assert require.NoError(t, err) - assert.Equal(t, "0", fs.Files[pwmExportPath].Contents) assert.Equal(t, "1", fs.Files[pwmEnablePath].Contents) assert.Equal(t, "3921568", fs.Files[pwmDutyCyclePath].Contents) assert.Equal(t, "10000000", fs.Files[pwmPeriodPath].Contents) // pwmPeriodDefault assert.Equal(t, "normal", fs.Files[pwmPolarityPath].Contents) - // prepare 50Hz for servos - fs.Files[pwmPeriodPath].Contents = fiftyHzNano - err = a.ServoWrite("32", 0) - require.NoError(t, err) + require.NoError(t, a.Finalize()) +} +func TestServoWrite(t *testing.T) { + // arrange: prepare 50Hz for servos + const ( + pin = "32" + fiftyHzNano = 20000000 + ) + a := NewAdaptor(adaptors.WithPWMDefaultPeriodForPin(pin, fiftyHzNano)) + fs := a.sys.UseMockFilesystem(pwmMockPaths) + require.NoError(t, a.Connect()) + fs.Files[pwmDutyCyclePath].Contents = "0" + fs.Files[pwmPeriodPath].Contents = "0" + // act & assert for 0° (min default value) + err := a.ServoWrite(pin, 0) + require.NoError(t, err) + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "500000", fs.Files[pwmDutyCyclePath].Contents) - assert.Equal(t, fiftyHzNano, fs.Files[pwmPeriodPath].Contents) - - err = a.ServoWrite("32", 180) + // act & assert for 180° (max default value) + err = a.ServoWrite(pin, 180) require.NoError(t, err) - + assert.Equal(t, strconv.Itoa(fiftyHzNano), fs.Files[pwmPeriodPath].Contents) assert.Equal(t, "2500000", fs.Files[pwmDutyCyclePath].Contents) - assert.Equal(t, fiftyHzNano, fs.Files[pwmPeriodPath].Contents) require.NoError(t, a.Finalize()) }