From 9ce45c005602ecd80eaff83d0c2d7b882fd02b76 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Sat, 28 Jan 2023 12:22:32 +0100 Subject: [PATCH] add advanced digital pin options (pull, bias, drive, debounce, event) --- adaptor.go | 19 +- examples/raspi_direct_pin.go | 78 +++++ examples/raspi_direct_pin_event.go | 97 ++++++ examples/tinkerboard_direct_pin.go | 39 ++- platforms/adaptors/digitalpinsadaptor.go | 226 +++++++++++++- platforms/adaptors/digitalpinsadaptor_test.go | 92 +++++- platforms/intel-iot/edison/edison_adaptor.go | 24 +- platforms/raspi/raspi_adaptor.go | 5 + platforms/tinkerboard/adaptor.go | 4 + platforms/tinkerboard/pin_map.go | 12 +- system/digitalpin_access_test.go | 8 +- system/digitalpin_config.go | 167 +++++++++- system/digitalpin_config_test.go | 295 +++++++++++++++++- system/digitalpin_gpiod.go | 142 +++++++-- system/digitalpin_gpiod_test.go | 203 ++++++++++-- system/digitalpin_sysfs.go | 45 ++- system/digitalpin_sysfs_test.go | 2 +- system/spi_gpio.go | 8 +- 18 files changed, 1344 insertions(+), 122 deletions(-) create mode 100644 examples/raspi_direct_pin.go create mode 100644 examples/raspi_direct_pin_event.go diff --git a/adaptor.go b/adaptor.go index 4b24b70bd..159499537 100644 --- a/adaptor.go +++ b/adaptor.go @@ -1,6 +1,9 @@ package gobot -import "io" +import ( + "io" + "time" +) // DigitalPinOptioner is the interface to provide the possibility to change pin behavior for the next usage type DigitalPinOptioner interface { @@ -10,6 +13,20 @@ type DigitalPinOptioner interface { SetDirectionOutput(initialState int) (changed bool) // SetDirectionInput sets the pins direction to input SetDirectionInput() (changed bool) + // SetActiveLow initializes the pin with inverse reaction (applies on input and output). + SetActiveLow() (changed bool) + // SetBias initializes the pin with the given bias (applies on input and output). + SetBias(bias int) (changed bool) + // SetDrive initializes the output pin with the given drive option. + SetDrive(drive int) (changed bool) + // SetDebounce initializes the input pin with the given debounce period. + SetDebounce(period time.Duration) (changed bool) + // SetEventHandlerForEdge initializes the input pin for edge detection and to call the event handler on specified edge. + // lineOffset is within the GPIO chip (needs to transformed to the pin id), timestamp is the detection time, + // detectedEdge contains the direction of the pin changes, seqno is the sequence number for this event in the sequence + // of events for all the lines in this line request, lseqno is the same but for this line + SetEventHandlerForEdge(handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, + lseqno uint32), edge int) (changed bool) } // DigitalPinOptionApplier is the interface to apply options to change pin behavior immediately diff --git a/examples/raspi_direct_pin.go b/examples/raspi_direct_pin.go new file mode 100644 index 000000000..968e6e199 --- /dev/null +++ b/examples/raspi_direct_pin.go @@ -0,0 +1,78 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/gpio" + "gobot.io/x/gobot/platforms/adaptors" + "gobot.io/x/gobot/platforms/raspi" +) + +// Wiring +// PWR Raspi: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) +// GPIO Raspi: header pin 21 (GPIO9) is input, pin 24 (GPIO8) is normal output, pin 26 (GPIO7) is inverted output +// Button: the input pin is wired with a button to GND, the internal pull up resistor is used +// LED's: the output pins are wired to the cathode of a LED, the anode is wired with a resistor (70-130Ohm for 20mA) to VCC +// Expected behavior: always one LED is on, the other in opposite state, if button is pressed for >2 seconds the state changes +func main() { + const ( + inPinNum = "21" + outPinNum = "24" + outPinInvertedNum = "26" + ) + // note: WithGpiosOpenDrain() is optional, if using WithGpiosOpenSource() the LED's will not light up + board := raspi.NewAdaptor(adaptors.WithGpiosActiveLow(outPinInvertedNum), + adaptors.WithGpiosOpenDrain(outPinNum, outPinInvertedNum), + adaptors.WithGpiosPullUp(inPinNum), + adaptors.WithGpioDebounce(inPinNum, 2*time.Second)) + + inPin := gpio.NewDirectPinDriver(board, inPinNum) + outPin := gpio.NewDirectPinDriver(board, outPinNum) + outPinInverted := gpio.NewDirectPinDriver(board, outPinInvertedNum) + + work := func() { + level := byte(1) + + gobot.Every(500*time.Millisecond, func() { + read, err := inPin.DigitalRead() + fmt.Printf("pin %s state is %d\n", inPinNum, read) + if err != nil { + fmt.Println(err) + } else { + level = byte(read) + } + + err = outPin.DigitalWrite(level) + fmt.Printf("pin %s is now %d\n", outPinNum, level) + if err != nil { + fmt.Println(err) + } + + err = outPinInverted.DigitalWrite(level) + fmt.Printf("pin %s is now not %d\n", outPinInvertedNum, level) + if err != nil { + fmt.Println(err) + } + + if level == 1 { + level = 0 + } else { + level = 1 + } + }) + } + + robot := gobot.NewRobot("pinBot", + []gobot.Connection{board}, + []gobot.Device{inPin, outPin, outPinInverted}, + work, + ) + + robot.Start() +} diff --git a/examples/raspi_direct_pin_event.go b/examples/raspi_direct_pin_event.go new file mode 100644 index 000000000..b2d469adc --- /dev/null +++ b/examples/raspi_direct_pin_event.go @@ -0,0 +1,97 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/platforms/raspi" + "gobot.io/x/gobot/system" +) + +const ( + inPinNum = "21" + outPinNum = "24" + outPinInvertedNum = "26" + debounceTime = 2 * time.Second +) + +var ( + outPin gobot.DigitalPinner + outPinInverted gobot.DigitalPinner +) + +// Wiring +// PWR Raspi: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) +// GPIO Raspi: header pin 21 (GPIO9) is input, pin 24 (GPIO8) is normal output, pin 26 (GPIO7) is inverted output +// Button: the input pin is wired with a button to GND, the internal pull up resistor is used +// LED's: the output pins are wired to the cathode of a LED, the anode is wired with a resistor (70-130Ohm for 20mA) to VCC +// Expected behavior: always one LED is on, the other in opposite state, if button is pressed for >2 seconds the state changes +func main() { + + board := raspi.NewAdaptor() + + work := func() { + inPin, err := board.DigitalPin(inPinNum) + if err != nil { + fmt.Println(err) + } + if err := inPin.ApplyOptions(system.WithPinDirectionInput(), system.WithPinPullUp(), + system.WithPinDebounce(debounceTime), system.WithPinEventOnBothEdges(buttonEventHandler)); err != nil { + fmt.Println(err) + } + + // note: WithPinOpenDrain() is optional, if using WithPinOpenSource() the LED's will not light up + outPin, err = board.DigitalPin(outPinNum) + if err != nil { + fmt.Println(err) + } + if err := outPin.ApplyOptions(system.WithPinDirectionOutput(1), system.WithPinOpenDrain()); err != nil { + fmt.Println(err) + } + + outPinInverted, err = board.DigitalPin(outPinInvertedNum) + if err != nil { + fmt.Println(err) + } + if err := outPinInverted.ApplyOptions(system.WithPinActiveLow(), system.WithPinDirectionOutput(1), + system.WithPinOpenDrain()); err != nil { + fmt.Println(err) + } + + fmt.Printf("\nPlease press and hold the button for at least %s\n", debounceTime) + } + + robot := gobot.NewRobot("pinEdgeBot", + []gobot.Connection{board}, + []gobot.Device{}, + work, + ) + + robot.Start() +} + +func buttonEventHandler(offset int, t time.Duration, et string, sn uint32, lsn uint32) { + fmt.Printf("%s: %s detected on line %d with total sequence %d and line sequence %d\n", t, et, offset, sn, lsn) + level := 1 + + if et == "falling edge" { + level = 0 + } + + err := outPin.Write(level) + fmt.Printf("pin %s is now %d\n", outPinNum, level) + if err != nil { + fmt.Println(err) + } + + err = outPinInverted.Write(level) + fmt.Printf("pin %s is now not %d\n", outPinInvertedNum, level) + if err != nil { + fmt.Println(err) + } +} diff --git a/examples/tinkerboard_direct_pin.go b/examples/tinkerboard_direct_pin.go index a716ea4c0..b37999cac 100644 --- a/examples/tinkerboard_direct_pin.go +++ b/examples/tinkerboard_direct_pin.go @@ -10,26 +10,51 @@ import ( "gobot.io/x/gobot" "gobot.io/x/gobot/drivers/gpio" + "gobot.io/x/gobot/platforms/adaptors" "gobot.io/x/gobot/platforms/tinkerboard" ) // Wiring // PWR Tinkerboard: 1 (+3.3V, VCC), 2(+5V), 6, 9, 14, 20 (GND) -// GPIO Tinkerboard: header pin 26 used as output +// GPIO Tinkerboard: header pin 21 is input, pin 24 used as normal output, pin 26 used as inverted output +// Button: the input pin is wired with a button to GND, an external pull up resistor is needed (e.g. 1K) +// LED's: the output pins are wired to the cathode of a LED, the anode is wired with a resistor (70-130Ohm for 20mA) to VCC +// Expected behavior: always one LED is on, the other in opposite state, on button press the state changes func main() { - const pinNo = "26" - board := tinkerboard.NewAdaptor() - pin := gpio.NewDirectPinDriver(board, pinNo) + const ( + inPinNum = "21" + outPinNum = "24" + outPinInvertedNum = "26" + ) + board := tinkerboard.NewAdaptor(adaptors.WithGpiosActiveLow(outPinInvertedNum)) + inPin := gpio.NewDirectPinDriver(board, inPinNum) + outPin := gpio.NewDirectPinDriver(board, outPinNum) + outPinInverted := gpio.NewDirectPinDriver(board, outPinInvertedNum) work := func() { level := byte(1) gobot.Every(500*time.Millisecond, func() { - err := pin.DigitalWrite(level) - fmt.Printf("pin %s is now %d\n", pinNo, level) + read, err := inPin.DigitalRead() + fmt.Printf("pin %s state is %d\n", inPinNum, read) + if err != nil { + fmt.Println(err) + } else { + level = byte(read) + } + + err = outPin.DigitalWrite(level) + fmt.Printf("pin %s is now %d\n", outPinNum, level) if err != nil { fmt.Println(err) } + + err = outPinInverted.DigitalWrite(level) + fmt.Printf("pin %s is now not %d\n", outPinInvertedNum, level) + if err != nil { + fmt.Println(err) + } + if level == 1 { level = 0 } else { @@ -41,7 +66,7 @@ func main() { robot := gobot.NewRobot("pinBot", []gobot.Connection{board}, - []gobot.Device{pin}, + []gobot.Device{inPin, outPin, outPinInverted}, work, ) diff --git a/platforms/adaptors/digitalpinsadaptor.go b/platforms/adaptors/digitalpinsadaptor.go index 86785b6bb..75d57caa5 100644 --- a/platforms/adaptors/digitalpinsadaptor.go +++ b/platforms/adaptors/digitalpinsadaptor.go @@ -3,6 +3,7 @@ package adaptors import ( "fmt" "sync" + "time" "github.com/hashicorp/go-multierror" "gobot.io/x/gobot" @@ -16,6 +17,18 @@ type digitalPinsOptioner interface { setDigitalPinInitializer(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)) } // DigitalPinsAdaptor is a adaptor for digital pins, normally used for composition in platforms. @@ -24,6 +37,7 @@ type DigitalPinsAdaptor struct { translate digitalPinTranslator initialize digitalPinInitializer pins map[string]gobot.DigitalPinner + pinOptions map[string][]func(gobot.DigitalPinOptioner) bool mutex sync.Mutex } @@ -75,6 +89,108 @@ func WithSpiGpioAccess(sclkPin, nssPin, mosiPin, misoPin string) func(Optioner) } } +// 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...) + } + } +} + +// 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...) + } + } +} + +// 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...) + } + } +} + +// 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...) + } + } +} + +// 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...) + } + } +} + +// 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) + } + } +} + +// WithGpioEventOnFallingEdge prepares the given input pin to be generate an event on falling edge. +// 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) + } + } +} + +// WithGpioEventOnRisingEdge prepares the given input pin to be generate an event on rising edge. +// 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) + } + } +} + +// WithGpioEventOnBothEdges prepares the given input pin to be generate an event on rising and falling edges. +// 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) + } + } +} + // Connect prepare new connection to digital pins. func (a *DigitalPinsAdaptor) Connect() error { a.mutex.Lock() @@ -97,6 +213,7 @@ func (a *DigitalPinsAdaptor) Finalize() (err error) { } } a.pins = nil + a.pinOptions = nil return } @@ -114,7 +231,7 @@ func (a *DigitalPinsAdaptor) DigitalRead(id string) (int, error) { a.mutex.Lock() defer a.mutex.Unlock() - pin, err := a.digitalPin(id, system.WithDirectionInput()) + pin, err := a.digitalPin(id, system.WithPinDirectionInput()) if err != nil { return 0, err } @@ -126,7 +243,7 @@ func (a *DigitalPinsAdaptor) DigitalWrite(id string, val byte) error { a.mutex.Lock() defer a.mutex.Unlock() - pin, err := a.digitalPin(id, system.WithDirectionOutput(int(val))) + pin, err := a.digitalPin(id, system.WithPinDirectionOutput(int(val))) if err != nil { return err } @@ -145,11 +262,112 @@ func (a *DigitalPinsAdaptor) setDigitalPinsForSystemSpi(sclkPin, nssPin, mosiPin system.WithSpiGpioAccess(a, sclkPin, nssPin, mosiPin, misoPin)(a.sys) } -func (a *DigitalPinsAdaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) { +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) digitalPin(id string, opts ...func(gobot.DigitalPinOptioner) bool) (gobot.DigitalPinner, error) { if a.pins == nil { - return nil, fmt.Errorf("not connected") + return nil, fmt.Errorf("not connected for pin %s", id) } + o := append(a.pinOptions[id], opts...) pin := a.pins[id] if pin == nil { diff --git a/platforms/adaptors/digitalpinsadaptor_test.go b/platforms/adaptors/digitalpinsadaptor_test.go index 7c2e6769a..29533c1bd 100644 --- a/platforms/adaptors/digitalpinsadaptor_test.go +++ b/platforms/adaptors/digitalpinsadaptor_test.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "testing" + "time" "runtime" "strconv" @@ -40,6 +41,18 @@ 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 + gobottest.Assert(t, len(a.pinOptions), 3) +} + func TestDigitalPinsConnect(t *testing.T) { translate := func(pin string) (chip string, line int, err error) { return } sys := system.NewAccesser() @@ -48,10 +61,10 @@ func TestDigitalPinsConnect(t *testing.T) { gobottest.Assert(t, a.pins, (map[string]gobot.DigitalPinner)(nil)) _, err := a.DigitalRead("13") - gobottest.Assert(t, err.Error(), "not connected") + gobottest.Assert(t, err.Error(), "not connected for pin 13") err = a.DigitalWrite("7", 1) - gobottest.Assert(t, err.Error(), "not connected") + gobottest.Assert(t, err.Error(), "not connected for pin 7") err = a.Connect() gobottest.Assert(t, err, nil) @@ -129,6 +142,7 @@ func TestDigitalIO(t *testing.T) { } func TestDigitalRead(t *testing.T) { + // arrange mockedPaths := []string{ "/sys/class/gpio/export", "/sys/class/gpio/unexport", @@ -138,20 +152,58 @@ func TestDigitalRead(t *testing.T) { a, fs := initTestDigitalPinsAdaptorWithMockedFilesystem(mockedPaths) fs.Files["/sys/class/gpio/gpio24/value"].Contents = "1" + // assert read correct value without error i, err := a.DigitalRead("13") gobottest.Assert(t, err, nil) gobottest.Assert(t, i, 1) + // assert error bubbling for read errors fs.WithReadError = true _, err = a.DigitalRead("13") gobottest.Assert(t, err, errors.New("read error")) + // assert error bubbling for write errors fs.WithWriteError = true _, err = a.DigitalRead("7") gobottest.Assert(t, err, errors.New("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 + gobottest.Assert(t, err1, nil) + gobottest.Assert(t, err2, nil) + gobottest.Assert(t, got1, 1) // there is no mechanism to negate mocked values + gobottest.Assert(t, got2, 0) + gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio25/active_low"].Contents, "1") + gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio26/active_low"].Contents, "1") +} + func TestDigitalWrite(t *testing.T) { + // arrange mockedPaths := []string{ "/sys/class/gpio/export", "/sys/class/gpio/unexport", @@ -160,20 +212,51 @@ func TestDigitalWrite(t *testing.T) { } 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) err := a.DigitalWrite("7", 1) gobottest.Assert(t, err, nil) gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio18/value"].Contents, "1") + // 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) gobottest.Assert(t, err, nil) + // assert error on bad id gobottest.Assert(t, a.DigitalWrite("notexist", 1), errors.New("not a valid pin")) + // assert error bubbling fs.WithWriteError = true err = a.DigitalWrite("7", 0) gobottest.Assert(t, err, errors.New("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 + gobottest.Assert(t, err, nil) + gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio19/value"].Contents, "2") + gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio19/active_low"].Contents, "1") +} + func TestDigitalPinConcurrency(t *testing.T) { oldProcs := runtime.GOMAXPROCS(0) runtime.GOMAXPROCS(8) @@ -200,3 +283,8 @@ 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/intel-iot/edison/edison_adaptor.go b/platforms/intel-iot/edison/edison_adaptor.go index 8cc4d32e0..99da035b8 100644 --- a/platforms/intel-iot/edison/edison_adaptor.go +++ b/platforms/intel-iot/edison/edison_adaptor.go @@ -137,7 +137,7 @@ func (c *Adaptor) DigitalRead(pin string) (i int, err error) { c.mutex.Lock() defer c.mutex.Unlock() - sysPin, err := c.digitalPin(pin, system.WithDirectionInput()) + sysPin, err := c.digitalPin(pin, system.WithPinDirectionInput()) if err != nil { return } @@ -198,20 +198,20 @@ func (c *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.WithDirectionOutput(system.LOW)) + tpin, err := c.newExportedDigitalPin(214, system.WithPinDirectionOutput(system.LOW)) if err != nil { return err } c.tristate = tpin for _, i := range []int{263, 262} { - if err := c.newUnexportedDigitalPin(i, system.WithDirectionOutput(system.HIGH)); err != nil { + if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.HIGH)); err != nil { return err } } for _, i := range []int{240, 241, 242, 243} { - if err := c.newUnexportedDigitalPin(i, system.WithDirectionOutput(system.LOW)); err != nil { + if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { return err } } @@ -241,13 +241,13 @@ func (c *Adaptor) arduinoI2CSetup() error { } for _, i := range []int{14, 165, 212, 213} { - if err := c.newUnexportedDigitalPin(i, system.WithDirectionInput()); err != nil { + if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionInput()); err != nil { return err } } for _, i := range []int{236, 237, 204, 205} { - if err := c.newUnexportedDigitalPin(i, system.WithDirectionOutput(system.LOW)); err != nil { + if err := c.newUnexportedDigitalPin(i, system.WithPinDirectionOutput(system.LOW)); err != nil { return err } } @@ -291,9 +291,9 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool } dir := vpin.DirectionBehavior() if i.resistor > 0 { - rop := system.WithDirectionOutput(system.LOW) + rop := system.WithPinDirectionOutput(system.LOW) if dir == system.OUT { - rop = system.WithDirectionInput() + rop = system.WithPinDirectionInput() } if err := c.ensureDigitalPin(i.resistor, rop); err != nil { return nil, err @@ -301,9 +301,9 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool } if i.levelShifter > 0 { - lop := system.WithDirectionOutput(system.LOW) + lop := system.WithPinDirectionOutput(system.LOW) if dir == system.OUT { - lop = system.WithDirectionOutput(system.HIGH) + lop = system.WithPinDirectionOutput(system.HIGH) } if err := c.ensureDigitalPin(i.levelShifter, lop); err != nil { return nil, err @@ -312,7 +312,7 @@ func (c *Adaptor) digitalPin(id string, o ...func(gobot.DigitalPinOptioner) bool if len(i.mux) > 0 { for _, mux := range i.mux { - if err := c.ensureDigitalPin(mux.pin, system.WithDirectionOutput(mux.value)); err != nil { + if err := c.ensureDigitalPin(mux.pin, system.WithPinDirectionOutput(mux.value)); err != nil { return nil, err } } @@ -388,7 +388,7 @@ func (c *Adaptor) changePinMode(pin, mode string) error { } func (c *Adaptor) digitalWrite(pin string, val byte) (err error) { - sysPin, err := c.digitalPin(pin, system.WithDirectionOutput(int(val))) + sysPin, err := c.digitalPin(pin, system.WithPinDirectionOutput(int(val))) if err != nil { return } diff --git a/platforms/raspi/raspi_adaptor.go b/platforms/raspi/raspi_adaptor.go index 8a2108346..9ab60c8e5 100644 --- a/platforms/raspi/raspi_adaptor.go +++ b/platforms/raspi/raspi_adaptor.go @@ -43,6 +43,11 @@ type Adaptor struct { // Optional parameters: // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs (still used by default) // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# +// adaptors.WithGpiosActiveLow(pin's): invert the pin behavior +// adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor +// adaptors.WithGpiosOpenDrain/Source(pin's): sets the output behavior +// adaptors.WithGpioDebounce(pin, period): sets the input debouncer +// adaptors.WithGpioEventOnFallingEdge/RaisingEdge/BothEdges(pin, handler): activate edge detection func NewAdaptor(opts ...func(adaptors.Optioner)) *Adaptor { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) c := &Adaptor{ diff --git a/platforms/tinkerboard/adaptor.go b/platforms/tinkerboard/adaptor.go index f1d91ef0f..5b41cf55a 100644 --- a/platforms/tinkerboard/adaptor.go +++ b/platforms/tinkerboard/adaptor.go @@ -46,6 +46,10 @@ type Adaptor struct { // Optional parameters: // adaptors.WithGpiodAccess(): use character device gpiod driver instead of sysfs (still used by default) // adaptors.WithSpiGpioAccess(sclk, nss, mosi, miso): use GPIO's instead of /dev/spidev#.# +// adaptors.WithGpiosActiveLow(pin's): invert the pin behavior +// adaptors.WithGpiosPullUp/Down(pin's): sets the internal pull resistor +// 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 { sys := system.NewAccesser(system.WithDigitalPinGpiodAccess()) c := &Adaptor{ diff --git a/platforms/tinkerboard/pin_map.go b/platforms/tinkerboard/pin_map.go index 7aaf13526..2039b182d 100644 --- a/platforms/tinkerboard/pin_map.go +++ b/platforms/tinkerboard/pin_map.go @@ -23,7 +23,7 @@ var gpioPinDefinitions = map[string]gpioPinDefinition{ "29": gpioPinDefinition{sysfs: 165, cdev: cdevPin{chip: 5, line: 13}}, // GPIO5_B5_SPI0CSN_UART4RTSN - NO, initial 0 "13": gpioPinDefinition{sysfs: 166, cdev: cdevPin{chip: 5, line: 14}}, // GPIO5_B6_SPI0_TXD_UART4TX - NO, initial 1 "15": gpioPinDefinition{sysfs: 167, cdev: cdevPin{chip: 5, line: 15}}, // GPIO5_B7_SPI0_RXD_UART4RX - IN, initial 1 - "31": gpioPinDefinition{sysfs: 168, cdev: cdevPin{chip: 5, line: 16}}, // GPIO5_C0_SPI0CSN1 - OK + "31": gpioPinDefinition{sysfs: 168, cdev: cdevPin{chip: 5, line: 16}}, // GPIO5_C0_SPI0CSN1 - OK if SPI0 off "22": gpioPinDefinition{sysfs: 171, cdev: cdevPin{chip: 5, line: 19}}, // GPIO5_C3 - OK "12": gpioPinDefinition{sysfs: 184, cdev: cdevPin{chip: 6, line: 0}}, // GPIO6_A0_PCM/I2S_CLK - NO, initial 1 "35": gpioPinDefinition{sysfs: 185, cdev: cdevPin{chip: 6, line: 1}}, // GPIO6_A1_PCM/I2S_FS - NO, initial 0 @@ -35,13 +35,13 @@ var gpioPinDefinitions = map[string]gpioPinDefinition{ "28": gpioPinDefinition{sysfs: 234, cdev: cdevPin{chip: 7, line: 18}}, // GPIO7_C2_I2C_SCL - OK if I2C4 off "33": gpioPinDefinition{sysfs: 238, cdev: cdevPin{chip: 7, line: 22}}, // GPIO7_C6_UART2RX_PWM2 - IN, initial 1 "32": gpioPinDefinition{sysfs: 239, cdev: cdevPin{chip: 7, line: 23}}, // GPIO7_C7_UART2TX_PWM3 - NO, initial 1 - "26": gpioPinDefinition{sysfs: 251, cdev: cdevPin{chip: 8, line: 3}}, // GPIO8_A3_SPI2CSN1 - OK + "26": gpioPinDefinition{sysfs: 251, cdev: cdevPin{chip: 8, line: 3}}, // GPIO8_A3_SPI2CSN1 - OK if SPI2 off "3": gpioPinDefinition{sysfs: 252, cdev: cdevPin{chip: 8, line: 4}}, // GPIO8_A4_I2C1_SDA - OK if I2C1 off "5": gpioPinDefinition{sysfs: 253, cdev: cdevPin{chip: 8, line: 5}}, // GPIO8_A5_I2C1_SCL - OK if I2C1 off - "23": gpioPinDefinition{sysfs: 254, cdev: cdevPin{chip: 8, line: 6}}, // GPIO8_A6_SPI2CLK - OK - "24": gpioPinDefinition{sysfs: 255, cdev: cdevPin{chip: 8, line: 7}}, // GPIO8_A7_SPI2CSN0 - OK - "21": gpioPinDefinition{sysfs: 256, cdev: cdevPin{chip: 8, line: 8}}, // GPIO8_B0_SPI2RXD - OK - "19": gpioPinDefinition{sysfs: 257, cdev: cdevPin{chip: 8, line: 9}}, // GPIO8_B1_SPI2TXD - OK + "23": gpioPinDefinition{sysfs: 254, cdev: cdevPin{chip: 8, line: 6}}, // GPIO8_A6_SPI2CLK - OK if SPI2 off + "24": gpioPinDefinition{sysfs: 255, cdev: cdevPin{chip: 8, line: 7}}, // GPIO8_A7_SPI2CSN0 - OK if SPI2 off + "21": gpioPinDefinition{sysfs: 256, cdev: cdevPin{chip: 8, line: 8}}, // GPIO8_B0_SPI2RXD - OK if SPI2 off + "19": gpioPinDefinition{sysfs: 257, cdev: cdevPin{chip: 8, line: 9}}, // GPIO8_B1_SPI2TXD - OK if SPI2 off } var pwmPinDefinitions = map[string]pwmPinDefinition{ diff --git a/system/digitalpin_access_test.go b/system/digitalpin_access_test.go index af0e3ee61..2d026edb5 100644 --- a/system/digitalpin_access_test.go +++ b/system/digitalpin_access_test.go @@ -72,28 +72,28 @@ func Test_createAsGpiod(t *testing.T) { } func Test_createPinWithOptionsSysfs(t *testing.T) { - // This is a general test, that options are applied by using "create" with the WithLabel() option. + // This is a general test, that options are applied by using "create" with the WithPinLabel() option. // All other configuration options will be tested in tests for "digitalPinConfig". // // arrange const label = "my sysfs label" dpa := sysfsDigitalPinAccess{} // act - dp := dpa.createPin("", 9, WithLabel(label)) + dp := dpa.createPin("", 9, WithPinLabel(label)) dps := dp.(*digitalPinSysfs) // assert gobottest.Assert(t, dps.label, label) } func Test_createPinWithOptionsGpiod(t *testing.T) { - // This is a general test, that options are applied by using "create" with the WithLabel() option. + // This is a general test, that options are applied by using "create" with the WithPinLabel() option. // All other configuration options will be tested in tests for "digitalPinConfig". // // arrange const label = "my gpiod label" dpa := gpiodDigitalPinAccess{} // act - dp := dpa.createPin("", 19, WithLabel(label)) + dp := dpa.createPin("", 19, WithPinLabel(label)) dpg := dp.(*digitalPinGpiod) // assert gobottest.Assert(t, dpg.label, label) diff --git a/system/digitalpin_config.go b/system/digitalpin_config.go index 07176b9cd..bbf4d34e6 100644 --- a/system/digitalpin_config.go +++ b/system/digitalpin_config.go @@ -1,6 +1,8 @@ package system import ( + "time" + "gobot.io/x/gobot" ) @@ -15,10 +17,42 @@ const ( LOW = 0 ) +const ( + digitalPinBiasDefault = 0 // GPIO uses the hardware default + digitalPinBiasDisable = 1 // GPIO has pull disabled + digitalPinBiasPullDown = 2 // GPIO has pull up enabled + digitalPinBiasPullUp = 3 // GPIO has pull down enabled + + // open drain and open source allows the connection of output ports with the same mode (OR logic) + // * for open drain/collector pull up the ports with an external resistor/load + // * for open source/emitter pull down the ports with an external resistor/load + digitalPinDrivePushPull = 0 // the pin will be driven actively high and low (default) + digitalPinDriveOpenDrain = 1 // the pin will be driven active to low only + digitalPinDriveOpenSource = 2 // the pin will be driven active to high only + + digitalPinEventNone = 0 // no event will be triggered on any pin change (default) + digitalPinEventOnFallingEdge = 1 // an event will be triggered on changes from high to low state + digitalPinEventOnRisingEdge = 2 // an event will be triggered on changes from low to high state + digitalPinEventOnBothEdges = 3 // an event will be triggered on all changes +) + +const ( + // DigitalPinEventRisingEdge indicates an inactive to active event. + DigitalPinEventRisingEdge = "rising edge" + // DigitalPinEventFallingEdge indicates an active to inactive event. + DigitalPinEventFallingEdge = "falling edge" +) + type digitalPinConfig struct { - label string - direction string - outInitialState int + label string + direction string + outInitialState int + activeLow bool + bias int + drive int + debouncePeriod time.Duration + edge int + edgeEventHandler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32) } func newDigitalPinConfig(label string, options ...func(gobot.DigitalPinOptioner) bool) *digitalPinConfig { @@ -32,22 +66,78 @@ func newDigitalPinConfig(label string, options ...func(gobot.DigitalPinOptioner) return cfg } -// WithLabel use a pin label, which will replace the default label "gobotio#". -func WithLabel(label string) func(gobot.DigitalPinOptioner) bool { +// WithPinLabel use a pin label, which will replace the default label "gobotio#". +func WithPinLabel(label string) func(gobot.DigitalPinOptioner) bool { return func(d gobot.DigitalPinOptioner) bool { return d.SetLabel(label) } } -// WithDirectionOutput initializes the pin as output instead of the default "input". -func WithDirectionOutput(initial int) func(gobot.DigitalPinOptioner) bool { +// WithPinDirectionOutput initializes the pin as output instead of the default "input". +func WithPinDirectionOutput(initial int) func(gobot.DigitalPinOptioner) bool { return func(d gobot.DigitalPinOptioner) bool { return d.SetDirectionOutput(initial) } } -// WithDirectionInput initializes the pin as input. -func WithDirectionInput() func(gobot.DigitalPinOptioner) bool { +// WithPinDirectionInput initializes the pin as input. +func WithPinDirectionInput() func(gobot.DigitalPinOptioner) bool { return func(d gobot.DigitalPinOptioner) bool { return d.SetDirectionInput() } } -// SetLabel sets the label to use for next reconfigure. The function is intended to use by WithLabel(). +// WithPinActiveLow initializes the pin with inverse reaction (applies on input and output). +func WithPinActiveLow() func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetActiveLow() } +} + +// WithPinPullDown initializes the pin to be pulled down (high impedance to GND, applies on input and output). +// This is working since Kernel 5.5. +func WithPinPullDown() func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetBias(digitalPinBiasPullDown) } +} + +// WithPinPullUp initializes the pin to be pulled up (high impedance to VDD, applies on input and output). +// This is working since Kernel 5.5. +func WithPinPullUp() func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetBias(digitalPinBiasPullUp) } +} + +// WithPinOpenDrain initializes the output pin to be driven with open drain/collector. +func WithPinOpenDrain() func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetDrive(digitalPinDriveOpenDrain) } +} + +// WithPinOpenSource initializes the output pin to be driven with open source/emitter. +func WithPinOpenSource() func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetDrive(digitalPinDriveOpenSource) } +} + +// WithPinDebounce initializes the input pin to be debounced. +func WithPinDebounce(period time.Duration) func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { return d.SetDebounce(period) } +} + +// WithPinEventOnFallingEdge initializes the input pin for edge detection and call the event handler on falling edges. +func WithPinEventOnFallingEdge(handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, + lseqno uint32)) func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { + return d.SetEventHandlerForEdge(handler, digitalPinEventOnFallingEdge) + } +} + +// WithPinEventOnRisingEdge initializes the input pin for edge detection and call the event handler on rising edges. +func WithPinEventOnRisingEdge(handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, + lseqno uint32)) func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { + return d.SetEventHandlerForEdge(handler, digitalPinEventOnRisingEdge) + } +} + +// WithPinEventOnBothEdges initializes the input pin for edge detection and call the event handler on all edges. +func WithPinEventOnBothEdges(handler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, + lseqno uint32)) func(gobot.DigitalPinOptioner) bool { + return func(d gobot.DigitalPinOptioner) bool { + return d.SetEventHandlerForEdge(handler, digitalPinEventOnBothEdges) + } +} + +// SetLabel sets the label to use for next reconfigure. The function is intended to use by WithPinLabel(). func (d *digitalPinConfig) SetLabel(label string) bool { if d.label == label { return false @@ -56,7 +146,8 @@ func (d *digitalPinConfig) SetLabel(label string) bool { return true } -// SetDirectionOutput sets the direction to output for next reconfigure. The function is intended to use by WithLabel(). +// SetDirectionOutput sets the direction to output for next reconfigure. The function is intended to use +// by WithPinDirectionOutput(). func (d *digitalPinConfig) SetDirectionOutput(initial int) bool { if d.direction == OUT { // in this case also the initial value will not be written @@ -67,7 +158,8 @@ func (d *digitalPinConfig) SetDirectionOutput(initial int) bool { return true } -// SetDirectionInput sets the direction to input for next reconfigure. The function is intended to use by WithLabel(). +// SetDirectionInput sets the direction to input for next reconfigure. The function is intended to use +// by WithPinDirectionInput(). func (d *digitalPinConfig) SetDirectionInput() bool { if d.direction == IN { return false @@ -75,3 +167,54 @@ func (d *digitalPinConfig) SetDirectionInput() bool { d.direction = IN return true } + +// SetActiveLow sets the pin with inverse reaction (applies on input and output) for next reconfigure. The function +// is intended to use by WithPinActiveLow(). +func (d *digitalPinConfig) SetActiveLow() bool { + if d.activeLow { + return false + } + d.activeLow = true + return true +} + +// SetBias sets the pin bias (applies on input and output) for next reconfigure. The function +// is intended to use by WithPinPullUp() and WithPinPullDown(). +func (d *digitalPinConfig) SetBias(bias int) bool { + if d.bias == bias { + return false + } + d.bias = bias + return true +} + +// SetDrive sets the pin drive mode (applies on output only) for next reconfigure. The function +// is intended to use by WithPinOpenDrain(), WithPinOpenSource() and WithPinPushPull(). +func (d *digitalPinConfig) SetDrive(drive int) bool { + if d.drive == drive { + return false + } + d.drive = drive + return true +} + +// SetDebounce sets the input pin with the given debounce period for next reconfigure. The function +// is intended to use by WithPinDebounce(). +func (d *digitalPinConfig) SetDebounce(period time.Duration) bool { + if d.debouncePeriod == period { + return false + } + d.debouncePeriod = period + return true +} + +// SetEventHandlerForEdge sets the input pin to edge detection and to call the event handler on specified edge. The +// function is intended to use by WithPinEventOnFallingEdge(), WithPinEventOnRisingEdge() and WithPinEventOnBothEdges(). +func (d *digitalPinConfig) SetEventHandlerForEdge(handler func(int, time.Duration, string, uint32, uint32), edge int) bool { + if d.edge == edge { + return false + } + d.edge = edge + d.edgeEventHandler = handler + return true +} diff --git a/system/digitalpin_config_test.go b/system/digitalpin_config_test.go index 45b144575..9d0d437f7 100644 --- a/system/digitalpin_config_test.go +++ b/system/digitalpin_config_test.go @@ -2,6 +2,7 @@ package system import ( "testing" + "time" "gobot.io/x/gobot" "gobot.io/x/gobot/gobottest" @@ -23,7 +24,17 @@ func Test_newDigitalPinConfig(t *testing.T) { gobottest.Assert(t, d.outInitialState, 0) } -func TestWithLabel(t *testing.T) { +func Test_newDigitalPinConfigWithOption(t *testing.T) { + // arrange + const label = "gobotio18" + // act + d := newDigitalPinConfig("not used", WithPinLabel(label)) + // assert + gobottest.Refute(t, d, nil) + gobottest.Assert(t, d.label, label) +} + +func TestWithPinLabel(t *testing.T) { const ( oldLabel = "old label" newLabel = "my optional label" @@ -45,7 +56,7 @@ func TestWithLabel(t *testing.T) { // arrange dpc := &digitalPinConfig{label: oldLabel} // act - got := WithLabel(tc.setLabel)(dpc) + got := WithPinLabel(tc.setLabel)(dpc) // assert gobottest.Assert(t, got, tc.want) gobottest.Assert(t, dpc.label, tc.setLabel) @@ -53,7 +64,7 @@ func TestWithLabel(t *testing.T) { } } -func TestWithDirectionOutput(t *testing.T) { +func TestWithPinDirectionOutput(t *testing.T) { const ( // values other than 0, 1 are normally not useful, just to test oldVal = 3 @@ -79,7 +90,7 @@ func TestWithDirectionOutput(t *testing.T) { // arrange dpc := &digitalPinConfig{direction: tc.oldDir, outInitialState: oldVal} // act - got := WithDirectionOutput(newVal)(dpc) + got := WithPinDirectionOutput(newVal)(dpc) // assert gobottest.Assert(t, got, tc.want) gobottest.Assert(t, dpc.direction, "out") @@ -88,7 +99,7 @@ func TestWithDirectionOutput(t *testing.T) { } } -func TestWithDirectionInput(t *testing.T) { +func TestWithPinDirectionInput(t *testing.T) { var tests = map[string]struct { oldDir string want bool @@ -107,7 +118,7 @@ func TestWithDirectionInput(t *testing.T) { const initValOut = 2 // 2 is normally not useful, just to test that is not touched dpc := &digitalPinConfig{direction: tc.oldDir, outInitialState: initValOut} // act - got := WithDirectionInput()(dpc) + got := WithPinDirectionInput()(dpc) // assert gobottest.Assert(t, got, tc.want) gobottest.Assert(t, dpc.direction, "in") @@ -115,3 +126,275 @@ func TestWithDirectionInput(t *testing.T) { }) } } + +func TestWithPinActiveLow(t *testing.T) { + var tests = map[string]struct { + oldActiveLow bool + want bool + }{ + "no_change": { + oldActiveLow: true, + }, + "change": { + oldActiveLow: false, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{activeLow: tc.oldActiveLow} + // act + got := WithPinActiveLow()(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.activeLow, true) + }) + } +} + +func TestWithPinPullDown(t *testing.T) { + var tests = map[string]struct { + oldBias int + want bool + wantVal int + }{ + "no_change": { + oldBias: digitalPinBiasPullDown, + }, + "change": { + oldBias: digitalPinBiasPullUp, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{bias: tc.oldBias} + // act + got := WithPinPullDown()(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.bias, digitalPinBiasPullDown) + }) + } +} + +func TestWithPinPullUp(t *testing.T) { + var tests = map[string]struct { + oldBias int + want bool + wantVal int + }{ + "no_change": { + oldBias: digitalPinBiasPullUp, + }, + "change": { + oldBias: digitalPinBiasPullDown, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{bias: tc.oldBias} + // act + got := WithPinPullUp()(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.bias, digitalPinBiasPullUp) + }) + } +} + +func TestWithPinOpenDrain(t *testing.T) { + var tests = map[string]struct { + oldDrive int + want bool + wantVal int + }{ + "no_change": { + oldDrive: digitalPinDriveOpenDrain, + }, + "change_from_pushpull": { + oldDrive: digitalPinDrivePushPull, + want: true, + }, + "change_from_opensource": { + oldDrive: digitalPinDriveOpenSource, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{drive: tc.oldDrive} + // act + got := WithPinOpenDrain()(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.drive, digitalPinDriveOpenDrain) + }) + } +} + +func TestWithPinOpenSource(t *testing.T) { + var tests = map[string]struct { + oldDrive int + want bool + wantVal int + }{ + "no_change": { + oldDrive: digitalPinDriveOpenSource, + }, + "change_from_pushpull": { + oldDrive: digitalPinDrivePushPull, + want: true, + }, + "change_from_opendrain": { + oldDrive: digitalPinDriveOpenDrain, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{drive: tc.oldDrive} + // act + got := WithPinOpenSource()(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.drive, digitalPinDriveOpenSource) + }) + } +} + +func TestWithPinDebounce(t *testing.T) { + const ( + oldVal = time.Duration(10) + newVal = time.Duration(14) + ) + var tests = map[string]struct { + oldDebouncePeriod time.Duration + want bool + wantVal time.Duration + }{ + "no_change": { + oldDebouncePeriod: newVal, + }, + "change": { + oldDebouncePeriod: oldVal, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{debouncePeriod: tc.oldDebouncePeriod} + // act + got := WithPinDebounce(newVal)(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.debouncePeriod, newVal) + }) + } +} + +func TestWithPinEventOnFallingEdge(t *testing.T) { + const ( + oldVal = digitalPinEventNone + newVal = digitalPinEventOnFallingEdge + ) + var tests = map[string]struct { + oldEdge int + want bool + wantVal int + }{ + "no_change": { + oldEdge: newVal, + }, + "change": { + oldEdge: oldVal, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{edge: tc.oldEdge} + handler := func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32) {} + // act + got := WithPinEventOnFallingEdge(handler)(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.edge, newVal) + gobottest.Refute(t, dpc.edgeEventHandler, nil) + }) + } +} + +func TestWithPinEventOnRisingEdge(t *testing.T) { + const ( + oldVal = digitalPinEventNone + newVal = digitalPinEventOnRisingEdge + ) + var tests = map[string]struct { + oldEdge int + want bool + wantVal int + }{ + "no_change": { + oldEdge: newVal, + }, + "change": { + oldEdge: oldVal, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{edge: tc.oldEdge} + handler := func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32) {} + // act + got := WithPinEventOnRisingEdge(handler)(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.edge, newVal) + gobottest.Refute(t, dpc.edgeEventHandler, nil) + }) + } +} + +func TestWithPinEventOnBothEdges(t *testing.T) { + const ( + oldVal = digitalPinEventNone + newVal = digitalPinEventOnBothEdges + ) + var tests = map[string]struct { + oldEdge int + want bool + wantVal int + }{ + "no_change": { + oldEdge: newVal, + }, + "change": { + oldEdge: oldVal, + want: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // arrange + dpc := &digitalPinConfig{edge: tc.oldEdge} + handler := func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32) {} + // act + got := WithPinEventOnBothEdges(handler)(dpc) + // assert + gobottest.Assert(t, got, tc.want) + gobottest.Assert(t, dpc.edge, newVal) + gobottest.Refute(t, dpc.edgeEventHandler, nil) + }) + } +} diff --git a/system/digitalpin_gpiod.go b/system/digitalpin_gpiod.go index d2ad5581c..2781be9f2 100644 --- a/system/digitalpin_gpiod.go +++ b/system/digitalpin_gpiod.go @@ -2,14 +2,16 @@ package system import ( "fmt" + "log" "strconv" "strings" + "time" "github.com/warthog618/gpiod" "gobot.io/x/gobot" ) -const systemGpiodDebug = true +const systemGpiodDebug = false type cdevLine interface { SetValue(value int) error @@ -24,23 +26,25 @@ type digitalPinGpiod struct { line cdevLine } -var used = map[bool]string{true: "used", false: "unused"} -var activeLow = map[bool]string{true: "low", false: "high"} -var debounced = map[bool]string{true: "debounced", false: "not debounced"} +var digitalPinGpiodReconfigure = digitalPinGpiodReconfigureLine // to allow unit testing -var direction = map[gpiod.LineDirection]string{gpiod.LineDirectionUnknown: "unknown direction", +var digitalPinGpiodUsed = map[bool]string{true: "used", false: "unused"} +var digitalPinGpiodActiveLow = map[bool]string{true: "low", false: "high"} +var digitalPinGpiodDebounced = map[bool]string{true: "debounced", false: "not debounced"} + +var digitalPinGpiodDirection = map[gpiod.LineDirection]string{gpiod.LineDirectionUnknown: "unknown direction", gpiod.LineDirectionInput: "input", gpiod.LineDirectionOutput: "output"} -var drive = map[gpiod.LineDrive]string{gpiod.LineDrivePushPull: "push-pull", gpiod.LineDriveOpenDrain: "open-drain", +var digitalPinGpiodDrive = map[gpiod.LineDrive]string{gpiod.LineDrivePushPull: "push-pull", gpiod.LineDriveOpenDrain: "open-drain", gpiod.LineDriveOpenSource: "open-source"} -var bias = map[gpiod.LineBias]string{gpiod.LineBiasUnknown: "unknown", gpiod.LineBiasDisabled: "disabled", +var digitalPinGpiodBias = map[gpiod.LineBias]string{gpiod.LineBiasUnknown: "unknown", gpiod.LineBiasDisabled: "disabled", gpiod.LineBiasPullUp: "pull-up", gpiod.LineBiasPullDown: "pull-down"} -var edgeDetect = map[gpiod.LineEdge]string{gpiod.LineEdgeNone: "no", gpiod.LineEdgeRising: "rising", +var digitalPinGpiodEdgeDetect = map[gpiod.LineEdge]string{gpiod.LineEdgeNone: "no", gpiod.LineEdgeRising: "rising", gpiod.LineEdgeFalling: "falling", gpiod.LineEdgeBoth: "both"} -var eventClock = map[gpiod.LineEventClock]string{gpiod.LineEventClockMonotonic: "monotonic", +var digitalPinGpiodEventClock = map[gpiod.LineEventClock]string{gpiod.LineEventClockMonotonic: "monotonic", gpiod.LineEventClockRealtime: "realtime"} // newDigitalPinGpiod returns a digital pin given the pin number, with the label "gobotio" followed by the pin number. @@ -62,10 +66,10 @@ func newDigitalPinGpiod(chipName string, pin int, options ...func(gobot.DigitalP func (d *digitalPinGpiod) ApplyOptions(options ...func(gobot.DigitalPinOptioner) bool) error { anyChange := false for _, option := range options { - anyChange = anyChange || option(d) + anyChange = option(d) || anyChange } if anyChange { - return d.reconfigure(false) + return digitalPinGpiodReconfigure(d, false) } return nil } @@ -78,7 +82,7 @@ func (d *digitalPinGpiod) DirectionBehavior() string { // Export sets the pin as used by this driver. Implements the interface gobot.DigitalPinner. func (d *digitalPinGpiod) Export() error { - err := d.reconfigure(false) + err := digitalPinGpiodReconfigure(d, false) if err != nil { return fmt.Errorf("gpiod.Export(): %v", err) } @@ -89,7 +93,7 @@ func (d *digitalPinGpiod) Export() error { func (d *digitalPinGpiod) Unexport() error { var errs []string if d.line != nil { - if err := d.reconfigure(true); err != nil { + if err := digitalPinGpiodReconfigure(d, true); err != nil { errs = append(errs, err.Error()) } if err := d.line.Close(); err != nil { @@ -139,7 +143,7 @@ func (d *digitalPinGpiod) ListLines() error { if err != nil { return err } - fmt.Println(fmtLine(li)) + fmt.Println(digitalPinGpiodFmtLine(li)) } return nil @@ -161,12 +165,12 @@ func (d *digitalPinGpiod) List() error { if err != nil { return err } - fmt.Println(fmtLine(li)) + fmt.Println(digitalPinGpiodFmtLine(li)) return nil } -func (d *digitalPinGpiod) reconfigure(forceInput bool) error { +func digitalPinGpiodReconfigureLine(d *digitalPinGpiod, forceInput bool) error { // cleanup old line if d.line != nil { d.line.Close() @@ -182,43 +186,113 @@ func (d *digitalPinGpiod) reconfigure(forceInput bool) error { } defer gpiodChip.Close() - // acquire line - gpiodLine, err := gpiodChip.RequestLine(d.pin) + // collect line configuration options + var opts []gpiod.LineReqOption + + // configure direction, debounce period (inputs only), edge detection (inputs only) and drive (outputs only) + if d.direction == IN || forceInput { + if systemGpiodDebug { + log.Printf("input (%s): debounce %s, edge %d, handler %t, inverse %t, bias %d", + id, d.debouncePeriod, d.edge, d.edgeEventHandler != nil, d.activeLow == true, d.bias) + } + opts = append(opts, gpiod.AsInput) + if !forceInput && d.drive != digitalPinDrivePushPull && systemGpiodDebug { + log.Printf("\n++ drive option (%d) is dropped for input++\n", d.drive) + } + if d.debouncePeriod != 0 { + opts = append(opts, gpiod.WithDebounce(d.debouncePeriod)) + } + // edge detection + if d.edgeEventHandler != nil { + wrappedHandler := digitalPinGpiodGetWrappedEventHandler(d.edgeEventHandler) + switch d.edge { + case digitalPinEventOnFallingEdge: + opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithFallingEdge) + case digitalPinEventOnRisingEdge: + opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithRisingEdge) + case digitalPinEventOnBothEdges: + opts = append(opts, gpiod.WithEventHandler(wrappedHandler), gpiod.WithBothEdges) + default: + opts = append(opts, gpiod.WithoutEdges) + } + } + } else { + if systemGpiodDebug { + log.Printf("ouput (%s): ini-state %d, drive %d, inverse %t, bias %d", + id, d.outInitialState, d.drive, d.activeLow == true, d.bias) + } + opts = append(opts, gpiod.AsOutput(d.outInitialState)) + switch d.drive { + case digitalPinDriveOpenDrain: + opts = append(opts, gpiod.AsOpenDrain) + case digitalPinDriveOpenSource: + opts = append(opts, gpiod.AsOpenSource) + default: + opts = append(opts, gpiod.AsPushPull) + } + if d.debouncePeriod != 0 && systemGpiodDebug { + log.Printf("\n++debounce option (%d) is dropped for output++\n", d.drive) + } + if d.edgeEventHandler != nil || d.edge != digitalPinEventNone && systemGpiodDebug { + log.Printf("\n++edge detection is dropped for output++\n") + } + } + + // configure inverse logic (inputs and outputs) + if d.activeLow { + opts = append(opts, gpiod.AsActiveLow) + } + + // configure bias (inputs and outputs) + switch d.bias { + case digitalPinBiasPullDown: + opts = append(opts, gpiod.WithPullDown) + case digitalPinBiasPullUp: + opts = append(opts, gpiod.WithPullUp) + default: + opts = append(opts, gpiod.WithBiasAsIs) + } + + // acquire line with collected options + gpiodLine, err := gpiodChip.RequestLine(d.pin, opts...) if err != nil { if gpiodLine != nil { gpiodLine.Close() } d.line = nil - return fmt.Errorf("gpiod.reconfigure(%s)-c.RequestLine(%d): %v", id, d.pin, err) + return fmt.Errorf("gpiod.reconfigure(%s)-c.RequestLine(%d, %v): %v", id, d.pin, opts, err) } d.line = gpiodLine - // configure line - if d.direction == IN || forceInput { - if err := gpiodLine.Reconfigure(gpiod.AsInput); err != nil { - return fmt.Errorf("gpiod.reconfigure(%s)-l.Reconfigure(gpiod.AsInput): %v", id, err) - } - return nil - } + return nil +} - if err := gpiodLine.Reconfigure(gpiod.AsOutput(d.outInitialState)); err != nil { - return fmt.Errorf("gpiod.reconfigure(%s)-l.Reconfigure(gpiod.AsOutput(%d)): %v", id, d.outInitialState, err) +func digitalPinGpiodGetWrappedEventHandler(handler func(int, time.Duration, string, uint32, uint32)) func(gpiod.LineEvent) { + return func(evt gpiod.LineEvent) { + detectedEdge := "none" + switch evt.Type { + case gpiod.LineEventRisingEdge: + detectedEdge = DigitalPinEventRisingEdge + case gpiod.LineEventFallingEdge: + detectedEdge = DigitalPinEventFallingEdge + } + handler(evt.Offset, evt.Timestamp, detectedEdge, evt.Seqno, evt.LineSeqno) } - return nil } -func fmtLine(li gpiod.LineInfo) string { +func digitalPinGpiodFmtLine(li gpiod.LineInfo) string { var consumer string if li.Consumer != "" { consumer = fmt.Sprintf(" by '%s'", li.Consumer) } return fmt.Sprintf("++ Info line %d '%s', %s%s ++\n Config: %s\n", - li.Offset, li.Name, used[li.Used], consumer, fmtLineConfig(li.Config)) + li.Offset, li.Name, digitalPinGpiodUsed[li.Used], consumer, digitalPinGpiodFmtLineConfig(li.Config)) } -func fmtLineConfig(cfg gpiod.LineConfig) string { +func digitalPinGpiodFmtLineConfig(cfg gpiod.LineConfig) string { t := "active-%s, %s, %s, %s bias, %s edge detect, %s, debounce-period: %v, %s event clock" - return fmt.Sprintf(t, activeLow[cfg.ActiveLow], direction[cfg.Direction], drive[cfg.Drive], bias[cfg.Bias], - edgeDetect[cfg.EdgeDetection], debounced[cfg.Debounced], cfg.DebouncePeriod, eventClock[cfg.EventClock]) + return fmt.Sprintf(t, digitalPinGpiodActiveLow[cfg.ActiveLow], digitalPinGpiodDirection[cfg.Direction], + digitalPinGpiodDrive[cfg.Drive], digitalPinGpiodBias[cfg.Bias], digitalPinGpiodEdgeDetect[cfg.EdgeDetection], + digitalPinGpiodDebounced[cfg.Debounced], cfg.DebouncePeriod, digitalPinGpiodEventClock[cfg.EventClock]) } diff --git a/system/digitalpin_gpiod_test.go b/system/digitalpin_gpiod_test.go index 12ed40db6..6e24c5a7b 100644 --- a/system/digitalpin_gpiod_test.go +++ b/system/digitalpin_gpiod_test.go @@ -33,38 +33,187 @@ func Test_newDigitalPinGpiod(t *testing.T) { } func Test_newDigitalPinGpiodWithOptions(t *testing.T) { - // This is a general test, that options are applied by using "newDigitalPinGpiod" with the WithLabel() option. + // This is a general test, that options are applied by using "newDigitalPinGpiod" with the WithPinLabel() option. // All other configuration options will be tested in tests for "digitalPinConfig". // // arrange const label = "my own label" // act - dp := newDigitalPinGpiod("", 9, WithLabel(label)) + dp := newDigitalPinGpiod("", 9, WithPinLabel(label)) // assert gobottest.Assert(t, dp.label, label) } func TestApplyOptions(t *testing.T) { - // currently the gpiod.Chip has no interface for RequestLine(), - // so we can only test without trigger of reconfigure - // arrange - d := &digitalPinGpiod{digitalPinConfig: &digitalPinConfig{direction: "in"}} - // act - d.ApplyOptions(WithDirectionInput()) - // assert - gobottest.Assert(t, d.digitalPinConfig.direction, "in") + var tests = map[string]struct { + changed []bool + simErr error + wantReconfigured int + wantErr error + }{ + "both_changed": { + changed: []bool{true, true}, + wantReconfigured: 1, + }, + "first_changed": { + changed: []bool{true, false}, + wantReconfigured: 1, + }, + "second_changed": { + changed: []bool{false, true}, + wantReconfigured: 1, + }, + "none_changed": { + changed: []bool{false, false}, + simErr: fmt.Errorf("error not raised"), + wantReconfigured: 0, + }, + "error_on_change": { + changed: []bool{false, true}, + simErr: fmt.Errorf("error raised"), + wantReconfigured: 1, + wantErr: fmt.Errorf("error raised"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // currently the gpiod.Chip has no interface for RequestLine(), + // so we can only test without trigger of real reconfigure + // arrange + orgReconf := digitalPinGpiodReconfigure + defer func() { digitalPinGpiodReconfigure = orgReconf }() + + inputForced := true + reconfigured := 0 + digitalPinGpiodReconfigure = func(d *digitalPinGpiod, forceInput bool) error { + inputForced = forceInput + reconfigured++ + return tc.simErr + } + d := &digitalPinGpiod{digitalPinConfig: &digitalPinConfig{direction: "in"}} + optionFunction1 := func(gobot.DigitalPinOptioner) bool { + d.digitalPinConfig.direction = "test" + return tc.changed[0] + } + optionFunction2 := func(gobot.DigitalPinOptioner) bool { + d.digitalPinConfig.drive = 15 + return tc.changed[1] + } + // act + err := d.ApplyOptions(optionFunction1, optionFunction2) + // assert + gobottest.Assert(t, err, tc.wantErr) + gobottest.Assert(t, d.digitalPinConfig.direction, "test") + gobottest.Assert(t, d.digitalPinConfig.drive, 15) + gobottest.Assert(t, reconfigured, tc.wantReconfigured) + if reconfigured > 0 { + gobottest.Assert(t, inputForced, false) + } + }) + } +} + +func TestExport(t *testing.T) { + var tests = map[string]struct { + simErr error + wantReconfigured int + wantErr error + }{ + "no_err": { + wantReconfigured: 1, + }, + "error": { + wantReconfigured: 1, + simErr: fmt.Errorf("reconfigure error"), + wantErr: fmt.Errorf("gpiod.Export(): reconfigure error"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // currently the gpiod.Chip has no interface for RequestLine(), + // so we can only test without trigger of real reconfigure + // arrange + orgReconf := digitalPinGpiodReconfigure + defer func() { digitalPinGpiodReconfigure = orgReconf }() + + inputForced := true + reconfigured := 0 + digitalPinGpiodReconfigure = func(d *digitalPinGpiod, forceInput bool) error { + inputForced = forceInput + reconfigured++ + return tc.simErr + } + d := &digitalPinGpiod{} + // act + err := d.Export() + // assert + gobottest.Assert(t, err, tc.wantErr) + gobottest.Assert(t, inputForced, false) + gobottest.Assert(t, reconfigured, tc.wantReconfigured) + }) + } } func TestUnexport(t *testing.T) { - // currently the gpiod.Chip has no interface for RequestLine(), - // so we can only test without trigger of reconfigure - // arrange - dp := newDigitalPinGpiod("", 4) - dp.line = nil // ensures no reconfigure - // act - err := dp.Unexport() - // assert - gobottest.Assert(t, err, nil) + var tests = map[string]struct { + simNoLine bool + simReconfErr error + simCloseErr error + wantReconfigured int + wantErr error + }{ + "no_line_no_err": { + simNoLine: true, + wantReconfigured: 0, + }, + "no_line_with_err": { + simNoLine: true, + simReconfErr: fmt.Errorf("reconfigure error"), + wantReconfigured: 0, + }, + "no_err": { + wantReconfigured: 1, + }, + "error_reconfigure": { + wantReconfigured: 1, + simReconfErr: fmt.Errorf("reconfigure error"), + wantErr: fmt.Errorf("reconfigure error"), + }, + "error_close": { + wantReconfigured: 1, + simCloseErr: fmt.Errorf("close error"), + wantErr: fmt.Errorf("gpiod.Unexport()-line.Close(): close error"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // currently the gpiod.Chip has no interface for RequestLine(), + // so we can only test without trigger of real reconfigure + // arrange + orgReconf := digitalPinGpiodReconfigure + defer func() { digitalPinGpiodReconfigure = orgReconf }() + + inputForced := false + reconfigured := 0 + digitalPinGpiodReconfigure = func(d *digitalPinGpiod, forceInput bool) error { + inputForced = forceInput + reconfigured++ + return tc.simReconfErr + } + dp := newDigitalPinGpiod("", 4) + if !tc.simNoLine { + dp.line = &lineMock{simCloseErr: tc.simCloseErr} + } + // act + err := dp.Unexport() + // assert + gobottest.Assert(t, err, tc.wantErr) + gobottest.Assert(t, reconfigured, tc.wantReconfigured) + if reconfigured > 0 { + gobottest.Assert(t, inputForced, true) + } + }) + } } func TestWrite(t *testing.T) { @@ -99,7 +248,7 @@ func TestWrite(t *testing.T) { t.Run(name, func(t *testing.T) { // arrange dp := newDigitalPinGpiod("", 4) - lm := &lineMock{lastVal: 10, simErr: tc.simErr} + lm := &lineMock{lastVal: 10, simSetErr: tc.simErr} dp.line = lm // act err := dp.Write(tc.val) @@ -134,7 +283,7 @@ func TestRead(t *testing.T) { t.Run(name, func(t *testing.T) { // arrange dp := newDigitalPinGpiod("", 4) - lm := &lineMock{lastVal: tc.simVal, simErr: tc.simErr} + lm := &lineMock{lastVal: tc.simVal, simValueErr: tc.simErr} dp.line = lm // act got, err := dp.Read() @@ -152,10 +301,12 @@ func TestRead(t *testing.T) { } type lineMock struct { - lastVal int - simErr error + lastVal int + simSetErr error + simValueErr error + simCloseErr error } -func (lm *lineMock) SetValue(value int) error { lm.lastVal = value; return lm.simErr } -func (lm *lineMock) Value() (int, error) { return lm.lastVal, lm.simErr } -func (*lineMock) Close() error { return nil } +func (lm *lineMock) SetValue(value int) error { lm.lastVal = value; return lm.simSetErr } +func (lm *lineMock) Value() (int, error) { return lm.lastVal, lm.simValueErr } +func (lm *lineMock) Close() error { return lm.simCloseErr } diff --git a/system/digitalpin_sysfs.go b/system/digitalpin_sysfs.go index 8c94c5386..da5119ecc 100644 --- a/system/digitalpin_sysfs.go +++ b/system/digitalpin_sysfs.go @@ -3,6 +3,7 @@ package system import ( "errors" "fmt" + "log" "os" "strconv" "syscall" @@ -12,6 +13,7 @@ import ( ) const ( + systemSysfsDebug = false // gpioPath default linux sysfs gpio path gpioPath = "/sys/class/gpio" ) @@ -24,8 +26,9 @@ type digitalPinSysfs struct { *digitalPinConfig fs filesystem - dirFile File - valFile File + dirFile File + valFile File + activeLowFile File } // newDigitalPinSysfs returns a digital pin using for the given number. The name of the sysfs file will prepend "gpio" @@ -80,6 +83,10 @@ func (d *digitalPinSysfs) Unexport() error { d.valFile.Close() d.valFile = nil } + if d.activeLowFile != nil { + d.activeLowFile.Close() + d.activeLowFile = nil + } _, err = writeFile(unexport, []byte(d.pin)) if err != nil { @@ -148,11 +155,43 @@ func (d *digitalPinSysfs) reconfigure() error { d.valFile, err = d.fs.openFile(fmt.Sprintf("%s/%s/value", gpioPath, d.label), os.O_RDWR, 0644) } - // configure line + // configure direction if err == nil { err = d.writeDirectionWithInitialOutput() } + // configure inverse logic + if err == nil { + if d.activeLow { + d.activeLowFile, err = d.fs.openFile(fmt.Sprintf("%s/%s/active_low", gpioPath, d.label), os.O_RDWR, 0644) + if err == nil { + _, err = writeFile(d.activeLowFile, []byte("1")) + } + } + } + + // configure bias (unsupported) + if err == nil { + if d.bias != digitalPinBiasDefault && systemSysfsDebug { + log.Printf("bias options (%d) are not supported by sysfs, please use hardware resistors instead\n", d.bias) + } + } + + // configure drive (unsupported) + if d.drive != digitalPinDrivePushPull && systemSysfsDebug { + log.Printf("drive options (%d) are not supported by sysfs\n", d.drive) + } + + // configure debounce (unsupported) + if d.debouncePeriod != 0 && systemSysfsDebug { + log.Printf("debounce period option (%d) is not supported by sysfs\n", d.debouncePeriod) + } + + // configure edge detection (not implemented) + if d.edge != 0 && systemSysfsDebug { + log.Printf("edge detect option (%d) is not implemented for sysfs\n", d.edge) + } + if err != nil { d.Unexport() } diff --git a/system/digitalpin_sysfs_test.go b/system/digitalpin_sysfs_test.go index 50286c664..8d88c5a74 100644 --- a/system/digitalpin_sysfs_test.go +++ b/system/digitalpin_sysfs_test.go @@ -47,7 +47,7 @@ func TestDigitalPin(t *testing.T) { gobottest.Assert(t, err, nil) gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio10/value"].Contents, "1") - err = pin.ApplyOptions(WithDirectionInput()) + err = pin.ApplyOptions(WithPinDirectionInput()) gobottest.Assert(t, err, nil) gobottest.Assert(t, fs.Files["/sys/class/gpio/gpio10/direction"].Contents, "in") diff --git a/system/spi_gpio.go b/system/spi_gpio.go index 0b6fdbc10..68ad91a8e 100644 --- a/system/spi_gpio.go +++ b/system/spi_gpio.go @@ -135,12 +135,12 @@ func (s *spiGpio) transferByte(txByte uint8) (uint8, error) { func (s *spiGpio) initializeGpios() error { var err error - // nss is an output, negotiated (currently not implemented at pin level) + // nss is an output, negated (currently not implemented at pin level) s.nssPin, err = s.cfg.pinProvider.DigitalPin(s.cfg.nssPinID) if err != nil { return err } - if err := s.nssPin.ApplyOptions(WithDirectionOutput(1)); err != nil { + if err := s.nssPin.ApplyOptions(WithPinDirectionOutput(1)); err != nil { return err } // sclk is an output, CPOL = 0 @@ -148,7 +148,7 @@ func (s *spiGpio) initializeGpios() error { if err != nil { return err } - if err := s.sclkPin.ApplyOptions(WithDirectionOutput(0)); err != nil { + if err := s.sclkPin.ApplyOptions(WithPinDirectionOutput(0)); err != nil { return err } // miso is an input @@ -161,5 +161,5 @@ func (s *spiGpio) initializeGpios() error { if err != nil { return err } - return s.mosiPin.ApplyOptions(WithDirectionOutput(0)) + return s.mosiPin.ApplyOptions(WithPinDirectionOutput(0)) }