Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MCP23009 I/O Expander Button Input Reading via I2C #73

Merged
merged 3 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions modules/button_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,58 @@ class Button_Config:
},
"Pirate_Audio_old": {},
"Display_HAT_Mini": {},
"IOExpander": {
"MAIN": {
"GP3": ("scroll_prev", ""),
"GP4": ("count_laps", "reset_count"),
"GP5": ("get_screenshot", ""),
# "GP5": ("multiscan", ""),
"GP6": ("start_and_stop_manual", ""),
"GP7": ("scroll_next", "enter_menu"),
},
"MENU": {
"GP3": ("back_menu", ""),
"GP4": ("brightness_control", ""),
"GP5": ("press_space", ""),
"GP6": ("press_shift_tab", ""),
"GP7": ("press_tab", ""),
},
"MAP": {
"GP3": ("scroll_prev", ""),
"GP4": ("map_zoom_minus", ""),
"GP5": ("change_map_overlays", "change_mode"),
"GP6": ("map_zoom_plus", ""),
"GP7": ("scroll_next", "enter_menu"),
},
"MAP_1": {
"GP3": ("map_move_x_minus", ""),
"GP4": ("map_move_y_minus", "map_zoom_minus"),
"GP5": ("change_map_overlays", "change_mode"),
"GP6": ("map_move_y_plus", "map_zoom_plus"),
"GP7": ("map_move_x_plus", "map_search_route"),
},
# "MAP_2": {
# "GP3": ("timeline_past", ""),
# "GP4": ("map_zoom_minus", ""),
# "GP5": ("timeline_reset", "change_mode"),
# "GP6": ("map_zoom_plus", ""),
# "GP7": ("timeline_future", ""),
# },
"COURSE_PROFILE": {
"GP3": ("scroll_prev", ""),
"GP4": ("map_zoom_minus", ""),
"GP5": ("change_mode", ""),
"GP6": ("map_zoom_plus", ""),
"GP7": ("scroll_next", "enter_menu"),
},
"COURSE_PROFILE_1": {
"GP3": ("map_move_x_minus", ""),
"GP4": ("map_zoom_minus", ""),
"GP5": ("change_mode", ""),
"GP6": ("map_zoom_plus", ""),
"GP7": ("map_move_x_plus", ""),
},
},
}
# copy button definition
G_BUTTON_DEF["Display_HAT_Mini"] = copy.deepcopy(G_BUTTON_DEF["Pirate_Audio"])
Expand Down
23 changes: 23 additions & 0 deletions modules/sensor/i2c/MCP23009.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from .base.button_io_expander import ButtonIOExpander
from adafruit_mcp230xx.mcp23008 import MCP23008 as MCP
import board
import busio

# https://www.microchip.com/en-us/product/mcp23009
# https://ww1.microchip.com/downloads/en/DeviceDoc/20002121C.pdf


# NOTE: no need to set TEST and RESET address and value, due to adafruit_mcp230xx library handling it.
class MCP23009(ButtonIOExpander):

# address
SENSOR_ADDRESS = 0x27

# The amount of available channels (8 for MCP23009)
CHANNELS = 8

def __init__(self, config):
i2c = busio.I2C(board.SCL, board.SDA)
self.mcp = MCP(i2c, address=self.SENSOR_ADDRESS)

super().__init__(config, self.mcp)
120 changes: 120 additions & 0 deletions modules/sensor/i2c/base/button_io_expander.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from digitalio import Direction, Pull
import time
from threading import Thread
from logger import app_logger


try:
# run from top directory (pizero_bikecomputer)
from .. import i2c
except:
# directly run this program
import modules.sensor.i2c.i2c


class ButtonIOExpander(i2c.i2c):

# The amount of available channels (8 for MCP23009)
CHANNELS = 8

# A button press counts as long press after this amount of milliseconds
LONG_PRESS_DURATION_MS = 1000

# After the button is pressed, it is disabled for this amount of milliseconds to prevent bouncing
DEBOUNCE_DURATION_MS = 80

# Button reads per second. Should be at least 20 for precise results
FPS = 30

_thread = None

def __init__(self, config, mcp):
# reset=False because the adafruit_mcp230xx library is resetting.
super().__init__(reset=False)

self.config = config
self.mcp = mcp

self._ms_per_frame = 1000 / self.FPS

# How many frames it takes to reach the LONG_PRESS_DURATION_MS
self._long_press_frames = int(self.LONG_PRESS_DURATION_MS // self._ms_per_frame)

# How many frames a button is disabled after release
self._debounce_frames = int(self.DEBOUNCE_DURATION_MS // self._ms_per_frame)

# The amount of frames a button is held
self._counter = [0] * self.CHANNELS
# Previous button state
self._previous = [0] * self.CHANNELS
# Saves the lock state of a Button
# NOTE: A button is getting locked to debounce or after long press to prevent that the short and long press event gets triggered simultaneously.
self._locked = [False] * self.CHANNELS

self.pins = []
for i in range(self.CHANNELS):
pin = self.mcp.get_pin(i)
pin.direction = Direction.INPUT
pin.pull = Pull.UP
self.pins.append(pin)

self._start_thread()

def _start_thread(self):
self._thread = Thread(target=self._run)
self._thread.daemon = True
self._thread.start()

def _run(self):
sleep_time = 1.0 / self.FPS

while True:
try:
self.read()
except:
app_logger.error(
f"I/O Expander connection issue! Resetting all buttons..."
)
# if an I2C error occurs due to e.g. connection issues, reset all buttons
for index in range(self.CHANNELS):
self._reset_button(index)
time.sleep(sleep_time)

def press_button(self, button, index):
try:
self.config.button_config.press_button("IOExpander", button, index)
except:
app_logger.warning(f"No button_config for button '{button}'")

def get_pressed_buttons(self):
return [not button.value for button in self.pins]

def read(self):
buttons_pressed = self.get_pressed_buttons()

for i, pressed in enumerate(buttons_pressed):
if pressed:
if not self._locked[i]:
self._counter[i] += 1

if self._counter[i] >= self._long_press_frames and not self._locked[i]:
self._on_button_pressed(i, True)
else:
if self._locked[i]:
if self._counter[i] <= self._debounce_frames:
self._counter[i] += 1
continue
else:
self._reset_button(i)
elif not self._locked[i] and self._previous[i] != 0:
self._on_button_pressed(i, False)
self._previous = buttons_pressed

def _on_button_pressed(self, button_index, long_press):
self._locked[button_index] = True
self._counter[button_index] = 0
self.press_button(f"GP{button_index}", int(long_press))

def _reset_button(self, button_index):
self._locked[button_index] = False
self._counter[button_index] = 0
14 changes: 13 additions & 1 deletion modules/sensor/sensor_i2c.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ def detect_sensors(self):
"BUTTON_SHIM"
] = self.detect_button_button_shim()

self.available_sensors["BUTTON"]["MCP23009"] = self.detect_button_mcp23009()

# battery
self.available_sensors["BATTERY"]["PIJUICE"] = self.detect_battery_pijuice()
self.available_sensors["BATTERY"]["PISUGAR3"] = self.detect_battery_pisugar3()
Expand Down Expand Up @@ -1542,6 +1544,16 @@ def detect_button_button_shim(self):
except:
return False

def detect_button_mcp23009(self):
try:
from .i2c.MCP23009 import MCP23009

# device test
self.sensor_mcp23009 = MCP23009(self.config)
return True
except:
return False

def detect_battery_pijuice(self):
try:
from pijuice import PiJuice
Expand All @@ -1565,4 +1577,4 @@ def detect_battery_pisugar3(self):
self.sensor_pisugar3 = PiSugar3()
return True
except:
return False
return False
Loading