diff --git a/experimental/devices/gpioutil/example_test.go b/experimental/devices/gpioutil/example_test.go index ad5ed46..ecf6804 100644 --- a/experimental/devices/gpioutil/example_test.go +++ b/experimental/devices/gpioutil/example_test.go @@ -11,6 +11,7 @@ import ( "periph.io/x/periph/conn/gpio" "periph.io/x/periph/conn/gpio/gpioreg" + "periph.io/x/periph/conn/physic" "periph.io/x/periph/experimental/devices/gpioutil" ) @@ -34,3 +35,57 @@ func ExampleDebounce() { } } } + +func ExamplePollEdge() { + // Flow when it is known that the GPIO does not support edge detection. + p := gpioreg.ByName("XOI-P1") + if p != nil { + log.Fatal("please open another GPIO") + } + p = gpioutil.PollEdge(p, 20*physic.Hertz) + if err := p.In(gpio.PullDown, gpio.RisingEdge); err != nil { + log.Fatal(err) + } + + defer p.Halt() + for { + if p.WaitForEdge(-1) { + fmt.Println(p.Read()) + } + } +} + +func Example() { + // Complete solution: + // - Fallback to software polling if the GPIO doesn't support hardware edge + // detection. + // - Denoise and debounce the reading. + // + // Order is important, as Debounce() requires working edge detection. + p := gpioreg.ByName("XOI-P1") + if p != nil { + log.Fatal("please open another GPIO") + } + if err := p.In(gpio.PullDown, gpio.BothEdges); err == nil { + // Try to fallback into software polling, then reinitialize. + p = gpioutil.PollEdge(p, 50*physic.Hertz) + if err = p.In(gpio.PullDown, gpio.BothEdges); err != nil { + log.Fatal(err) + } + } + + // Ignore glitches lasting less than 10ms, and ignore repeated edges within + // 30ms. Make sure to not use denoiser period lower than the software poller + // frequency. + d, err := gpioutil.Debounce(p, 10*time.Millisecond, 30*time.Millisecond, gpio.BothEdges) + if err != nil { + log.Fatal(err) + } + + defer d.Halt() + for { + if d.WaitForEdge(-1) { + fmt.Println(d.Read()) + } + } +} diff --git a/experimental/devices/gpioutil/polledge.go b/experimental/devices/gpioutil/polledge.go new file mode 100644 index 0000000..7090ae0 --- /dev/null +++ b/experimental/devices/gpioutil/polledge.go @@ -0,0 +1,117 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package gpioutil + +import ( + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/physic" +) + +// pollEdge is a gpio.PinIO where edge detection is done manually. +type pollEdge struct { + // Immutable. + gpio.PinIO + // period is the delay between each poll. + period time.Duration + die chan struct{} + + // Mutable. + // edge is the current edge detection. + edge gpio.Edge +} + +// PollEdge returns a gpio.PinIO which implements edge detection via polling. +// +// Example of GPIOs without edge detection are GPIOs accessible over an I²C +// chip or over USB. +// +// freq must be above 0. A reasonable value is 20Hz reading. High rate +// essentially means a busy loop. +func PollEdge(p gpio.PinIO, freq physic.Frequency) gpio.PinIO { + return &pollEdge{PinIO: p, period: freq.Duration(), die: make(chan struct{}, 1)} +} + +// In implements gpio.PinIO. +func (p *pollEdge) In(pull gpio.Pull, edge gpio.Edge) error { + p.edge = gpio.NoEdge + err := p.PinIO.In(pull, gpio.NoEdge) + if err == nil { + p.edge = edge + } + return err +} + +// WaitForEdge implements gpio.PinIO. +func (p *pollEdge) WaitForEdge(timeout time.Duration) bool { + select { + case <-p.die: + default: + } + defer func() { + select { + case <-p.die: + default: + } + }() + curr := p.PinIO.Read() + // -1 means to wait indefinitely. + if timeout >= 0 { + defer time.AfterFunc(timeout, func() { + p.die <- struct{}{} + }).Stop() + } + // Sadly it's not possible to stop then restart a ticker, so we can't cache + // it in the object. + t := time.NewTicker(p.period) + defer t.Stop() + for { + select { + case <-t.C: + n := p.PinIO.Read() + if n != curr { + switch p.edge { + case gpio.RisingEdge: + if n == gpio.High { + return true + } + curr = n + case gpio.FallingEdge: + if n == gpio.Low { + return true + } + curr = n + case gpio.BothEdges: + return true + } + } + case <-p.die: + return false + } + } +} + +// Halt implements gpio.PinIO. +// +// It unblocks any WaitForEdge loop. +func (p *pollEdge) Halt() error { + select { + // If a WaitForEdge was pending, it will be unblocked. + case p.die <- struct{}{}: + default: + } + return nil +} + +// Real implements gpio.RealPin. +func (p *pollEdge) Real() gpio.PinIO { + if r, ok := p.PinIO.(gpio.RealPin); ok { + return r.Real() + } + return p.PinIO +} + +var _ gpio.PinIO = &pollEdge{} diff --git a/experimental/devices/gpioutil/polledge_test.go b/experimental/devices/gpioutil/polledge_test.go new file mode 100644 index 0000000..1c63220 --- /dev/null +++ b/experimental/devices/gpioutil/polledge_test.go @@ -0,0 +1,159 @@ +// Copyright 2018 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package gpioutil + +import ( + "sync" + "testing" + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpiotest" + "periph.io/x/periph/conn/physic" +) + +func TestAssumption(t *testing.T) { + f := gpiotest.Pin{} + if f.In(gpio.PullNoChange, gpio.BothEdges) == nil { + t.Fatal("Using gpiotest.Pin in no edge support mode") + } + if PollEdge(&f, 20*physic.Hertz) == nil { + t.Fatal("expected error") + } +} + +func TestPollEdge_Short(t *testing.T) { + p := PollEdge(&gpiotest.Pin{}, physic.Hertz) + if err := p.In(gpio.PullNoChange, gpio.BothEdges); err != nil { + t.Fatal(err) + } + if err := p.Halt(); err != nil { + t.Fatal(err) + } + // timeout triggers. + if p.WaitForEdge(time.Nanosecond) { + t.Fatal("unexpected edge") + } +} + +func TestPollEdge_Halt(t *testing.T) { + f := pinWait{wait: make(chan struct{})} + p := PollEdge(&f, physic.Hertz) + go func() { + // Make sure the pin was read at least once, which means the code below is + // inside WaitForEdge(). + <-f.wait + if err := p.Halt(); err != nil { + t.Fatal(err) + } + }() + // p.die triggers. + if p.WaitForEdge(-1) { + t.Fatal("unexpected edge") + } +} + +func TestPollEdge_RisingEdge(t *testing.T) { + f := pinLevels{levels: []gpio.Level{gpio.High, gpio.Low, gpio.High}} + p := PollEdge(&f, physic.KiloHertz) + if err := p.In(gpio.PullNoChange, gpio.RisingEdge); err != nil { + t.Fatal(err) + } + if !p.WaitForEdge(-1) { + t.Fatal("expected edge") + } + if len(f.levels) != 0 { + t.Fatalf("unconsumed levels: %v", f.levels) + } +} + +func TestPollEdge_FallingEdge(t *testing.T) { + f := pinLevels{levels: []gpio.Level{gpio.Low, gpio.High, gpio.Low}} + p := PollEdge(&f, physic.KiloHertz) + if err := p.In(gpio.PullNoChange, gpio.FallingEdge); err != nil { + t.Fatal(err) + } + if !p.WaitForEdge(-1) { + t.Fatal("expected edge") + } + if len(f.levels) != 0 { + t.Fatalf("unconsumed levels: %v", f.levels) + } +} + +func TestPollEdge_BothEdges(t *testing.T) { + f := pinLevels{levels: []gpio.Level{gpio.High, gpio.Low}} + p := PollEdge(&f, physic.KiloHertz) + if err := p.In(gpio.PullNoChange, gpio.BothEdges); err != nil { + t.Fatal(err) + } + if !p.WaitForEdge(-1) { + t.Fatal("expected edge") + } + if len(f.levels) != 0 { + t.Fatal("unconsumed level") + } +} + +func TestPollEdge_RealPin(t *testing.T) { + f := gpiotest.Pin{} + p := PollEdge(&f, physic.Hertz) + r, ok := p.(gpio.RealPin) + if !ok { + t.Fatal("expected gpio.RealPin") + } + a, ok := r.Real().(*gpiotest.Pin) + if !ok { + t.Fatal("expected gpiotest.Pin") + } + if a != &f { + t.Fatal("expected actual pin") + } +} + +func TestPollEdge_RealPin_Deep(t *testing.T) { + f := gpiotest.Pin{} + p := PollEdge(PollEdge(&f, physic.Hertz), physic.Hertz) + r, ok := p.(gpio.RealPin) + if !ok { + t.Fatal("expected gpio.RealPin") + } + a, ok := r.Real().(*gpiotest.Pin) + if !ok { + t.Fatal("expected gpiotest.Pin") + } + if a != &f { + t.Fatal("expected actual pin") + } +} + +// + +type pinLevels struct { + gpiotest.Pin + mu sync.Mutex + levels []gpio.Level +} + +func (p *pinLevels) Read() gpio.Level { + p.mu.Lock() + defer p.mu.Unlock() + l := p.levels[0] + p.levels = p.levels[1:] + return l +} + +type pinWait struct { + gpiotest.Pin + wait chan struct{} + once sync.Once +} + +func (p *pinWait) Read() gpio.Level { + p.once.Do(func() { + p.wait <- struct{}{} + }) + return true +}