Skip to content

Commit

Permalink
system(gpio): add edge polling function
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas committed Oct 25, 2023
1 parent ee4368b commit 0efa9fe
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 15 deletions.
6 changes: 6 additions & 0 deletions adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ type DigitalPinOptioner interface {
// 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)
// SetPollForEdgeDetection use a discrete input polling method to detect edges. A poll interval of zero or smaller will
// deactivate this function. Please note: Using this feature is CPU consuming and less accurate than using cdev event
// handler (gpiod implementation) and should be done only if the former is not implemented or not working for the
// adaptor. E.g. sysfs driver in gobot has not implemented edge detection yet. The function is only useful together with
// SetEventHandlerForEdge() and its corresponding With*() functions.
SetPollForEdgeDetection(pollInterval time.Duration, pollQuitChan chan struct{}) (changed bool)
}

// DigitalPinOptionApplier is the interface to apply options to change pin behavior immediately
Expand Down
22 changes: 22 additions & 0 deletions system/GPIO.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ Connect the input header pin26 to +3.3V with an resistor (e.g. 1kOhm).
1
```

### Test edge detection behavior of gpio251 (sysfs Tinkerboard)

investigate status:

```sh
# cat /sys/class/gpio/gpio251/edge
none
```

The file exists only if the pin can be configured as an interrupt generating input pin. To activate edge detection,
"rising", "falling", or "both" needs to be set.

```sh
# cat /sys/class/gpio/gpio251/value
1
```

If edge detection is activated, a poll will return only when the interrupt was triggered. The new value is written to
the beginning of the file.

> Not tested yet, not supported by gobot yet.
### Test output behavior of gpio251 (sysfs Tinkerboard)

Connect the output header pin26 to +3.3V with an resistor (e.g. 1kOhm leads to ~0.3mA, 300Ohm leads to ~10mA).
Expand Down
24 changes: 24 additions & 0 deletions system/digitalpin_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type digitalPinConfig struct {
debouncePeriod time.Duration
edge int
edgeEventHandler func(lineOffset int, timestamp time.Duration, detectedEdge string, seqno uint32, lseqno uint32)
pollInterval time.Duration
pollQuitChan chan struct{}
}

func newDigitalPinConfig(label string, options ...func(gobot.DigitalPinOptioner) bool) *digitalPinConfig {
Expand Down Expand Up @@ -140,6 +142,13 @@ func WithPinEventOnBothEdges(handler func(lineOffset int, timestamp time.Duratio
}
}

// WithPinPollForEdgeDetection initializes a discrete input pin polling function to use for edge detection.
func WithPinPollForEdgeDetection(pollInterval time.Duration, pollQuitChan chan struct{}) func(gobot.DigitalPinOptioner) bool {
return func(d gobot.DigitalPinOptioner) bool {
return d.SetPollForEdgeDetection(pollInterval, pollQuitChan)
}
}

// 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 {
Expand Down Expand Up @@ -221,3 +230,18 @@ func (d *digitalPinConfig) SetEventHandlerForEdge(handler func(int, time.Duratio
d.edgeEventHandler = handler
return true
}

// SetPollForEdgeDetection use a discrete input polling method to detect edges. A poll interval of zero or smaller will
// deactivate this function. Please note: Using this feature is CPU consuming and less accurate than using cdev event
// handler (gpiod implementation) and should be done only if the former is not implemented or not working for the
// adaptor. E.g. sysfs driver in gobot has not implemented edge detection yet. The function is only useful together with
// SetEventHandlerForEdge() and its corresponding With*() functions.
// The function is intended to use by WithPinPollForEdgeDetection().
func (d *digitalPinConfig) SetPollForEdgeDetection(pollInterval time.Duration, pollQuitChan chan struct{}) (changed bool) {
if d.pollInterval == pollInterval {
return false
}
d.pollInterval = pollInterval
d.pollQuitChan = pollQuitChan
return true
}
33 changes: 33 additions & 0 deletions system/digitalpin_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,36 @@ func TestWithPinEventOnBothEdges(t *testing.T) {
})
}
}

func TestWithPinPollForEdgeDetection(t *testing.T) {
const (
oldVal = time.Duration(1)
newVal = time.Duration(3)
)
tests := map[string]struct {
oldPollInterval time.Duration
want bool
wantVal time.Duration
}{
"no_change": {
oldPollInterval: newVal,
},
"change": {
oldPollInterval: oldVal,
want: true,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
dpc := &digitalPinConfig{pollInterval: tc.oldPollInterval}
stopChan := make(chan struct{})
defer close(stopChan)
// act
got := WithPinPollForEdgeDetection(newVal, stopChan)(dpc)
// assert
assert.Equal(t, tc.want, got)
assert.Equal(t, newVal, dpc.pollInterval)
})
}
}
15 changes: 13 additions & 2 deletions system/digitalpin_gpiod.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ func digitalPinGpiodReconfigureLine(d *digitalPinGpiod, forceInput bool) error {
opts = append(opts, gpiod.WithDebounce(d.debouncePeriod))
}
// edge detection
if d.edgeEventHandler != nil {
if d.edgeEventHandler != nil && d.pollInterval <= 0 {
// use edge detection provided by gpiod
wrappedHandler := digitalPinGpiodGetWrappedEventHandler(d.edgeEventHandler)
switch d.edge {
case digitalPinEventOnFallingEdge:
Expand Down Expand Up @@ -277,10 +278,20 @@ func digitalPinGpiodReconfigureLine(d *digitalPinGpiod, forceInput bool) error {
}
d.line = gpiodLine

// start discrete polling function and wait for first read is done
if (d.direction == IN || forceInput) && d.pollInterval > 0 {
if err := startEdgePolling(d.label, d.Read, d.pollInterval, d.edge, d.edgeEventHandler,
d.pollQuitChan); err != nil {
return err
}
}

return nil
}

func digitalPinGpiodGetWrappedEventHandler(handler func(int, time.Duration, string, uint32, uint32)) func(gpiod.LineEvent) {
func digitalPinGpiodGetWrappedEventHandler(
handler func(int, time.Duration, string, uint32, uint32),
) func(gpiod.LineEvent) {
return func(evt gpiod.LineEvent) {
detectedEdge := "none"
switch evt.Type {
Expand Down
83 changes: 83 additions & 0 deletions system/digitalpin_poll.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package system

import (
"fmt"
"sync"
"time"
)

// TODO write tests

func startEdgePolling(
pinLabel string,
pinReadFunc func() (int, error),
pollInterval time.Duration,
wantedEdge int,
eventHandler func(offset int, t time.Duration, et string, sn uint32, lsn uint32),
quitChan chan struct{},
) error {
if eventHandler == nil {
return fmt.Errorf("an event handler is mandatory for edge polling")
}
if quitChan == nil {
return fmt.Errorf("the quit channel is mandatory for edge polling")
}

const allEdges = "all"

triggerEventOn := "none"
switch wantedEdge {
case digitalPinEventOnFallingEdge:
triggerEventOn = DigitalPinEventFallingEdge
case digitalPinEventOnRisingEdge:
triggerEventOn = DigitalPinEventRisingEdge
case digitalPinEventOnBothEdges:
triggerEventOn = allEdges
default:
return fmt.Errorf("unsupported edge type %d for edge polling", wantedEdge)
}

wg := sync.WaitGroup{}
wg.Add(1)

go func() {
var oldState int
var readStart time.Time
var firstLoopDone bool
for {
select {
case <-quitChan:
return
default:
// note: pure reading takes between 30us and 1ms on rasperry Pi1, typically 50us, with sysfs also 500us
// can happen, so we use the time stamp before start of reading to reduce random duration offset
readStart = time.Now()
readedValue, err := pinReadFunc()
if err != nil {
fmt.Printf("edge polling error occurred while reading the pin %s: %v", pinLabel, err)
readedValue = oldState // keep the value
}
if readedValue != oldState {
detectedEdge := DigitalPinEventRisingEdge
if readedValue < oldState {
detectedEdge = DigitalPinEventFallingEdge
}
if firstLoopDone && (triggerEventOn == allEdges || triggerEventOn == detectedEdge) {
eventHandler(0, time.Duration(readStart.UnixNano()), detectedEdge, 0, 0)
}
oldState = readedValue
}
// the real poll interval is increased by the reading time, see also note above
// negative or zero duration causes no sleep
time.Sleep(pollInterval - time.Since(readStart))
if !firstLoopDone {
wg.Done()
firstLoopDone = true
}
}
}
}()

wg.Wait()
return nil
}
120 changes: 120 additions & 0 deletions system/digitalpin_poll_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package system

import (
"sync"
"testing"
"time"

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

func Test_startEdgePolling(t *testing.T) {
tests := map[string]struct {
eventOnEdge int
quitChan chan struct{}
simulateReturnValues []int
simulateNoEventHandler bool
wantEdgeTypes []string
wantErr string
}{
"edge_falling": {
eventOnEdge: digitalPinEventOnFallingEdge,
quitChan: make(chan struct{}),
simulateReturnValues: []int{1, 0, 1, 0, 0},
wantEdgeTypes: []string{DigitalPinEventFallingEdge, DigitalPinEventFallingEdge},
},
"no_edge_falling": {
eventOnEdge: digitalPinEventOnFallingEdge,
quitChan: make(chan struct{}),
simulateReturnValues: []int{0, 1, 1},
wantEdgeTypes: nil,
},
"edge_rising": {
eventOnEdge: digitalPinEventOnRisingEdge,
quitChan: make(chan struct{}),
simulateReturnValues: []int{0, 1, 0, 1, 1},
wantEdgeTypes: []string{DigitalPinEventRisingEdge, DigitalPinEventRisingEdge},
},
"no_edge_rising": {
eventOnEdge: digitalPinEventOnRisingEdge,
quitChan: make(chan struct{}),
simulateReturnValues: []int{1, 0, 0},
wantEdgeTypes: nil,
},
"edge_both": {
eventOnEdge: digitalPinEventOnBothEdges,
quitChan: make(chan struct{}),
simulateReturnValues: []int{0, 1, 0, 1, 1},
wantEdgeTypes: []string{DigitalPinEventRisingEdge, DigitalPinEventFallingEdge, DigitalPinEventRisingEdge},
},
"no_edges_low": {
eventOnEdge: digitalPinEventOnBothEdges,
quitChan: make(chan struct{}),
simulateReturnValues: []int{0, 0, 0},
wantEdgeTypes: nil,
},
"no_edges_high": {
eventOnEdge: digitalPinEventOnBothEdges,
quitChan: make(chan struct{}),
simulateReturnValues: []int{1, 1, 1},
wantEdgeTypes: nil,
},
"error_no_eventhandler": {
quitChan: make(chan struct{}),
wantErr: "event handler is mandatory",
},
"error_no_quitchannel": {
wantErr: "quit channel is mandatory",
},
"error_unsupported_edgetype_none": {
eventOnEdge: digitalPinEventNone,
quitChan: make(chan struct{}),
wantErr: "unsupported edge type 0",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// arrange
defer func() {
if tc.quitChan != nil {
close(tc.quitChan)
}
}()
pinLabel := "test_pin"
pollInterval := time.Microsecond // zero is possible, just to show usage
// arrange reads
numCallsRead := 0
wg := sync.WaitGroup{}
if tc.simulateReturnValues != nil {
wg.Add(1)
}
readFunc := func() (int, error) {
numCallsRead++
retVal := tc.simulateReturnValues[numCallsRead-1]
if numCallsRead >= len(tc.simulateReturnValues) {
close(tc.quitChan) // ensure no further read call
tc.quitChan = nil // lets skip defer routine
wg.Done() // release assertions
}
return retVal, nil
}
// arrange event handler
var edgeTypes []string
eventHandler := func(offset int, t time.Duration, et string, sn uint32, lsn uint32) {
edgeTypes = append(edgeTypes, et)
}
if tc.simulateNoEventHandler {
eventHandler = nil
}
// act
err := startEdgePolling(pinLabel, readFunc, pollInterval, tc.eventOnEdge, eventHandler, tc.quitChan)
wg.Wait()
// assert
if tc.wantErr != "" {
assert.Error(t, err, tc.wantErr)
}
assert.Equal(t, len(tc.simulateReturnValues), numCallsRead)
assert.Equal(t, tc.wantEdgeTypes, edgeTypes)
})
}
}
Loading

0 comments on commit 0efa9fe

Please sign in to comment.