Skip to content

Commit 5f4b746

Browse files
authored
Merge pull request #73 from McJackson164/feat/mcp23009
Add MCP23009 I/O Expander Button Input Reading via I2C
2 parents 42072d2 + 55bac51 commit 5f4b746

File tree

4 files changed

+208
-1
lines changed

4 files changed

+208
-1
lines changed

modules/button_config.py

+52
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,58 @@ class Button_Config:
178178
},
179179
"Pirate_Audio_old": {},
180180
"Display_HAT_Mini": {},
181+
"IOExpander": {
182+
"MAIN": {
183+
"GP3": ("scroll_prev", ""),
184+
"GP4": ("count_laps", "reset_count"),
185+
"GP5": ("get_screenshot", ""),
186+
# "GP5": ("multiscan", ""),
187+
"GP6": ("start_and_stop_manual", ""),
188+
"GP7": ("scroll_next", "enter_menu"),
189+
},
190+
"MENU": {
191+
"GP3": ("back_menu", ""),
192+
"GP4": ("brightness_control", ""),
193+
"GP5": ("press_space", ""),
194+
"GP6": ("press_shift_tab", ""),
195+
"GP7": ("press_tab", ""),
196+
},
197+
"MAP": {
198+
"GP3": ("scroll_prev", ""),
199+
"GP4": ("map_zoom_minus", ""),
200+
"GP5": ("change_map_overlays", "change_mode"),
201+
"GP6": ("map_zoom_plus", ""),
202+
"GP7": ("scroll_next", "enter_menu"),
203+
},
204+
"MAP_1": {
205+
"GP3": ("map_move_x_minus", ""),
206+
"GP4": ("map_move_y_minus", "map_zoom_minus"),
207+
"GP5": ("change_map_overlays", "change_mode"),
208+
"GP6": ("map_move_y_plus", "map_zoom_plus"),
209+
"GP7": ("map_move_x_plus", "map_search_route"),
210+
},
211+
# "MAP_2": {
212+
# "GP3": ("timeline_past", ""),
213+
# "GP4": ("map_zoom_minus", ""),
214+
# "GP5": ("timeline_reset", "change_mode"),
215+
# "GP6": ("map_zoom_plus", ""),
216+
# "GP7": ("timeline_future", ""),
217+
# },
218+
"COURSE_PROFILE": {
219+
"GP3": ("scroll_prev", ""),
220+
"GP4": ("map_zoom_minus", ""),
221+
"GP5": ("change_mode", ""),
222+
"GP6": ("map_zoom_plus", ""),
223+
"GP7": ("scroll_next", "enter_menu"),
224+
},
225+
"COURSE_PROFILE_1": {
226+
"GP3": ("map_move_x_minus", ""),
227+
"GP4": ("map_zoom_minus", ""),
228+
"GP5": ("change_mode", ""),
229+
"GP6": ("map_zoom_plus", ""),
230+
"GP7": ("map_move_x_plus", ""),
231+
},
232+
},
181233
}
182234
# copy button definition
183235
G_BUTTON_DEF["Display_HAT_Mini"] = copy.deepcopy(G_BUTTON_DEF["Pirate_Audio"])

modules/sensor/i2c/MCP23009.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from .base.button_io_expander import ButtonIOExpander
2+
from adafruit_mcp230xx.mcp23008 import MCP23008 as MCP
3+
import board
4+
import busio
5+
6+
# https://www.microchip.com/en-us/product/mcp23009
7+
# https://ww1.microchip.com/downloads/en/DeviceDoc/20002121C.pdf
8+
9+
10+
# NOTE: no need to set TEST and RESET address and value, due to adafruit_mcp230xx library handling it.
11+
class MCP23009(ButtonIOExpander):
12+
13+
# address
14+
SENSOR_ADDRESS = 0x27
15+
16+
# The amount of available channels (8 for MCP23009)
17+
CHANNELS = 8
18+
19+
def __init__(self, config):
20+
i2c = busio.I2C(board.SCL, board.SDA)
21+
self.mcp = MCP(i2c, address=self.SENSOR_ADDRESS)
22+
23+
super().__init__(config, self.mcp)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from digitalio import Direction, Pull
2+
import time
3+
from threading import Thread
4+
from logger import app_logger
5+
6+
7+
try:
8+
# run from top directory (pizero_bikecomputer)
9+
from .. import i2c
10+
except:
11+
# directly run this program
12+
import modules.sensor.i2c.i2c
13+
14+
15+
class ButtonIOExpander(i2c.i2c):
16+
17+
# The amount of available channels (8 for MCP23009)
18+
CHANNELS = 8
19+
20+
# A button press counts as long press after this amount of milliseconds
21+
LONG_PRESS_DURATION_MS = 1000
22+
23+
# After the button is pressed, it is disabled for this amount of milliseconds to prevent bouncing
24+
DEBOUNCE_DURATION_MS = 80
25+
26+
# Button reads per second. Should be at least 20 for precise results
27+
FPS = 30
28+
29+
_thread = None
30+
31+
def __init__(self, config, mcp):
32+
# reset=False because the adafruit_mcp230xx library is resetting.
33+
super().__init__(reset=False)
34+
35+
self.config = config
36+
self.mcp = mcp
37+
38+
self._ms_per_frame = 1000 / self.FPS
39+
40+
# How many frames it takes to reach the LONG_PRESS_DURATION_MS
41+
self._long_press_frames = int(self.LONG_PRESS_DURATION_MS // self._ms_per_frame)
42+
43+
# How many frames a button is disabled after release
44+
self._debounce_frames = int(self.DEBOUNCE_DURATION_MS // self._ms_per_frame)
45+
46+
# The amount of frames a button is held
47+
self._counter = [0] * self.CHANNELS
48+
# Previous button state
49+
self._previous = [0] * self.CHANNELS
50+
# Saves the lock state of a Button
51+
# NOTE: A button is getting locked to debounce or after long press to prevent that the short and long press event gets triggered simultaneously.
52+
self._locked = [False] * self.CHANNELS
53+
54+
self.pins = []
55+
for i in range(self.CHANNELS):
56+
pin = self.mcp.get_pin(i)
57+
pin.direction = Direction.INPUT
58+
pin.pull = Pull.UP
59+
self.pins.append(pin)
60+
61+
self._start_thread()
62+
63+
def _start_thread(self):
64+
self._thread = Thread(target=self._run)
65+
self._thread.daemon = True
66+
self._thread.start()
67+
68+
def _run(self):
69+
sleep_time = 1.0 / self.FPS
70+
71+
while True:
72+
try:
73+
self.read()
74+
except:
75+
app_logger.error(
76+
f"I/O Expander connection issue! Resetting all buttons..."
77+
)
78+
# if an I2C error occurs due to e.g. connection issues, reset all buttons
79+
for index in range(self.CHANNELS):
80+
self._reset_button(index)
81+
time.sleep(sleep_time)
82+
83+
def press_button(self, button, index):
84+
try:
85+
self.config.button_config.press_button("IOExpander", button, index)
86+
except:
87+
app_logger.warning(f"No button_config for button '{button}'")
88+
89+
def get_pressed_buttons(self):
90+
return [not button.value for button in self.pins]
91+
92+
def read(self):
93+
buttons_pressed = self.get_pressed_buttons()
94+
95+
for i, pressed in enumerate(buttons_pressed):
96+
if pressed:
97+
if not self._locked[i]:
98+
self._counter[i] += 1
99+
100+
if self._counter[i] >= self._long_press_frames and not self._locked[i]:
101+
self._on_button_pressed(i, True)
102+
else:
103+
if self._locked[i]:
104+
if self._counter[i] <= self._debounce_frames:
105+
self._counter[i] += 1
106+
continue
107+
else:
108+
self._reset_button(i)
109+
elif not self._locked[i] and self._previous[i] != 0:
110+
self._on_button_pressed(i, False)
111+
self._previous = buttons_pressed
112+
113+
def _on_button_pressed(self, button_index, long_press):
114+
self._locked[button_index] = True
115+
self._counter[button_index] = 0
116+
self.press_button(f"GP{button_index}", int(long_press))
117+
118+
def _reset_button(self, button_index):
119+
self._locked[button_index] = False
120+
self._counter[button_index] = 0

modules/sensor/sensor_i2c.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ def detect_sensors(self):
360360
"BUTTON_SHIM"
361361
] = self.detect_button_button_shim()
362362

363+
self.available_sensors["BUTTON"]["MCP23009"] = self.detect_button_mcp23009()
364+
363365
# battery
364366
self.available_sensors["BATTERY"]["PIJUICE"] = self.detect_battery_pijuice()
365367
self.available_sensors["BATTERY"]["PISUGAR3"] = self.detect_battery_pisugar3()
@@ -1542,6 +1544,16 @@ def detect_button_button_shim(self):
15421544
except:
15431545
return False
15441546

1547+
def detect_button_mcp23009(self):
1548+
try:
1549+
from .i2c.MCP23009 import MCP23009
1550+
1551+
# device test
1552+
self.sensor_mcp23009 = MCP23009(self.config)
1553+
return True
1554+
except:
1555+
return False
1556+
15451557
def detect_battery_pijuice(self):
15461558
try:
15471559
from pijuice import PiJuice
@@ -1565,4 +1577,4 @@ def detect_battery_pisugar3(self):
15651577
self.sensor_pisugar3 = PiSugar3()
15661578
return True
15671579
except:
1568-
return False
1580+
return False

0 commit comments

Comments
 (0)