Skip to content

Commit

Permalink
move poll to system
Browse files Browse the repository at this point in the history
  • Loading branch information
gen2thomas committed Oct 26, 2023
1 parent 5e4e6f4 commit d8c52de
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 99 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
116 changes: 37 additions & 79 deletions drivers/gpio/hcsr04_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package gpio

import (
"fmt"
"log"
"sync"
"time"

Expand Down Expand Up @@ -31,7 +30,7 @@ type HCSR04 struct {
*Driver
triggerPinID string
echoPinID string
simulateEventHandler bool // use event handler simulation instead "cdev" from gpiod
useEdgePolling bool // use discrete edge polling instead "cdev" from gpiod
triggerPin gobot.DigitalPinner
echoPin gobot.DigitalPinner
lastDistanceMm int // distance in mm, ~20..4000
Expand All @@ -40,18 +39,19 @@ type HCSR04 struct {
measureMutex *sync.Mutex // to ensure that only one measurement is done at a time
delayMicroSecChan chan int64 // channel for event handler return value
distanceMonitorStarted bool
pollQuitChan chan struct{} // channel for quit the continuous polling
}

// NewHCSR04 creates a new instance of the driver for HC-SR04 (same as SEN-US01).
//
// Datasheet: https://www.makershop.de/download/HCSR04-datasheet-version-1.pdf
func NewHCSR04(a gobot.Adaptor, triggerPinID string, echoPinID string, simulateEventHandler bool) *HCSR04 {
func NewHCSR04(a gobot.Adaptor, triggerPinID string, echoPinID string, useEdgePolling bool) *HCSR04 {
h := HCSR04{
Driver: NewDriver(a, "HCSR04"),
triggerPinID: triggerPinID,
echoPinID: echoPinID,
simulateEventHandler: simulateEventHandler,
measureMutex: &sync.Mutex{},
Driver: NewDriver(a, "HCSR04"),
triggerPinID: triggerPinID,
echoPinID: echoPinID,
useEdgePolling: useEdgePolling,
measureMutex: &sync.Mutex{},
}

h.afterStart = func() error {
Expand All @@ -70,10 +70,13 @@ func NewHCSR04(a gobot.Adaptor, triggerPinID string, echoPinID string, simulateE
return fmt.Errorf("error on get echo pin: %v", err)
}

if !simulateEventHandler {
if err := epin.ApplyOptions(system.WithPinEventOnBothEdges(h.createEventHandler())); err != nil {
return fmt.Errorf("error on apply event handler for echo pin: %v", err)
}
epinOptions := []func(gobot.DigitalPinOptioner) bool{system.WithPinEventOnBothEdges(h.createEventHandler())}
if h.useEdgePolling {
h.pollQuitChan = make(chan struct{})
epinOptions = append(epinOptions, system.WithPinPollForEdgeDetection(hcsr04PollInputIntervall, h.pollQuitChan))
}
if err := epin.ApplyOptions(epinOptions...); err != nil {
return fmt.Errorf("error on apply options for echo pin: %v", err)
}
h.echoPin = epin

Expand All @@ -83,6 +86,9 @@ func NewHCSR04(a gobot.Adaptor, triggerPinID string, echoPinID string, simulateE
}

h.beforeHalt = func() error {
if useEdgePolling {
close(h.pollQuitChan)
}
// TODO: create summarized error
if err := h.stopDistanceMonitor(); err != nil {
fmt.Printf("no need to stop distance monitoring: %v\n", err)
Expand Down Expand Up @@ -173,96 +179,48 @@ func (h *HCSR04) GetDistance() float64 {
return float64(h.lastDistanceMm) / 1000.0
}

func (h *HCSR04) createEventHandler() func(int, time.Duration, string, uint32, uint32) {
var startTimestamp time.Duration
return func(offset int, t time.Duration, et string, sn uint32, lsn uint32) {
switch et {
case system.DigitalPinEventRisingEdge:
startTimestamp = t
case system.DigitalPinEventFallingEdge:
// unfortunately there is an additional falling edge at each start trigger, so we need to filter this
// we use the start duration value for filtering
if startTimestamp == 0 {
return
}
h.delayMicroSecChan <- (t - startTimestamp).Microseconds()
startTimestamp = 0
}
}
}

func (h *HCSR04) measureDistance() error {
h.measureMutex.Lock()
defer h.measureMutex.Unlock()

errChan := make(chan error)
quitChan := make(chan struct{})
defer close(quitChan)
if h.simulateEventHandler {
go h.edgePolling(hcsr04PollInputIntervall, errChan, quitChan, h.createEventHandler())
time.Sleep(hcsr04PollInputIntervall) // to ensure the first reading is done
}

if err := h.emitTrigger(); err != nil {
return err
}

// stop the loop if the state is on target state, the timeout is elapsed or an error occurred
// stop the loop if the measure is done or the timeout is elapsed
timeout := hcsr04StartTransmitTimeout + hcsr04ReceiveTimeout
select {
case <-time.After(timeout):
return fmt.Errorf("timeout %s reached while waiting for value with echo pin %s", timeout, h.echoPinID)
case err := <-errChan:
return fmt.Errorf("error while detect echo: %v", err)
case durMicro := <-h.delayMicroSecChan:
log.Println(durMicro, "us")
h.lastDistanceMm = int(durMicro * hcsr04SoundSpeed / 1000 / 2)
}

return nil
}

func (h *HCSR04) createEventHandler() func(int, time.Duration, string, uint32, uint32) {
var startTimestamp time.Duration
return func(offset int, t time.Duration, et string, sn uint32, lsn uint32) {
switch et {
case system.DigitalPinEventRisingEdge:
startTimestamp = t
case system.DigitalPinEventFallingEdge:
// unfortunately there is an additional falling edge at each start trigger, so we need to filter this
// we use the start duration value for filtering
if startTimestamp == 0 {
return
}
h.delayMicroSecChan <- (t - startTimestamp).Microseconds()
startTimestamp = 0
}
}
}

func (h *HCSR04) emitTrigger() error {
if err := h.triggerPin.Write(1); err != nil {
return err
}
time.Sleep(hcsr04EmitTriggerDuration)
return h.triggerPin.Write(0)
}

// TODO: move this function to system level and add a "WithEdgePolling()" option
func (h *HCSR04) edgePolling(
pollInterval time.Duration,
errChan chan error,
quitChan chan struct{},
eventHandler func(offset int, t time.Duration, et string, sn uint32, lsn uint32),
) {
var oldState int
var readStart time.Time
for {
select {
case <-quitChan:
return
default:
// note: pure reading takes between 30us and 1ms on rasperry Pi1, typically 50us
// so we use the time stamp before start of reading to reduce random duration offset
readStart = time.Now()
readedValue, err := h.echoPin.Read()
if err != nil {
errChan <- fmt.Errorf("an error occurred while reading the pin %s: %v", h.echoPinID, err)
return
}
if readedValue != oldState {
edge := system.DigitalPinEventRisingEdge
if readedValue < oldState {
edge = system.DigitalPinEventFallingEdge
}
eventHandler(0, time.Duration(readStart.UnixNano()), edge, 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))
}
}
}
13 changes: 8 additions & 5 deletions examples/raspi_hcsr04.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
func main() {
// this is mandatory for systems with defunct edge detection, although the "cdev" is used with an newer Kernel
// keep in mind, that this cause more inaccurate measurements
const simulateEventHandler = true
const pollEdgeDetection = true
r := raspi.NewAdaptor()
hcsr04 := gpio.NewHCSR04(r, "11", "13", simulateEventHandler)
hcsr04 := gpio.NewHCSR04(r, "11", "13", pollEdgeDetection)

work := func() {
if simulateEventHandler {
fmt.Println("after startup the system is under load and the measurement becomes very inaccurate, so wait a bit")
if pollEdgeDetection {
fmt.Println("Please note that measurements are CPU consuming and will be more inaccurate with this setting.")
fmt.Println("After startup the system is under load and the measurement is very inaccurate, so wait a bit...")
time.Sleep(2000 * time.Millisecond)
}

Expand Down Expand Up @@ -70,5 +71,7 @@ func main() {
work,
)

robot.Start()
if err := robot.Start(); err != nil {
log.Fatal(err)
}
}
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
Loading

0 comments on commit d8c52de

Please sign in to comment.