Skip to content

Commit

Permalink
system(gpio): add edge polling function (hybridgroup#1015)
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas authored Oct 26, 2023
1 parent 002c75c commit 1f09353
Show file tree
Hide file tree
Showing 10 changed files with 737 additions and 83 deletions.
8 changes: 7 additions & 1 deletion adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ type DigitalPinOptioner interface {
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.
// SetEventHandlerForEdge initializes the input pin for edge detection 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)
// 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
37 changes: 35 additions & 2 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,16 @@ 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 @@ -211,13 +223,34 @@ func (d *digitalPinConfig) SetDebounce(period time.Duration) bool {
return true
}

// SetEventHandlerForEdge sets the input pin to edge detection and to call the event handler on specified edge. The
// SetEventHandlerForEdge sets the input pin to edge detection 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 {
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
}

// 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
8 changes: 4 additions & 4 deletions system/digitalpin_gpiod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestApplyOptions(t *testing.T) {
}
}

func TestExport(t *testing.T) {
func TestExportGpiod(t *testing.T) {
tests := map[string]struct {
simErr error
wantReconfigured int
Expand Down Expand Up @@ -155,7 +155,7 @@ func TestExport(t *testing.T) {
}
}

func TestUnexport(t *testing.T) {
func TestUnexportGpiod(t *testing.T) {
tests := map[string]struct {
simNoLine bool
simReconfErr error
Expand Down Expand Up @@ -217,7 +217,7 @@ func TestUnexport(t *testing.T) {
}
}

func TestWrite(t *testing.T) {
func TestWriteGpiod(t *testing.T) {
tests := map[string]struct {
val int
simErr error
Expand Down Expand Up @@ -266,7 +266,7 @@ func TestWrite(t *testing.T) {
}
}

func TestRead(t *testing.T) {
func TestReadGpiod(t *testing.T) {
tests := map[string]struct {
simVal int
simErr error
Expand Down
81 changes: 81 additions & 0 deletions system/digitalpin_poll.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package system

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

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()
readValue, err := pinReadFunc()
if err != nil {
fmt.Printf("edge polling error occurred while reading the pin %s: %v", pinLabel, err)
readValue = oldState // keep the value
}
if readValue != oldState {
detectedEdge := DigitalPinEventRisingEdge
if readValue < oldState {
detectedEdge = DigitalPinEventFallingEdge
}
if firstLoopDone && (triggerEventOn == allEdges || triggerEventOn == detectedEdge) {
eventHandler(0, time.Duration(readStart.UnixNano()), detectedEdge, 0, 0)
}
oldState = readValue
}
// 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
}
Loading

0 comments on commit 1f09353

Please sign in to comment.