-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbouncer.go
206 lines (186 loc) · 6.41 KB
/
bouncer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// bouncer is an input recognition package that recognizes button-presses
// of various lengths, notifies an arbitrary number of subscribers, and implements
// debouncing using the systick.
package bouncer
import (
"errors"
"time"
"machine"
)
const (
ERROR_INVALID_PRESSLENGTH = "PressLength not understood"
ERROR_NO_OUTPUT_CHANNELS = "New bouncer wasn't given any output channels"
)
type PressLength uint8
const (
Bounce PressLength = iota
ShortPress
LongPress
ExtraLongPress
)
type sysTickSubscriber struct {
channel chan struct{}
}
var sysTickSubcribers []sysTickSubscriber
type Config struct {
Short time.Duration
Long time.Duration
ExtraLong time.Duration
}
type bouncer struct {
pin *machine.Pin
debounceInterval time.Duration
shortPress time.Duration
longPress time.Duration
extraLongPress time.Duration
tickerCh chan struct{} // produced by sendTicks (relaying systick_handler ticks) -> consumed by RecognizeAndPublish (listening for ticks)
isrChan chan bool // produced by the pin interrupt handler -> consumed by RecognizeAndPublish
outChans []chan PressLength // various channels produced by RecognizeAndPublish -> consumed by subscribers of this bouncer's events
}
type Bouncer interface {
Configure(Config) error
RecognizeAndPublish()
State() bool
Duration(PressLength) time.Duration
}
// New returns a new Bouncer (or error) with the given pin, name & channels, with default durations for
// shortPress, longPress, extraLongPress
func New(p machine.Pin, outs ...chan PressLength) (Bouncer, error) {
if len(outs) < 1 {
return nil, errors.New(ERROR_NO_OUTPUT_CHANNELS)
}
outChans := make([]chan PressLength, 0)
for i := range outs {
outChans = append(outChans, outs[i])
}
return &bouncer{
pin: &p,
shortPress: 22 * time.Millisecond,
longPress: 500 * time.Millisecond,
extraLongPress: 1971 * time.Millisecond,
tickerCh: make(chan struct{}, 1),
isrChan: make(chan bool, 1),
outChans: outChans,
}, nil
}
// Configure sets the pin mode to InputPullup, assigns interrupt handler, overrides default durations
func (b *bouncer) Configure(cfg Config) error {
b.pin.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
err := b.pin.SetInterrupt(machine.PinFalling|machine.PinRising, func(machine.Pin) {
b.isrChan <- b.pin.Get()
})
if err != nil {
return err
}
if b.shortPress > 0 {
b.shortPress = cfg.Short
}
if b.longPress > 0 {
b.longPress = cfg.Long
}
if b.extraLongPress > 0 {
b.extraLongPress = cfg.ExtraLong
}
addSysTickConsumer(b.tickerCh)
return nil
}
// State returns an on-demand measurement of the bouncer's pin
func (b *bouncer) State() bool {
return b.pin.Get()
}
// RecognizeAndPublish should be a goroutine; reads pin state & sample time from channel,
// awaits completion of a buttonDown -> buttonUp sequence, recognizes press length,
// publishes the recognized press event to the button's output channel(s)
func (b *bouncer) RecognizeAndPublish() {
ticks := 0 // ticks will begin to increment when a button 'down' is registered
btnDown := time.Time{} // btnDown is the beginning time of a button press event
dur := btnDown.Sub(btnDown) // initial duration zero
for {
select {
case <-b.tickerCh:
if ticks == 0 { // we aren't listening
btnDown = time.Time{} // ensure this is empty because occasionally it isn't
continue
} else {
ticks += 1
}
case up := <-b.isrChan:
switch up {
case true: // button is 'up'
if ticks == 0 { // if we were awaiting a new bounce sequence to begin
continue // ignore 'up' signal & reset the loop
} else { // if we were awaiting the conclusion of a bounce sequence
if ticks >= 2 { // if the interval between down & up is greater than systick interval
dur = time.Now().Sub(btnDown) // calculate sequence duration
ticks = 0 // stop & reset ticks + look for new bounce sequence
btnDown = time.Time{} // reset button down time
// Recognize & publish to channel(s)
b.publish(b.recognize(dur))
} // or ignore & await next buttonUp if debounce interval was not exceeded
}
case false: // button is 'down'
if ticks == 0 { // if we were awaitng a new bounce sequence to begin
ticks = 1 // set ticks to 1 so that ticks begins to increment with each received systick
btnDown = time.Now() // set now as the beginning of the sequence
continue // reset the loop
} // otherwise if we were awaiting the conclusion of a bounce sequence, ignore
}
}
}
}
// Duration returns the duration of the passed-in PressLength
func (b *bouncer) Duration(l PressLength) time.Duration {
switch l {
case ShortPress:
return b.shortPress
case LongPress:
return b.longPress
case ExtraLongPress:
return b.extraLongPress
default:
return 0
}
}
// publish concurrently sends a PressLength to all channels subscribed to this Bouncer
func (b *bouncer) publish(p PressLength) {
for i := range b.outChans {
go func(i int) {
b.outChans[i] <- p
}(i)
}
}
// recognize returns a PressLength resulting from a passed-in duration matching a Bouncer's durations
func (b *bouncer) recognize(d time.Duration) PressLength {
if d >= b.extraLongPress { // duration was extraLongPress
return ExtraLongPress
} else if d < b.extraLongPress && d >= b.longPress { // duration was longPress
return LongPress
} else if d < b.longPress && d >= b.shortPress { // duration was shortPress
return ShortPress
}
return Bounce // should be unreachable
}
// addSysTickConsumer appends a channel to the pkg-level SysTickSubscriber slice.
// each Bouncer is added to this slice in New and ticks are relayed by spawning RelayTicks
func addSysTickConsumer(ch chan struct{}) {
sysTickSubcribers = append(sysTickSubcribers, sysTickSubscriber{channel: ch})
}
// sendTicks sends a signal to each Bouncer in the package-level SysTickSubscribers slice
func sendTicks() {
if len(sysTickSubcribers) > 0 {
for _, c := range sysTickSubcribers {
c.channel <- struct{}{}
}
}
}
// Debounce relays ticks from the SysTick_Handler to all bouncers;
// and is intended to be called as a long-lived goroutine, and only once regarldess of how many bouncers you make.
// The param tickCh is intended to be the same channel spammed by your SysTick_Handler
func Debounce(tickCh chan struct{}) {
for {
select {
case <-tickCh:
sendTicks()
}
}
}