diff --git a/adaptor.go b/adaptor.go index 596332642..394d3c079 100644 --- a/adaptor.go +++ b/adaptor.go @@ -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 diff --git a/drivers/gpio/hcsr04_driver.go b/drivers/gpio/hcsr04_driver.go index 232ab6fef..ae1fe1f8d 100644 --- a/drivers/gpio/hcsr04_driver.go +++ b/drivers/gpio/hcsr04_driver.go @@ -2,7 +2,6 @@ package gpio import ( "fmt" - "log" "sync" "time" @@ -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 @@ -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 { @@ -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 @@ -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) @@ -173,55 +179,44 @@ 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 @@ -229,40 +224,3 @@ func (h *HCSR04) emitTrigger() error { 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)) - } - } -} diff --git a/examples/raspi_hcsr04.go b/examples/raspi_hcsr04.go index 601cdb4b8..6b1452e84 100644 --- a/examples/raspi_hcsr04.go +++ b/examples/raspi_hcsr04.go @@ -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) } @@ -70,5 +71,7 @@ func main() { work, ) - robot.Start() + if err := robot.Start(); err != nil { + log.Fatal(err) + } } diff --git a/system/digitalpin_config.go b/system/digitalpin_config.go index 26d8bafaf..b11962b3a 100644 --- a/system/digitalpin_config.go +++ b/system/digitalpin_config.go @@ -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 { @@ -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 { @@ -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 +} diff --git a/system/digitalpin_config_test.go b/system/digitalpin_config_test.go index be73c5190..c14d6fb5b 100644 --- a/system/digitalpin_config_test.go +++ b/system/digitalpin_config_test.go @@ -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) + }) + } +} diff --git a/system/digitalpin_gpiod.go b/system/digitalpin_gpiod.go index df913cad1..c161dc00f 100644 --- a/system/digitalpin_gpiod.go +++ b/system/digitalpin_gpiod.go @@ -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: @@ -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 { diff --git a/system/digitalpin_poll.go b/system/digitalpin_poll.go new file mode 100644 index 000000000..f18386435 --- /dev/null +++ b/system/digitalpin_poll.go @@ -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() + 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 +} diff --git a/system/digitalpin_sysfs.go b/system/digitalpin_sysfs.go index f29a4157e..d27cd5bd9 100644 --- a/system/digitalpin_sysfs.go +++ b/system/digitalpin_sysfs.go @@ -47,7 +47,7 @@ func newDigitalPinSysfs(fs filesystem, pin string, options ...func(gobot.Digital func (d *digitalPinSysfs) 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() @@ -169,26 +169,38 @@ func (d *digitalPinSysfs) reconfigure() error { } } - // configure bias (unsupported) + // configure bias (inputs and outputs, 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 period (inputs only), edge detection (inputs only) and drive (outputs only) + if d.direction == IN { + // configure debounce (unsupported) + if d.debouncePeriod != 0 && systemSysfsDebug { + log.Printf("debounce period option (%d) is not supported by sysfs\n", d.debouncePeriod) + } - // 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 + if err == nil { + if d.edge != 0 && d.pollInterval <= 0 { + err = fmt.Errorf("edge detect option (%d) is not implemented for sysfs without discrete polling", d.edge) + } + } - // configure edge detection (not implemented) - if d.edge != 0 && systemSysfsDebug { - log.Printf("edge detect option (%d) is not implemented for sysfs\n", d.edge) + // start discrete polling function and wait for first read is done + if err == nil { + if d.pollInterval > 0 { + err = startEdgePolling(d.label, d.Read, d.pollInterval, d.edge, d.edgeEventHandler, d.pollQuitChan) + } + } + } else { + // configure drive (unsupported) + if d.drive != digitalPinDrivePushPull && systemSysfsDebug { + log.Printf("drive options (%d) are not supported by sysfs\n", d.drive) + } } if err != nil {