-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathi2c_button.py
352 lines (288 loc) · 12.4 KB
/
i2c_button.py
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# SPDX-FileCopyrightText: Copyright (c) 2020,2021 Greg Paris
#
# SPDX-License-Identifier: MIT
# This library based upon Sparkfun's Arduino library and firmware sources:
# github.com/sparkfun/SparkFun_Qwiic_Button_Arduino_Library/blob/master/src
# github.com/sparkfunX/Qwiic_Switch/blob/master/Firmware/Qwiic_Button
"""
`i2c_button`
================================================================================
CircuitPython I2C Button à la Sparkfun Qwiic Button/Switch/Arcade
* Author(s): Greg Paris
Implementation Notes
--------------------
**Hardware:**
* `Sparkfun Qwiic Button-Red <https://www.sparkfun.com/products/15584>`_
* `Sparkfun Qwiic Button-Blue <https://www.sparkfun.com/products/15585>`_
* `Sparkfun Qwiic Switch <https://www.sparkfun.com/products/15586>`_
* `Sparkfun Qwiic Arcade-Red <https://www.sparkfun.com/products/15591>`_
* `Sparkfun Qwiic Arcade-Blue <https://www.sparkfun.com/products/15592>`_
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
* Adafruit's Bus Device library:
https://github.com/adafruit/Adafruit_CircuitPython_BusDevice
"""
# imports
from collections import namedtuple
from adafruit_bus_device.i2c_device import I2CDevice
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/gmparis/CircuitPython_i2c_button.git"
_ENDIAN = "little"
_DEF_ADDR = 0x6F
_DEV_ID = 0x5D
# Button status flags and tuple
_BS_EVENT = 0x1 # clear after use
_BS_CLICKED = 0x2 # clear after use
_BS_PRESSED = 0x4 # user immutable
_BS = namedtuple("_BS", ("available", "been_clicked", "is_pressed"))
# Interrupt status flags
# _INT_CL = 0x1 # enable an interrupt on button click
# _INT_PR = 0x2 # enable an interrupt on button press
# _INT = namedtuple("_INT", ("on_click", "on_press"))
# Queue status flags and tuple
# _QS_POP = 0x1 # set to pop from queue
# _QS_EMPTY = 0x2 # user immutable
# _QS_FULL = 0x4 # user immutable
# _QS = namedtuple("_QS", ("empty", "full"))
# Commented out due to low utility.
# def _to_qs(status):
# """Queue status integer to **_QS** tuple."""
# return _QS((status & _QS_EMPTY != 0), (status & _QS_FULL != 0))
class ButtonError(Exception):
"""Button-related error conditions."""
def _read_register(button, register, n_bytes=1):
"""Write the register number, read back the value."""
regid = register.to_bytes(1, _ENDIAN)
buf = bytearray(n_bytes)
with button.device as dev:
dev.write_then_readinto(regid, buf)
return int.from_bytes(buf, _ENDIAN)
def _write_register(button, register, value, n_bytes=1):
"""Write the register number, write the value."""
buf = bytearray(1 + n_bytes)
buf[0] = register
buf[1:] = value.to_bytes(n_bytes, _ENDIAN)
with button.device as dev:
dev.write(buf)
# Button Register Descriptor class
class _Reg:
def __init__(self, addr, width, readonly=False):
self.addr = addr
self.width = width
self.readonly = readonly
def __get__(self, button, objtype):
return _read_register(button, self.addr, self.width)
def __set__(self, button, value):
if self.readonly:
raise AttributeError("write to read-only register " + hex(self.addr))
_write_register(button, self.addr, value, self.width)
# NOTE: This class is sortable and hashable to make it easier
# to organize buttons when one has more than a few.
# If you don't need this functionality, you can comment out the
# lines as noted below and save some memory.
class I2C_Button:
# pylint: disable=line-too-long
"""I2C-connected button, à la Sparkfun Qwiic Button/Switch/Arcade
:param i2c_obj: initialized I2C object
:param i2c_addr: I2C address of the button
:param dev_id: Device ID of the button
:param name: a name for the button
:raises ButtonError: if device I2C address does match the specified device ID
Besides being connected via I2C, which can provide some advantages when wiring up a project,
the I2C Button's chief advantage is that it has its own processor, which monitors the
state of the physical button. It does debouncing. It keeps track of time since the last
click event and the last press event. It reports whether a button has been pressed or
has been clicked since its state was last cleared. It can even manage operation of a
single (user-supplied) LED. That's a lot of processing that the calling program does not
have to do.
It should have functionality associated with keeping a queue of events. Unfortunately,
at the time of writing this, the only firmware available doesn't quite hit the mark when
it comes to queue management. It is not possible to clear the queues, nor does popping
an entry off a queue have an effect on its length. Previous versions of this library
included methods for interacting with the queues, but since they don't work, they waste
space and potentially waste the time of someone trying to use them. Those methods have
been commented out in this version of this library.
"""
def __init__(self, i2c_obj, i2c_addr=_DEF_ADDR, dev_id=_DEV_ID, name="button"):
self.i2c = i2c_obj
self.device = I2CDevice(i2c_obj, i2c_addr)
if self.dev_id != dev_id:
raise ButtonError(
f"not button {self.dev_id:02x} at i2c addr {i2c_addr:02x}"
)
self._name = name
self._key = (name, i2c_addr) # comment out for no sorting/hashing
# Commment out next four methods if you don't need to compare buttons,
# for example, by sorting them.
def __eq__(self, other):
# pylint: disable=protected-access
return self._key == other._key
def __gt__(self, other):
# pylint: disable=protected-access
return self._key > other._key
def __lt__(self, other):
# pylint: disable=protected-access
return self._key < other._key
# Commment out next method if you don't need to hash buttons,
# for example, by putting them in a set().
def __hash__(self):
return hash(self._key)
# Comment out next if you need a bit more memory.
def __repr__(self):
return "%s(%s, i2c_addr=%s, dev_id=%s, name=%s)" % (
self.__class__.__name__,
repr(self.i2c),
hex(self.i2c_addr),
hex(self.dev_id),
repr(self.name),
)
_fwmin = _Reg(0x01, 1, True) # FIRMWARE_MINOR (ro)
_fwmaj = _Reg(0x02, 1, True) # FIRMWARE_MAJOR (ro)
_bs = _Reg(0x03, 1) # BUTTON_STATUS (see _BS flags above)
# Next three commented out due to low utility.
# _int = _Reg(0x04, 1) # INTERRUPT_CONFIG (see _INT flags above)
# _prqs = _Reg(0x07, 1) # PRESSED_QUEUE_STATUS (see _QS flags above)
# _clqs = _Reg(0x10, 1) # CLICKED_QUEUE_STATUS (see _QS flags above)
#: Device ID. (1 byte; read-only)
dev_id = _Reg(0x00, 1, True)
#: Button debounce time in milliseconds. (4 bytes; read-write)
debounce_ms = _Reg(0x05, 2)
#: Time since most recent press in queue in milliseconds. (4 bytes; read-only)
last_press_ms = _Reg(0x08, 4, True)
#: Time since oldest press in queue in milliseconds. (4 bytes; read-only)
#:
#: Note that with firmware version 1.1, there is no way to clear the
#: queue, so this value is less useful than it might first seem.
first_press_ms = _Reg(0x0C, 4, True)
#: Time since most recent click in queue in milliseconds. (4 bytes; read-only)
last_click_ms = _Reg(0x11, 4, True)
#: Time since oldest click in queue in milliseconds. (4 bytes; read-only)
#:
#: Note that with firmware version 1.1, there is no way to clear the
#: queue, so this value is less useful than it might first seem.
first_click_ms = _Reg(0x15, 4, True)
#: LED brightness, 0 - 255. (1 byte; read-write)
led_bright = _Reg(0x19, 1)
#: LED granularity. A value of 1 is commonly useful. (1 byte; read-write)
led_gran = _Reg(0x1A, 1)
#: LED pulse cycle time in milliseconds. (4 bytes; read-write)
led_cycle_ms = _Reg(0x1B, 2)
#: LED pulse off time in milliseconds. (4 bytes; read-write)
led_off_ms = _Reg(0x1D, 2)
#: Button I2C address. (1 byte; read-write)
#:
#: If you set this property, you are changing the I2C address of the button. That change
#: will persist through power-off. When you make such a change, the :class:`I2C_Button`
#: instance will become invalid. Probably best to just make any such changes in a separate
#: program. One of the examples shows this.
i2c_addr = _Reg(0x1F, 1)
@property
def name(self):
"""Button name."""
return self._name
# @property
# def version(self):
# """Firmware version number. (2 bytes; read-only)"""
# return (self._fwmaj << 8) | self._fwmin # same way as Arduino library but kooky
@property
def version(self):
"""Firmware version string (read-only)"""
return f"{self._fwmaj:d}.{self._fwmin:d}"
@property
def status(self):
"""Button status. (**available**, **been_clicked**, **is_pressed** tuple; read-only)"""
intval = self._bs
return _BS(
(intval & _BS_EVENT != 0),
(intval & _BS_CLICKED != 0),
(intval & _BS_PRESSED != 0),
)
def clear(self):
"""Reset button status."""
self._bs = 0
# Commented out due to low utility.
# @property
# def click_queue(self):
# """Click queue status. (**empty**, **full** tuple; read-only)
#
# The utility of this method is low if firmware version is 1.1.
# This is because the queue is not cleared by **clear()**,
# nor are entries removed with **pop_click_queue()**."""
# return _to_qs(self._clqs)
# Commented out until new firmware fixes this issue.
# def pop_click_queue(self):
# """Get time since first click, pop click queue, return time.
#
# Popping the click queue does not work with firmware version 1.1,
# so this method rasies an exception in that case.
#
# :raises ButtonError: if queue is empty
# :raises RuntimeError: if firmware version is 1.1
# """
# if self.version == "1.1":
# raise RuntimeError("unsupported by firmware version")
# if self.click_queue.empty:
# raise ButtonError("click queue is empty")
# qtm = self.first_click_ms # oldest click
# self._clqs |= _QS_POP
# return qtm
# Commented out due to low utility.
# @property
# def press_queue(self):
# """Press queue status. (**empty**, **full** tuple; read-only)
#
# The utility of this method is low if firmware version is 1.1.
# This is because the queue is not cleared by **clear()**,
# nor are entries removed with **pop_press_queue()**."""
# return _to_qs(self._prqs)
# Commented out until new firmware fixes this issue.
# def pop_press_queue(self):
# """Get time since first press, pop press queue, return time.
#
# Popping the press queue does not work with firmware version 1.1,
# so this method rasies an exception in that case.
#
# :raises ButtonError: if queue is empty
# :raises RuntimeError: if firmware version is 1.1
# """
# if self.version == "1.1":
# raise RuntimeError("unsupported by firmware version")
# if self.press_queue.empty:
# raise ButtonError("press queue is empty")
# qtm = self.first_press_ms # oldest press
# self._prqs |= _QS_POP
# return qtm
# Commented-out due to low utility.
# @property
# def interrupts(self):
# """Interrupts settings. (**on_click**, **on_press** tuple; read-only)
#
# CircuitPython does not use these interrupts.
# """
# intval = self._int
# return _INT((intval & _INT_CL != 0), (intval & _INT_PR != 0))
# Commented-out due to low utility.
# def set_on_click(self, enable=True):
# """Enable or disable **on_click** interrupt.
#
# :param enable: True to enable interrupt (default)
#
# This interrupt is not used by CircuitPython.
# """
# if enable:
# self._int |= _INT_CL
# else:
# self._int &= ~_INT_CL & 0xFF
# Commented-out due to low utility.
# def set_on_press(self, enable=True):
# """Enable or disable **on_press** interrupt.
#
# :param enable: True to enable interrupt (default)
#
# This interrupt is not used by CircuitPython.
# """
# if enable:
# self._int |= _INT_PR
# else:
# self._int &= ~_INT_PR & 0xFF