From 6853591d2af1532ae4cf406c6e002c39c3077a8a Mon Sep 17 00:00:00 2001 From: Marc-Antoine Ruel Date: Wed, 17 Oct 2018 09:20:15 -0400 Subject: [PATCH] gpioutil: add minimal Deboune implementation. No algorithm yet. --- experimental/devices/gpioutil/debounce.go | 90 +++++++++ .../devices/gpioutil/debounce_test.go | 180 ++++++++++++++++++ experimental/devices/gpioutil/doc.go | 6 + experimental/devices/gpioutil/example_test.go | 36 ++++ 4 files changed, 312 insertions(+) create mode 100644 experimental/devices/gpioutil/debounce.go create mode 100644 experimental/devices/gpioutil/debounce_test.go create mode 100644 experimental/devices/gpioutil/doc.go create mode 100644 experimental/devices/gpioutil/example_test.go diff --git a/experimental/devices/gpioutil/debounce.go b/experimental/devices/gpioutil/debounce.go new file mode 100644 index 0000000..e00aaa7 --- /dev/null +++ b/experimental/devices/gpioutil/debounce.go @@ -0,0 +1,90 @@ +// 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" +) + +// debounced is a gpio.PinIO where reading and edge detection pass through a +// debouncing algorithm. +type debounced struct { + // Immutable. + gpio.PinIO + // denoise delays state changes. It waits for this amount before reporting it. + denoise time.Duration + // debounce locks on after a steady state change. Once a state change + // happened, don't change again for this amount of time. + debounce time.Duration + + // Mutable. +} + +// Debounce returns a debounced gpio.PinIO from a gpio.PinIO source. Only the +// PinIn behavior is mutated. +// +// denoise is a noise filter, which waits a pin to be steady for this amount +// of time BEFORE reporting the new level. +// +// debounce will lock on a level for this amount of time AFTER the pin changed +// state, ignoring following state changes. +// +// Either value can be 0. +func Debounce(p gpio.PinIO, denoise, debounce time.Duration, edge gpio.Edge) (gpio.PinIO, error) { + if denoise == 0 && debounce == 0 { + return p, nil + } + if err := p.In(gpio.PullNoChange, gpio.BothEdges); err != nil { + return nil, err + } + return &debounced{ + // Immutable. + PinIO: p, + denoise: denoise, + debounce: debounce, + // Mutable. + }, nil +} + +// In implements gpio.PinIO. +func (d *debounced) In(pull gpio.Pull, edge gpio.Edge) error { + err := d.PinIO.In(pull, gpio.BothEdges) + return err +} + +// Read implements gpio.PinIO. +// +// It is the smoothed out value from the underlying gpio.PinIO. +func (d *debounced) Read() gpio.Level { + return d.PinIO.Read() +} + +// WaitForEdge implements gpio.PinIO. +// +// It is the smoothed out value from the underlying gpio.PinIO. +func (d *debounced) WaitForEdge(timeout time.Duration) bool { + if !d.PinIO.WaitForEdge(timeout) { + return false + } + return true +} + +// Halt implements gpio.PinIO. +func (d *debounced) Halt() error { + return nil +} + +// Real implements gpio.RealPin. +func (d *debounced) Real() gpio.PinIO { + if r, ok := d.PinIO.(gpio.RealPin); ok { + return r.Real() + } + return d.PinIO +} + +var now = time.Now +var _ gpio.PinIO = &debounced{} diff --git a/experimental/devices/gpioutil/debounce_test.go b/experimental/devices/gpioutil/debounce_test.go new file mode 100644 index 0000000..8fcf85f --- /dev/null +++ b/experimental/devices/gpioutil/debounce_test.go @@ -0,0 +1,180 @@ +// 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 ( + "testing" + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpiotest" +) + +func TestDebounce_Err(t *testing.T) { + defer mocktime(t, nil)() + f := gpiotest.Pin{} + if _, err := Debounce(&f, time.Second, 0, gpio.BothEdges); err == nil { + t.Fatal("expected error") + } +} + +func TestDebounce_Zero(t *testing.T) { + defer mocktime(t, nil)() + f := gpiotest.Pin{} + p, err := Debounce(&f, 0, 0, gpio.BothEdges) + if err != nil { + t.Fatal("expected error") + } + if p1, ok := p.(*gpiotest.Pin); !ok || p1 != &f { + t.Fatal("expected the pin to be returned as-is") + } +} + +func TestDebounce_In(t *testing.T) { + defer mocktime(t, nil)() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + if err := p.In(gpio.PullNoChange, gpio.BothEdges); err != nil { + t.Fatal(err) + } + if p.Halt() != nil { + t.Fatal(err) + } +} + +func TestDebounce_Read_Low(t *testing.T) { + defer mocktime(t, nil)() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, time.Second, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + if p.Read() != gpio.Low { + t.Fatal("expected level") + } + if p.Read() != gpio.Low { + t.Fatal("expected level") + } +} + +func TestDebounce_Read_High(t *testing.T) { + defer mocktime(t, nil)() + f := gpiotest.Pin{L: gpio.High, EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, time.Second, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + if p.Read() != gpio.High { + t.Fatal("expected level") + } + if p.Read() != gpio.High { + t.Fatal("expected level") + } +} + +func TestDebounce_WaitForEdge_Got(t *testing.T) { + offsets := []time.Duration{} + defer mocktime(t, offsets)() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level, 1)} + p, err := Debounce(&f, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + f.EdgesChan <- gpio.Low + if !p.WaitForEdge(0) { + t.Fatal("expected edge") + } +} + +func TestDebounce_WaitForEdge_Timeout(t *testing.T) { + offsets := []time.Duration{} + defer mocktime(t, offsets)() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + if p.WaitForEdge(0) { + t.Fatal("expected no edge") + } +} + +func TestDebounce_RealPin(t *testing.T) { + defer mocktime(t, []time.Duration{})() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + 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 TestDebounce_RealPin_Deep(t *testing.T) { + defer mocktime(t, []time.Duration{})() + f := gpiotest.Pin{EdgesChan: make(chan gpio.Level)} + p, err := Debounce(&f, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + p, err = Debounce(p, time.Second, 0, gpio.BothEdges) + if err != nil { + t.Fatal(err) + } + 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 init() { + resetNow() +} + +func resetNow() { + now = func() time.Time { + panic("unexpected call") + } +} + +func mocktime(t *testing.T, offsets []time.Duration) func() { + offset := 0 + d := time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC) + now = func() time.Time { + if offset == len(offsets) { + t.Fatal("need one more offset") + } + v := d.Add(offsets[offset]) + offset++ + return v + } + return func() { + resetNow() + if offset != len(offsets) { + t.Fatalf("expected to consume all time mocks; used %d, expectd %d", offset, len(offsets)) + } + } +} diff --git a/experimental/devices/gpioutil/doc.go b/experimental/devices/gpioutil/doc.go new file mode 100644 index 0000000..be12872 --- /dev/null +++ b/experimental/devices/gpioutil/doc.go @@ -0,0 +1,6 @@ +// 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 includes utilities to filter or augment GPIOs. +package gpioutil diff --git a/experimental/devices/gpioutil/example_test.go b/experimental/devices/gpioutil/example_test.go new file mode 100644 index 0000000..ad5ed46 --- /dev/null +++ b/experimental/devices/gpioutil/example_test.go @@ -0,0 +1,36 @@ +// 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_test + +import ( + "fmt" + "log" + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpioreg" + "periph.io/x/periph/experimental/devices/gpioutil" +) + +func ExampleDebounce() { + p := gpioreg.ByName("GPIO16") + if p != nil { + log.Fatal("please open another GPIO") + } + + // Ignore glitches lasting less than 3ms, and ignore repeated edges within + // 30ms. + d, err := gpioutil.Debounce(p, 3*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()) + } + } +}