Skip to content
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
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ The second command should identify an RP2040 MicroPython board.
Run these commands from the repository root. Supporting files and font assets are copied first; `main.py` is installed last as the automatic entry point.

```sh
mpremote connect auto fs cp configuration.py font_data.py font_renderer.py launch.py lcd_1inch28.py live_display.py params.json qmi8658.py settings.py splash.py timing.py touch_drive.py font_data*.bin startup_splash.rgb565 :
mpremote connect auto fs cp configuration.py font_data.py font_renderer.py hardware.py launch.py lcd_1inch28.py live_display.py params.json qmi8658.py settings.py splash.py timing.py touch_drive.py font_data*.bin startup_splash.rgb565 :
mpremote connect auto fs cp main.py :
mpremote connect auto reset
```
Expand All @@ -136,7 +136,14 @@ If first boot fails:
* The original text-only splash means `startup_splash.rgb565` is missing or has the wrong size; repeat the application upload command.
* An import error generally means a `.py` support module was omitted; repeat the upload command and keep `main.py` last.
* No serial device after flashing usually indicates a charge-only USB cable, an incorrect UF2, or a board still in BOOT mode.
* A missing touchscreen or IMU error indicates unsupported hardware or a board-level connection problem.
* A touchscreen hardware error is a controlled stop: check that this is the supported integrated board, then restart it. The serial message includes the failed operation or unexpected chip ID.
* An IMU hardware error disables Launch Mode for the current run. Swipe down to use the normal timer, which remains available without the IMU.

### Peripheral failure policy

The touchscreen is required for safe operation. A transient touchscreen I2C failure is retried three times at 100 ms intervals; an unexpected chip ID is not retried. If detection still fails, the firmware shows an actionable error, logs the detailed cause over serial, and stops before using an incomplete touch object.

The QMI8658 IMU is optional unless a non-zero Launch Mode sensitivity is selected. It is not initialized when Launch Mode is off. Transient initialization failures receive the same three attempts, while an unexpected chip ID fails immediately. If initialization or a launch-time sample fails, Launch Mode is disabled for the current run and the standard swipe-down timer remains available. The saved sensitivity is retained so the firmware can retry after a restart or the next Launch Mode configuration change.

## Configuration files

Expand Down
99 changes: 99 additions & 0 deletions hardware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Peripheral startup policy shared by the firmware and host-side tests."""

import time


class PeripheralError(Exception):
"""Base error for a peripheral that cannot be used safely."""

def __init__(self, peripheral, operation, detail, retryable=False):
self.peripheral = peripheral
self.operation = operation
self.detail = str(detail)
self.retryable = retryable
message = "{} {} failed: {}".format(peripheral, operation, self.detail)
super().__init__(message)


class PeripheralIOError(PeripheralError):
"""A potentially transient bus or register access failure."""

def __init__(self, peripheral, operation, detail):
super().__init__(peripheral, operation, detail, retryable=True)


class PeripheralIdentityError(PeripheralError):
"""A responding device does not have the expected chip identity."""

def __init__(self, peripheral, expected, actual):
self.expected = expected
self.actual = actual
detail = "expected chip ID 0x{:02X}, received 0x{:02X}".format(
expected, actual
)
super().__init__(peripheral, "detection", detail, retryable=False)


def _sleep_ms(clock, milliseconds):
sleep_ms = getattr(clock, "sleep_ms", None)
if sleep_ms is not None:
sleep_ms(milliseconds)
else:
clock.sleep(milliseconds / 1000)


def initialize_with_retry(
factory,
peripheral,
attempts=3,
retry_delay_ms=100,
clock=time,
logger=print,
):
"""Build a complete peripheral, retrying only transient I/O failures."""
if attempts < 1:
raise ValueError("attempts must be positive")

for attempt in range(1, attempts + 1):
try:
return factory()
except OSError as error:
failure = PeripheralIOError(peripheral, "initialization", error)
except PeripheralError as error:
failure = error

if not failure.retryable or attempt == attempts:
logger("Hardware error: {}".format(failure))
raise failure

logger(
"Hardware retry {}/{}: {}".format(attempt, attempts, failure)
)
_sleep_ms(clock, retry_delay_ms)


def initialize_optional_imu(sensitivity, factory, **retry_options):
"""Initialize the IMU only for Launch Mode and degrade on failure."""
if float(sensitivity) <= 0:
return None, None

logger = retry_options.get("logger", print)
try:
sensor = initialize_with_retry(factory, "QMI8658", **retry_options)
return sensor, None
except PeripheralError as error:
logger("Launch Mode disabled: {}".format(error))
return None, error


def show_hardware_message(lcd, title, lines, background=None):
"""Display a short, actionable hardware status message."""
if background is None:
background = lcd.red
lcd.fill(background)
lcd.write_centered(title, 48, 2, lcd.white)
y_position = 105
for line in lines:
lcd.write_centered(line, y_position, 1, lcd.white)
y_position += 30
lcd.show()
86 changes: 72 additions & 14 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
import time

from configuration import set_sensitivity, set_session
from hardware import (
PeripheralError,
initialize_optional_imu,
initialize_with_retry,
show_hardware_message,
)
from launch import accel_launch
from lcd_1inch28 import LCD_1inch28
from live_display import (
Expand All @@ -31,6 +37,29 @@
PLINE2 = [PIT_SESSION_MSG[1], None, 145, 2, "red"]


def _show_touch_failure(lcd, error):
print("Timer stopped: {}".format(error))
show_hardware_message(
lcd,
"Touch error",
["Touch not detected", "Check board / I2C", "Restart timer"],
)


def _show_imu_degraded(lcd, error):
print("Normal timing remains available: {}".format(error))
show_hardware_message(
lcd,
"Launch disabled",
["IMU not available", "Normal timer works", "Check board / I2C"],
background=lcd.brown,
)


def _initialize_imu(sensitivity):
return initialize_optional_imu(sensitivity, QMI8658)


def main():
system_params, user_params = load_configuration(PARAMS_FILE, USER_FILE)
duration_values = system_params["DURATION_VALUES"]
Expand All @@ -42,19 +71,29 @@ def main():

print("User Parameters: " + str(user_params))

# Gyro and accelerometer
qmi8658 = QMI8658()

# Display and touchscreen
lcd = LCD_1inch28()
lcd.set_bl_pwm(65535)
touch = Touch_CST816T(mode=1, LCD=lcd)
try:
touch = initialize_with_retry(
lambda: Touch_CST816T(mode=1, LCD=lcd),
"CST816T",
)
touch.BootScreen(lcd, version_number=version)
except PeripheralError as error:
_show_touch_failure(lcd, error)
return False

touch.BootScreen(lcd, version_number=version)
time.sleep(boot_delay_sec)

qmi8658, imu_error = _initialize_imu(user_params["SENSITIVITY"])
if imu_error is not None:
_show_imu_degraded(lcd, imu_error)
time.sleep(2)

while True:
sensitivity = user_params["SENSITIVITY"]
configured_sensitivity = user_params["SENSITIVITY"]
sensitivity = configured_sensitivity if qmi8658 is not None else 0
race_length = user_params["RACE_LENGTH"]
rest_length = user_params["REST_LENGTH"]

Expand Down Expand Up @@ -88,15 +127,28 @@ def main():
)
user_params, _ = persist_setting(USER_FILE, user_params, "REST_LENGTH", rest_length)
elif gesture == "up":
sensitivity = set_sensitivity(
configured_sensitivity = set_sensitivity(
LCD=lcd,
Touch=touch,
sensitivity_values=launch_sense_values,
sensitivity=sensitivity,
sensitivity=configured_sensitivity,
operation="Config",
back_colour="palegreen",
)
user_params, _ = persist_setting(USER_FILE, user_params, "SENSITIVITY", sensitivity)
user_params, _ = persist_setting(
USER_FILE,
user_params,
"SENSITIVITY",
configured_sensitivity,
)
if configured_sensitivity > 0 and qmi8658 is None:
qmi8658, imu_error = _initialize_imu(configured_sensitivity)
if imu_error is not None:
_show_imu_degraded(lcd, imu_error)
time.sleep(2)
sensitivity = (
configured_sensitivity if qmi8658 is not None else 0
)
elif gesture == "down":
print("Timer go!")
launch = True
Expand All @@ -112,11 +164,17 @@ def main():
else:
touch.GoScreen(lcd)

launch_detected = accel_launch(
qmi8658,
sensitivity=sensitivity,
cancel_check=lambda: touch.StopGesture(lcd),
)
try:
launch_detected = accel_launch(
qmi8658,
sensitivity=sensitivity,
cancel_check=lambda: touch.StopGesture(lcd),
)
except PeripheralError as error:
qmi8658 = None
_show_imu_degraded(lcd, error)
time.sleep(2)
continue
if not launch_detected:
print("Launch mode cancelled or timed out.")
continue
Expand Down
55 changes: 36 additions & 19 deletions qmi8658.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from machine import Pin,I2C,SPI,PWM,Timer,ADC
import framebuf
import time
Vbat_Pin = 29
from machine import I2C, Pin

from hardware import PeripheralIOError, PeripheralIdentityError

#Pin definition
I2C_SDA = 6
Expand All @@ -20,18 +19,37 @@


class QMI8658(object):
def __init__(self,address=0X6B):
def __init__(self,address=0X6B, bus=None):
self._address = address
self._bus = I2C(id=1,scl=Pin(I2C_SDL),sda=Pin(I2C_SDA),freq=100_000)
bRet=self.WhoAmI()
if bRet :
self.Read_Revision()
else :
return NULL
self.Config_apply()
try:
self._bus = bus
if self._bus is None:
self._bus = I2C(
id=1,
scl=Pin(I2C_SDL),
sda=Pin(I2C_SDA),
freq=100_000,
)
except OSError as error:
raise PeripheralIOError("QMI8658", "I2C setup", error)

try:
chip_id = self._read_byte(0x00)
except OSError as error:
raise PeripheralIOError("QMI8658", "chip ID read", error)
if chip_id != 0x05:
raise PeripheralIdentityError("QMI8658", 0x05, chip_id)

try:
self.revision = self.Read_Revision()
self.Config_apply()
except OSError as error:
raise PeripheralIOError("QMI8658", "configuration", error)

def _read_byte(self,cmd):
rec=self._bus.readfrom_mem(int(self._address),int(cmd),1)
if len(rec) != 1:
raise OSError("short I2C read")
return rec[0]
def _read_block(self, reg, length=1):
rec=self._bus.readfrom_mem(int(self._address),int(reg),length)
Expand Down Expand Up @@ -68,14 +86,13 @@ def Config_apply(self):

def Read_Raw_XYZ(self):
xyz=[0,0,0,0,0,0]
raw_timestamp = self._read_block(0x30,3)
raw_acc_xyz=self._read_block(0x35,6)
raw_gyro_xyz=self._read_block(0x3b,6)
raw_xyz=self._read_block(0x35,12)
timestamp = (raw_timestamp[2]<<16)|(raw_timestamp[1]<<8)|(raw_timestamp[0])
try:
raw_xyz=self._read_block(0x35,12)
except OSError as error:
raise PeripheralIOError("QMI8658", "sample read", error)
if len(raw_xyz) != 12:
raise PeripheralIOError("QMI8658", "sample read", "short I2C read")
for i in range(6):
# xyz[i]=(raw_acc_xyz[(i*2)+1]<<8)|(raw_acc_xyz[i*2])
# xyz[i+3]=(raw_gyro_xyz[((i+3)*2)+1]<<8)|(raw_gyro_xyz[(i+3)*2])
xyz[i] = (raw_xyz[(i*2)+1]<<8)|(raw_xyz[i*2])
if xyz[i] >= 32767:
xyz[i] = xyz[i]-65535
Expand Down
Loading
Loading