From 480bbd39b35ae18551154226503041fea6d0aafd Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:57:56 +0100 Subject: [PATCH 1/3] removed stepper support --- .github/copilot-instructions.md | 440 +++++-------------------- EEPROM/hexdrive.py | 64 +--- README.md | 13 +- app.py | 80 ++--- copilot_instructions.md | 402 ----------------------- dev/build_release.py | 1 - dev/download_to_device.py | 1 - hexpansion_mgr.py | 7 +- stepper_test.py | 557 -------------------------------- tests/conftest.py | 6 +- tests/test_hexpansion.py | 89 +---- tests/test_smoke.py | 6 +- 12 files changed, 115 insertions(+), 1551 deletions(-) delete mode 100644 copilot_instructions.md delete mode 100644 stepper_test.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4d8f157..91d543f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,402 +1,106 @@ -# BadgeBot — Copilot Instructions +# BadgeBot Copilot Instructions -## Project Overview +This is the canonical Copilot instructions file for BadgeBot. -**BadgeBot** is a MicroPython application for the **Tildagon badge** (EMF Camp 2024). It is the companion app for the **HexDrive** hexpansion — a motor/servo/stepper driver board that plugs into any of the badge's six hexpansion slots. +Keep this file concise and evergreen: +- Do not include hardcoded line numbers. +- Do not include "currently X" snapshots that quickly go stale. +- Prefer naming symbols/files over pinning implementation trivia. -The app lives at `sim/apps/BadgeBot/` inside the `badge-2024-software` repository and is submoduled there so the **badge simulator** (`sim/run.py`) can run it directly. It also runs on real badge hardware. +## Scope -- **Author**: Team Robotmad -- **License**: LGPL-3.0-only -- **App version**: defined as `APP_VERSION` in `app.py` (currently `"1.5"`) -- **HexDrive firmware version**: `VERSION` in `hexdrive.py` (integer, currently `6`) -- **Repository**: +These instructions apply to work in this package: +- `sim/apps/BadgeBot/` ---- +Unless explicitly requested, avoid editing files outside this package. -## Running the App +## Project Context -### Simulator -```bash -# From the sim directory: -pipenv run python run.py -``` -The simulator is detected at runtime via `sys.platform != "esp32"` and stored in `_IS_SIMULATOR`. - -### Real Badge -Install via the [Tildagon App Directory](https://apps.badge.emfcamp.org/) or copy the required runtime files onto the badge (as `.mpy` compiled files or `.py` source): -- `tildagon.toml`, `metadata.json` -- `app.py` / `app.mpy` -- `hexdrive.mpy` -- `utils.mpy` -- `hexpansion_mgr.mpy` -- `motor_controller.mpy` -- `motor_moves.mpy` -- `servo_test.mpy` -- `stepper_test.mpy` -- `settings_mgr.mpy` -- `line_follow.mpy` -- `autotune.mpy` -- `autotune_mgr.mpy` -- `sensor_manager.mpy` -- `sensor_test.mpy` - -Use `dev/build_release.py` to compile and package all modules for deployment. For incremental development deployment use `dev/download_to_device.py`. - ---- - -## File Map - -| File / Directory | Purpose | -|---|---| -| `__init__.py` | Re-exports `BadgeBotApp` from `app.py` | -| `app.py` | **Main application** (~2 280 lines). Contains `BadgeBotApp`, the state machine, UI, motor/servo/stepper control, hexpansion management, EEPROM programming, settings, drawing routines, and helper classes (`Stepper`, `HexDriveType`, `MySetting`, `Instruction`, `StepperMode`, `ServoMode`). | -| `hexdrive.py` | **HexDrive EEPROM app** (~548 lines). `HexDriveApp(app.App)` is the firmware stored on the HexDrive's EEPROM and executed by BadgeOS. Manages PWM outputs, motor driving, servo positioning, stepper phases, power control (SMPSU), and a keep-alive watchdog. Also contains a local `HexDriveType` class. Exports `__app_export__ = HexDriveApp`. | -| `sensor_manager.py` | **SensorManager** — opens an I²C port, scans for known sensors (auto-discovery), and manages the currently selected sensor for the sensor-test UI mode. | -| `sensors/` | Package of I²C sensor drivers, all inheriting from `SensorBase`. | -| `utils.py` | Shared helpers: `roundtext()`, `draw_logo_animated()`, `draw_QRCode()`, `parse_version()`, `chain()`. | -| `uQR.py` | QR code generation library (micro-QR for MicroPython). | -| `metadata.json` | App metadata for BadgeOS loader: `{ "callable": "BadgeBotApp", "name": "BadgeBot" }`. | -| `tildagon.toml` | Tildagon app manifest (name, category, author, license, version, URL, description). | -| `tests/test_smoke.py` | Pytest smoke tests: import checks, `__app_export__` consistency, version parity between `app.py` and `hexdrive.py`. | -| `dev/` | Dev tooling: `build_release.py`, `dev_requirements.txt`. | -| `.github/workflows/` | CI workflow(s). | - ---- - -## Architecture & State Machine - -### BadgeBotApp (app.py) - -`BadgeBotApp` extends `app.App` (the Tildagon app base class) and uses a **manager-based state-machine** pattern. The app holds a `current_state` integer and routes `update(delta)`, `draw(ctx)`, and `background_update(delta)` calls through dispatch tables populated at startup. - -**State constants** (defined at module level in `app.py`): - -| Constant | Value | Description | -|---|---|---| -| `STATE_MENU` | 0 | Main menu | -| `STATE_MESSAGE` | 1 | General message / notification display | -| `STATE_LOGO` | 2 | Animated logo screen | -| `STATE_COUNTDOWN` | 3 | Shared countdown (Motor Moves & PID AutoTune) | -| `STATE_SETTINGS` | 4 | Settings editor (managed by `SettingsMgr`) | -| `STATE_MOTOR_MOVES` | 5 | Programmed motor-move sequence (managed by `MotorMovesMgr`) | -| `STATE_SERVO` | 6 | Servo test UI (managed by `ServoTestMgr`) | -| `STATE_STEPPER` | 7 | Stepper test UI (managed by `StepperTestMgr`) | -| `STATE_FOLLOWER` | 8 | Line-follower mode (managed by `LineFollowMgr`) | -| `STATE_AUTOTUNE` | 9 | PID auto-tune (managed by `AutotuneMgr`) | -| `STATE_SENSOR` | 10 | I²C sensor test (managed by `SensorTestMgr`) | -| `STATE_AUTODRIVE` | 11 | Autonomous drive (managed by `AutoDriveMgr`) | -| `STATE_HEXPANSION` | 12 | Hexpansion management (sub-states managed by `HexpansionMgr`) | - -**State management pattern**: `update(delta)` calls `_update_main_application(delta)`, which: -1. Handles the `STATE_MENU` and `STATE_MESSAGE`/`STATE_LOGO` states directly. -2. For all other states, looks up the manager's `update` function in `_state_update_dispatch` and calls it. - -`draw(ctx)` and `background_update(delta)` follow the same dispatch pattern via `_state_draw_dispatch` and `_state_background_dispatch`. - -**`HexpansionMgr` sub-states** (module-level constants in `hexpansion_mgr.py`, prefixed `_SUB_*`): - -| Constant | Value | Description | -|---|---|---| -| `_SUB_INIT` | 0 | Initial state, before first port scan | -| `_SUB_CHECK` | 1 | Scanning ports for EEPROMs and HexDrives | -| `_SUB_DETECTED` | 2 | Hexpansion detected, ready for EEPROM init | -| `_SUB_ERASE_CONFIRM` | 3 | Confirmation prompt for EEPROM erase | -| `_SUB_ERASE` | 4 | EEPROM erase in progress | -| `_SUB_UPGRADE_CONFIRM` | 5 | Ready for firmware upgrade | -| `_SUB_PROGRAMMING` | 6 | EEPROM programming in progress | -| `_SUB_PORT_SELECT` | 7 | Selecting which hexpansion to erase | - -When adding or modifying state-driven behaviour, always inspect the current state constants, manager registrations, and dispatch logic in `app.py` — treat that file as the source of truth. - -### Event-Driven Architecture - -The app uses the Tildagon **eventbus** for: -- `HexpansionInsertionEvent` / `HexpansionRemovalEvent` — hot-plug hexpansion detection -- `RequestForegroundPushEvent` / `RequestForegroundPopEvent` — focus management -- `PatternDisable` / `PatternEnable` — LED pattern control -- `ButtonUpEvent` — button unpress handling (used in instruction-entry mode) - -Button input uses the `Buttons` helper with types: `UP`, `DOWN`, `LEFT`, `RIGHT`, `CANCEL`, `CONFIRM` and supports long-press detection and auto-repeat with acceleration. - ---- - -## Key Classes - -### `BadgeBotApp` (app.py, line 209) -Main app class. Key methods: - -| Method | Description | -|---|---| -| `update(delta)` | Main update loop tick; routes to state handlers | -| `draw(ctx)` | Main draw; routes to state-specific draw methods | -| `background_update(delta)` | Keep-alive and periodic tasks | -| `_apply_fwd_dir(output)` | Negates all motor outputs when `fwd_dir=1`; called at every `set_motors` callsite so the setting applies to both programmed moves and auto drive | -| `_set_direction_leds(direction)` | Lights the two LEDs corresponding to the given direction, rotated by `front_face` | -| `_scan_ports()` | Scan all 6 hexpansion ports for HexDrives | -| `check_port_for_hexdrive(port)` | Check if a specific port has a HexDrive | -| `_update_app_in_eeprom(port, addr)` | Write HexDrive firmware to EEPROM | -| `_prepare_eeprom(port, addr)` | Initialize blank EEPROM as HexDrive | -| `_erase_eeprom(port, addr)` | Erase EEPROM back to 0xFF | -| `find_hexdrive_app(port)` | Find the running HexDriveApp instance for a port | -| `set_menu(menu_name)` | Switch between "main" and "settings" menus | -| `reset_robot()` | Clear instructions and reset motor state | -| `get_current_power_level(delta)` | Ramp motor power with acceleration limiting | -| `finalize_instruction()` | Complete current instruction and advance | -| `reset_servo()` | Reset all servos to defaults | - -### `HexDriveApp` (hexdrive.py, line 34) -Runs on the EEPROM per-hexpansion. Key methods: - -| Method | Description | -|---|---| -| `initialise()` | Set up pins, detect HexDrive type, init PWM | -| `deinitialise()` | Release PWM, turn off power | -| `set_power(state)` | Enable/disable SMPSU boost converter | -| `get_booster_power()` | Check if external power is present | -| `set_motors(outputs)` | Set motor power levels (tuple of duty cycles) | -| `set_servoposition(channel, position)` | Set servo pulse width in µs | -| `set_servocentre(centre, channel)` | Set servo trim/centre | -| `set_freq(freq, channel)` | Set PWM frequency | -| `set_pwm(duty_cycles)` | Raw PWM duty cycle control | -| `motor_step(phase)` | Drive stepper to specific phase | -| `motor_release()` | De-energise stepper coils | -| `background_update(delta)` | Keep-alive watchdog; zeros outputs on timeout | - -### `Stepper` (app.py, line 1929) -Software stepper motor controller using a hardware `Timer` for step timing. - -| Method | Description | -|---|---| -| `step(d)` | Single step in direction d (+1/-1) | -| `free_run(d)` | Continuous stepping via timer | -| `track_target()` | Move towards target position via timer | -| `stop()` | Stop timer and halt stepping | -| `speed(sps)` | Set speed in full steps per second | -| `target(t)` | Set target position in half-steps | -| `enable(e)` / `is_enabled()` | Enable/disable stepper | - -### `Instruction` (app.py, line 2221) -Represents a single movement command in a user-programmed sequence. - -| Method | Description | -|---|---| -| `press_type` | Direction (`UP`/`DOWN`/`LEFT`/`RIGHT`) | -| `inc()` | Increase repetition count | -| `directional_power_tuple(power)` | Get motor power tuple for this direction | -| `directional_duration(settings)` | Get duration based on direction type | -| `make_power_plan(settings)` | Generate time-power schedule for execution | +BadgeBot is a MicroPython app for the EMF Camp Tildagon badge. -### `MySetting` (app.py, line 2141) -Persistable setting with min/max bounds and auto-repeat increment/decrement. +Core capabilities: +- HexDrive motor/servo control via hexpansion. +- Sensor probing/testing via I2C sensor drivers. +- App UI and mode switching through a manager/state-machine pattern. +- Simulator support for desktop iteration. -### `HexDriveType` (app.py, line 2131) -Describes a HexDrive variant (PID, motor count, servo count, stepper count, name). +## Primary Files -### `StepperMode` / `ServoMode` (app.py, lines 164/186) -Enum-like classes for stepper (OFF/POSITION/SPEED) and servo (OFF/TRIM/POSITION/SCANNING) modes. +- `app.py`: Main app (`BadgeBotApp`), state routing, menus, draw/update loops. +- `hexdrive.py`: EEPROM app (`HexDriveApp`) controlling PWM/motors/servos and keep-alive safety. +- `hexpansion_mgr.py`: Port scanning, EEPROM prep/program/erase, HexDrive lifecycle. +- `motor_controller.py`: Higher-level movement control and assisted maneuvers. +- `motor_moves.py`, `servo_test.py`, `line_follow.py`, `autotune_mgr.py`, `sensor_test.py`, `autodrive.py`: Mode managers. +- `sensor_manager.py` + `sensors/`: Sensor discovery and sensor driver implementations. +- `utils.py`: Shared UI/drawing and helper utilities. ---- +## Architecture Guidance -## Sensor Subsystem +`BadgeBotApp` is the orchestration layer: +- It owns state and dispatch tables for `update`, `draw`, and optional background tasks. +- It delegates mode behavior to manager classes. +- It interacts with HexDrive apps discovered/managed through the platform scheduler and hexpansion APIs. -### SensorBase (sensors/sensor_base.py) -Abstract base class. Subclasses must implement: -- `I2C_ADDR: int` — 7-bit I²C address -- `NAME: str` — display name -- `READ_INTERVAL_MS: int` — how often to read the sensor (ms) -- `_init() -> bool` — hardware init -- `_measure() -> dict` — take measurement, return `{label: value_str}` -- `_shutdown()` — optional power-down +When changing behavior: +- Prefer adding/changing manager logic rather than embedding mode-specific code in unrelated modules. +- Keep responsibilities separated: UI/rendering, state transitions, motor control, sensor IO. -Provides helpers: `_write_reg()`, `_read_reg()`, `_read_u8()`, `_read_u16_le()`, `_read_u16_be()`, `_write_u8()`. +## Simulator vs Hardware -### Supported Sensors +BadgeBot runs in both simulator and on-badge environments. -| Driver | Sensor | I²C Addr | Measurements | -|---|---|---|---| -| `vl53l0x.py` | VL53L0X ToF | 0x29 | distance (mm) | -| `vl6180x.py` | VL6180X ToF + ALS | 0x29 | range (mm), lux | -| `tcs3472.py` | TCS3472 Colour | 0x29 | RGBC, CCT, lux | -| `tcs3430.py` | TCS3430 Colour | 0x39 | XYZI | +When editing: +- Guard hardware-only paths with platform checks where needed. +- Do not assume desktop-only modules exist on-device. +- Keep simulator fakes compatible with call signatures used by app code. -### SensorManager (sensor_manager.py) -- `open(port)` — Open I²C, scan, auto-instantiate matching sensor drivers -- `close()` — Shutdown all sensors, release bus -- `read_current()` — Read from selected sensor -- `next_sensor()` / `prev_sensor()` — Cycle through found sensors -- `select_sensor(name)` — Select by name +## Versioning Rules (HexDrive) ---- +If `hexdrive.py` behavior or interface changes: +1. Bump `VERSION` in `hexdrive.py`. +2. Bump the matching app-side HexDrive version constant used for compatibility checks. +3. Rebuild/update any generated `.mpy` artifact used for EEPROM programming. +4. Ensure related smoke/version tests still pass. -## HexDrive Hexpansion +Never change only one side of app/HexDrive version pairing. -The HexDrive is a custom hexpansion PCB with: -- **4 high-side PWM outputs** for motors/servos (via badge HS pins) -- **2 low-side pins**: one for SMPSU enable, one for power-detect -- **EEPROM** (8 KB, I²C address 0x50, 16-bit addressing) storing the hexpansion header and `hexdrive.py` (as `app.py`/`.mpy`) -- **SMPSU** boost converter for motor power from USB/battery +## Coding Conventions -### HexDrive Types (Flavours) +- Target MicroPython constraints: keep allocations and per-frame overhead low. +- Use relative imports inside the package. +- Keep public API names stable unless a migration plan is included. +- Prefer clear, small changes over broad refactors. +- Maintain existing formatting and style in touched files. -| PID | Name | Motors | Servos | Steppers | PWM Channels Used | -|---|---|---|---|---|---| -| 0xCBCA | 2 Motor | 2 | 0 | 0 | 2 (trick: swap active signal on direction change) | -| 0xCBCC | 4 Servo | 0 | 4 | 0 | 4 | -| 0xCBCD | 1 Mot 2 Srvo | 1 | 2 | 0 | 3 | -| 0xCBCE | Stepper | 0 | 0 | 1 | 4 | -| 0xCBCB | Unknown | 2 | 4 | 0 | 4 | +## Testing and Validation -ESP32 has 8 PWM channels total, so max 2 "4-channel" HexDrives or 4 "2 Motor" HexDrives simultaneously. +From `sim/apps/BadgeBot/`: ---- - -## Settings System - -Settings are stored as `MySetting` objects in `self._settings` dict. Each has a default, min, and max. Settings are persisted using the badge `settings` module. - -| Key | Default | Description | -|---|---|---| -| `acceleration` | 7500 | Motor power ramp rate per tick | -| `max_power` | 65535 | Maximum PWM duty cycle | -| `drive_step_ms` | 50 | Duration per drive step | -| `turn_step_ms` | 20 | Duration per turn step | -| `servo_step` | 10 | Servo pulse increment (µs) | -| `servo_range` | 1000 | Servo range (±µs from centre) | -| `servo_period` | 20 | Servo PWM period (ms) | -| `brightness` | 1.0 | LED brightness | -| `logging` | False | Console logging | -| `step_max_pos` | 3100 | Stepper max position (half-steps) | -| `fwd_dir` | 0 | Motor direction: `0`=Normal (HexDrive faces away from robot front), `1`=Reversed (HexDrive faces toward robot front). Applied by `_apply_fwd_dir()` at every `set_motors` call — affects programmed moves **and** auto drive. Display labels: `Normal` / `Reverse`. | -| `front_face` | 0 | Which physical face of the badge is the robot's front, for LED indicators only (does **not** affect motors). 12 positions clockwise: `0`=BtnA (corner between slot 6 & 1, default top), `1`=Slot 1, `2`=BtnB … `10`=BtnF, `11`=Slot 6. Corners A–F match the badge's physical buttons. | - -### Orientation System Design - -The two orientation settings are **independent** and affect different subsystems: - -- **`fwd_dir`** — a single hardware-polarity switch applied at `set_motors`. Negating every output element correctly handles forward, reverse, and both turn directions simultaneously (since left/right is relative to forward). It also applies to auto drive (cruise, scan spin, turn) without any extra code in those paths. Display: `Normal` / `Reverse`. - -- **`front_face`** — a 0–11 ring position (each step = 30° clockwise). `_set_direction_leds()` offsets the lit LED pair from the front position: forward = `front_face`, right = `+2`, backward = `+6`, left = `+8`. Position 0 is the top corner (BtnA, between slot 6 & slot 1). Motor logic is not affected. - ---- - -## Important Constants - -```python -_TICK_MS = 10 # Smallest motor update interval (ms) -_USER_DRIVE_MS = 50 # Default drive step duration (ms) -_USER_TURN_MS = 20 # Default turn step duration (ms) -_LONG_PRESS_MS = 750 # Long-press threshold (ms) -_RUN_COUNTDOWN_MS = 5000 # Countdown before execute (ms) -_AUTO_REPEAT_MS = 200 # Button auto-repeat interval (ms) - -_EEPROM_ADDR = 0x50 -_EEPROM_PAGE_SIZE = 32 -_EEPROM_TOTAL_SIZE = 8192 # 64 Kbit = 8 KB - -_MAX_POWER = 65535 # Full-scale PWM duty -_SERVO_DEFAULT_CENTRE = 1500 # Servo centre (µs) -_MAX_SERVO_RANGE = 1400 # Maximum servo range (µs) - -# Orientation constants -_FWD_DIR_DEFAULT = 0 -_FWD_DIR_LABELS = ("Normal", "Reverse") # display labels for fwd_dir setting -_FRONT_FACE_DEFAULT = 0 -_FRONT_FACE_LABELS = ( # display labels for front_face setting - "BtnA", # 0 - corner between slot 6 & slot 1 (default top) - "Slot 1", # 1 - "BtnB", # 2 - corner between slot 1 & slot 2 - "Slot 2", # 3 - "BtnC", # 4 - "Slot 3", # 5 - "BtnD", # 6 - corner between slot 3 & slot 4 (bottom) - "Slot 4", # 7 - "BtnE", # 8 - "Slot 5", # 9 - "BtnF", # 10 - "Slot 6", # 11 -) -``` - ---- - -## Badge Framework APIs Used - -The app depends on these Tildagon/BadgeOS APIs: - -| Module | Usage | -|---|---| -| `app.App` | Base application class (`update()`, `draw()`, `background_update()`) | -| `events.input` | `Buttons`, `Button`, `ButtonUpEvent`, `BUTTON_TYPES` | -| `app_components` | `Menu`, `Notification`, display tokens | -| `system.eventbus` | Publish/subscribe event system | -| `system.hexpansion.*` | Hexpansion detection, header read/write, block devices | -| `system.scheduler` | App lifecycle events (foreground push/pop, stop) | -| `system.patterndisplay.events` | LED pattern enable/disable | -| `machine` | `I2C`, `PWM`, `Pin`, `Timer` | -| `tildagonos` | LED control (`tildagonos.leds[n]`) | -| `settings` | Persistent key-value storage | -| `ota` | `get_version()` for badge firmware version check | -| `vfs` | Virtual filesystem for EEPROM mounting | -| `ctx` | 2D drawing context (rectangles, text, arcs, etc.) | - ---- - -## Development Notes - -### Coding Patterns -- **MicroPython target**: code must run on ESP32 with limited RAM. Avoid large allocations, prefer generators (`chain()`), and use lazy imports where possible. -- **Relative imports**: within the package use `from .utils import ...`, `from .sensors import ...`. -- **`__app_export__`**: every module that can be loaded as an app must set this to the main class at module level. -- **`_IS_SIMULATOR`**: `sys.platform != "esp32"` — use this to guard hardware-only code paths. The Tildagon badge uses an ESP32-S3 so `sys.platform` is `"esp32"` on real hardware. -- **Keep-alive pattern**: `HexDriveApp.background_update()` zeros outputs if no command arrives within `_keep_alive_period` ms — the main app must send updates faster than this. - -### Testing ```bash -cd sim/apps/BadgeBot pytest tests/ ``` -Smoke tests verify imports, `__app_export__` consistency, and that `HEXDRIVE_APP_VERSION` in `app.py` matches `VERSION` in `hexdrive.py`. - -### HexDrive Version Bump (MANDATORY when hexdrive.py changes) -Whenever `hexdrive.py` is modified, you **must** perform all three steps: +For simulator smoke checks (from repo root): -1. **Bump `VERSION`** in `hexdrive.py` (integer at the top of the file). -2. **Bump `HEXDRIVE_APP_VERSION`** in `app.py` (and `linefollower.py` if present) to the **same** integer. This is how the app detects that the EEPROM firmware is out-of-date and prompts the user to reprogram. -3. **Rebuild the `.mpy`** by running from the BadgeBot directory: - ```bash - mpy-cross -v EEPROM/hexdrive.py - ``` - This produces `hexdrive.mpy`, which is what actually gets written to the HexDrive EEPROM. - -The smoke tests (`tests/test_smoke.py::test_app_versions_match`) will fail if the versions diverge. - -### Current Development Branch — Sensor Integration - -This branch is adding **sensor capabilities** to BadgeBot: +```bash +python sim/run.py --screenshot BadgeBot.BadgeBotApp +``` -- **Current approach**: sensors sit on a **separate "dumb" hexpansion** (one without an EEPROM) plugged into another slot. The `SensorManager` and `sensors/` package handle I²C discovery and reading on that port independently of the HexDrive. -- **Future goal**: integrate sensor hardware directly onto a **new revision of the HexDrive PCB**, so a single hexpansion provides both motor driving and sensor input. When that happens, `hexdrive.py` will need new initialisation and reading logic for the on-board sensors, which will trigger a version bump and EEPROM reprogram cycle as described above. -- **Design principle**: keep the sensor abstraction (`SensorBase` / `SensorManager`) cleanly separated from motor control so that the transition from "separate hexpansion" to "on-board sensors" requires minimal refactoring in `app.py`. +After changes, verify: +- App imports cleanly. +- Simulator launches without tracebacks. +- Relevant tests pass. -### Editing Scope -**Only edit files inside `sim/apps/BadgeBot/`** unless explicitly instructed otherwise. The surrounding `badge-2024-software` repo is the badge firmware — treat it as read-only context. +## Common Pitfalls -### Key Relationships -``` -BadgeBotApp (app.py) - ├── finds HexDriveApp instances via scheduler.apps - ├── programs hexdrive.py onto EEPROM - ├── sends motor/servo commands to HexDriveApp - ├── uses SensorManager for sensor test mode - └── uses utils.py for drawing helpers +- Updating HexDrive logic without syncing app-side compatibility version. +- Mixing simulator-only assumptions into badge runtime paths. +- Editing outside `sim/apps/BadgeBot/` without explicit requirement. +- Adding brittle docs tied to exact file lengths or line numbers. -HexDriveApp (hexdrive.py) - ├── runs from EEPROM, managed by BadgeOS - ├── owns PWM outputs, SMPSU control - └── implements keep-alive safety timeout +## Documentation Maintenance -SensorManager (sensor_manager.py) - └── auto-discovers sensors via sensors/ package -``` +When you update architecture or behavior: +- Update this file only for durable guidance. +- Keep operational details short and actionable. +- Remove or rewrite stale statements instead of adding caveats. diff --git a/EEPROM/hexdrive.py b/EEPROM/hexdrive.py index 5d48959..7cb0a83 100644 --- a/EEPROM/hexdrive.py +++ b/EEPROM/hexdrive.py @@ -33,19 +33,6 @@ _MAX_SERVO_RANGE = 1400 # 1400us either side of centre (VERY WIDE) _SERVO_MAX_TRIM = 1000 # 1000us either side of centre for trimming the centre position -# Stepper Motor Constants -_STEPPER_NUM_PHASES = 8 # Number of phases in the stepping sequence (this includes half steps) -_STEPPER_SEQUENCE = ( - (1, 0, 1, 0), - (0, 0, 1, 0), - (0, 1, 1, 0), - (0, 1, 0, 0), - (0, 1, 0, 1), - (0, 0, 0, 1), - (1, 0, 0, 1), - (1, 0, 0, 0), -) - # EEPROM Constants _EEPROM_ADDR = 0x50 # I2C address of the EEPROM on the HexDrive and HexSense Hexpansion _EEPROM_NUM_ADDRESS_BYTES = 2 # Number of bytes used for the memory address when reading from the EEPROM (e.g. 2 for 16-bit addressing) @@ -57,21 +44,19 @@ class HexDriveType: """Represents a sub-type of HexDrive Hexpansion module.""" - __slots__ = ("pid", "name", "motors", "servos", "steppers") + __slots__ = ("pid", "name", "motors", "servos") - def __init__(self, pid_byte: int, motors: int = 0, servos: int = 0, steppers: int = 0, name: str = "Unknown"): + def __init__(self, pid_byte: int, motors: int = 0, servos: int = 0, name: str = "Unknown"): self.pid: int = pid_byte # Product ID byte read from the EEPROM to identify the type of HexDrive self.name: str = name # A friendly name for the type of HexDrive self.motors: int = motors # Number of motor channels supported by this type of HexDrive (0, 1 or 2) self.servos: int = servos # Number of servo channels supported by this type of HexDrive (0, 2 or 4) - self.steppers: int = steppers # Number of stepper motors supported by this type of HexDrive (0 or 1) _HEXDRIVE_TYPES = ( HexDriveType(0xCA, motors=2, name="2 Motor"), - HexDriveType(0xCB, motors=2, servos=4, steppers=1), + HexDriveType(0xCB, motors=2, servos=4), HexDriveType(0xCC, servos=4, name="4 Servo"), HexDriveType(0xCD, motors=1, servos=2, name="1 Mot 2 Srvo"), - HexDriveType(0xCE, steppers=1, name="1 Stepper"), ) class HexDriveApp(app.App): # pylint: disable=no-member @@ -89,7 +74,6 @@ def __init__(self, config: HexpansionConfig | None = None): self.PWMOutput: list[PWM | None] = [None] * _MAX_NUM_CHANNELS self._freq: list[int] = [0] * _MAX_NUM_CHANNELS self._motor_output: list[int] = [0] * _MAX_NUM_MOTORS - self._stepper: bool = False if config is None: print("D:No Config") return @@ -169,7 +153,7 @@ async def _handle_stop_app(self, event): def background_update(self, delta: int): """ This is called from the main loop of the BadgeOS to allow the app to do any background processing it needs to do. """ - if (self.config is None) or not (self._pwm_setup or self._stepper): + if (self.config is None) or not self._pwm_setup: # if we are not properly initialised then do not attempt to do anything return # Check keep alive period and turn off PWM outputs if exceeded @@ -189,8 +173,6 @@ def background_update(self, delta: int): except Exception as e: # pylint: disable=broad-except print(self._pwm_log_string(channel) + f"Off failed {e}") self.PWMOutput[channel] = None # Tidy Up - elif self._stepper: - self.motor_release() # we keep retriggering in case anything else has corrupted the PWM outputs @@ -201,7 +183,7 @@ def get_version(self) -> int: def get_status(self) -> bool: """ Get the current status of the app - True if the app is running and able to respond to commands, False if not. """ - return (self._pwm_setup or self._stepper) + return (self._pwm_setup) def set_logging(self, state: bool): @@ -344,7 +326,6 @@ def set_servoposition(self, channel: int | None = None, position: int | None = N return False self._outputs_energised = True - self._stepper = False self._time_since_last_update = 0 return True @@ -416,37 +397,6 @@ def set_pwm(self, duty_cycles: tuple[int, ...]) -> bool: return True -### Stepper Motor Support - -# -------------------------------------------------- -# Public methods for controlling stepper motors. -# --------------------------------------------------- - - def motor_step(self, phase: int) -> int | None: - """ Step the motor to a specific phase in the stepping sequence. Returns None if failed (e.g. invalid phase or not configured for stepper), - otherwise returns the phase that was set. The phase is a value from 0 to _STEPPER_NUM_PHASES-1 which corresponds to the - stepping sequence defined in _STEPPER_SEQUENCE.""" - if phase >= _STEPPER_NUM_PHASES or self._hexdrive_type.steppers == 0: - return None - if not self._stepper: - # not currently configured for stepper motor - configure - self._pwm_deinit() - self._stepper = True - for channel, value in enumerate(_STEPPER_SEQUENCE[phase]): - self.config.pin[channel].value(value) - self._outputs_energised = True - self._time_since_last_update = 0 - return phase - - - def motor_release(self): - """ Release the motor by setting all outputs to low. This will stop the motor and allow it to be turned by hand. """ - for channel in range(4 if self._stepper else (self._hexdrive_type.motors * 2)): - self.config.pin[channel].value(0) - self._outputs_energised = False - self._time_since_last_update = 0 - - # -------------------------------------------------- # Private methods for internal use only. # -------------------------------------------------- @@ -477,9 +427,7 @@ def _pwm_init(self) -> bool: # ignore the remaining channels - we will switch them on when needed pass if 0 < self._freq[channel]: - if self._set_pwmoutput(channel, 0): - self._stepper = False - else: + if not self._set_pwmoutput(channel, 0): return False self._pwm_setup = True return self._pwm_setup diff --git a/README.md b/README.md index 8b6667e..e080a96 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BadgeBot app -Companion app for the HexDrive hexpansion. Supports 2 brushed DC motors, 4 RC servos, 1 motor + 2 servos, or a single two-phase stepper. Features Logo-style motor programming, PID line following with automatic gain tuning, I²C sensor testing, servo/stepper test modes, and persistent settings management. +Companion app for the HexDrive hexpansion. Supports 2 brushed DC motors, 4 RC servos, 1 motor + 2 servos. Features Logo-style motor programming, PID line following with automatic gain tuning, I²C sensor testing, servo test mode, and persistent settings management. This guide is current for BadgeBot version 1.5 @@ -14,10 +14,9 @@ If your HexDrive software (stored on the EEPROM on the hexpansion) is not the la - 2 Motor - 4 Servo - 1 Motor and 2 Servos -- Stepper - Unknown -The board can drive 2 brushed DC motors, 4 RC servos, 1 DC motor and 2 servos or a single two phase Stepper Motor. +The board can drive 2 brushed DC motors, 4 RC servos, 1 DC motor and 2 servos. Once you have selected the desired 'flavour' - please confirm by pressing the "C" (confirm) button. There must be a HexDrive board plugged in and running the latest software to use the BadgeBot app. If this is not the case then you will see a warning that you need a HexDrive with a reference to this repo. @@ -27,7 +26,6 @@ There must be a HexDrive board plugged in and running the latest software to use The main menu presents the following options: - **Line Follower** – PID-controlled line following using a HexSense with QTRX reflectance sensors - **Motor Moves** – Logo/turtle-style motor programming (record UP/DOWN/LEFT/RIGHT sequences, then execute) -- **Stepper Test** – Test and control a single stepper motor (position and speed modes) - **Servo Test** – Test up to 4 RC servos (position, trim, and scanning modes) - **PID Auto Tune** – Automatic PID gain tuning using relay feedback (Åström-Hägglund method) - **Settings** – Adjust configurable parameters (see below) @@ -62,7 +60,6 @@ The main menu includes a sub-menu of Settings which can be adjusted. |------------------|-------------------------------------------|----------------|--------|--------| | brightness | LED brightness | 1.0 | 0.1 | 1.0 | | logging | Enable or disable logging | False | False | True | -| step_max_pos | Maximum stepper position | 3100 | 0 | 65535 | The PID gains are best set by using the "PID Auto Tune" menu option. Place the robot on a line and press C to start the tuning process. The auto-tuner uses relay feedback (Åström-Hägglund method) to determine the ultimate gain and period of oscillation, then calculates PID gains using Ziegler-Nichols tuning rules. The tuning process includes a quality score (0-100%) indicating how consistent the oscillation data was. Results are automatically saved to settings. @@ -74,7 +71,7 @@ When running from badge power the current available is limited - the best way to The maximum allowed servo range is VERY WIDE - most Servos will not be able to cope with this, so you probably want to reduce the ```servo_range``` setting to suit your servos. -Each Servo or Motor driver requires a PWM signals to control it, so a single HexDrive takes up four PWM resources on the ESP32. As there are 8 such resources, the 'flavour' of your HexDrives will determine how many you can run simultaneously as long as you don't have any other hexpansions or applications using PWM resources. Two '4 Servo', 'Stepper' or 'Unknown' flavour HexDrives will use up all the available PWM channels, whereas you can run up to 4 HexDrives in '2 Motor' flavour. (While each motor driver does actually require two PWM signals we have been able to reduce this to one by swapping it between the active signal when the motor direction changes.) +Each Servo or Motor driver requires a PWM signals to control it, so a single HexDrive takes up four PWM resources on the ESP32. As there are 8 such resources, the 'flavour' of your HexDrives will determine how many you can run simultaneously as long as you don't have any other hexpansions or applications using PWM resources. Two '4 Servo' or 'Unknown' flavour HexDrives will use up all the available PWM channels, whereas you can run up to 4 HexDrives in '2 Motor' flavour. (While each motor driver does actually require two PWM signals we have been able to reduce this to one by swapping it between the active signal when the motor direction changes.) If you unplug a HexDrive the PWM resources will be released immediately so you can move them around the badge easily. @@ -93,7 +90,6 @@ This repo contains lots of files that you don't need on your badge to use a HexD + motor_controller.mpy + motor_moves.mpy + servo_test.mpy -+ stepper_test.mpy + settings_mgr.mpy + line_follow.mpy + autotune.mpy @@ -164,9 +160,6 @@ You can use one motor and 1 or 2 servos simultaneously. ### Frequency You can adjust the PWM frequency, default 20000Hz for motors and 50Hz for servos by calling the ```set_freq()``` function. -### Stepper Motor -You can control a single 2 phase stepper motor using ```motor_step()``` specifying which of the 8 possible phases to output in the range 0 to 7. There are 8 possible values as half stepping is supported. To use only full steps specify phase values of 0, 2, 4 and 6. Information on the pros and cons of using full or half stepping can be found online and what is right for you will depend on your motor and the application. The motor can be released (so that it is not taking power to hold it in a fixed position) using ```motor_release()```. - #### Keep Alive To protect against most badge/software crashes causing the motors or servos to run out of control there is a keep alive mechanism which means that if you do not make a call to the ```set_pwm```, ```set_motors```, ```motor_step``` or ```set_servoposition``` functions the motors/servos will be turned off after 1000mS (default - which can be changed with a call to ```set_keep_alive()```). diff --git a/app.py b/app.py index f11e693..f80a252 100644 --- a/app.py +++ b/app.py @@ -89,12 +89,11 @@ STATE_SETTINGS = 4 # Edit Settings STATE_MOTOR_MOVES = 5 # Motor Moves (sub-states managed by MotorMovesMgr) STATE_SERVO = 6 # Servo test -STATE_STEPPER = 7 # Stepper test -STATE_FOLLOWER = 8 # Line Follower -STATE_AUTOTUNE = 9 # PID Auto Tune -STATE_SENSOR = 10 # Sensor Test -STATE_AUTODRIVE = 11 # Autonomous Drive -STATE_HEXPANSION = 12 # Hexpansion Management (sub-states managed by HexpansionMgr) +STATE_FOLLOWER = 7 # Line Follower +STATE_AUTOTUNE = 8 # PID Auto Tune +STATE_SENSOR = 9 # Sensor Test +STATE_AUTODRIVE = 10 # Autonomous Drive +STATE_HEXPANSION = 11 # Hexpansion Management (sub-states managed by HexpansionMgr) # App states where user can minimise app (Menu, Message, Logo) MINIMISE_VALID_STATES = [STATE_MENU, STATE_MESSAGE, STATE_LOGO] @@ -110,18 +109,17 @@ # Main Menu Items -MAIN_MENU_ITEMS = ["Line Follower","Motor Moves", "Stepper Test", "Servo Test", "PID Auto Tune", "Sensor Test", "Auto Drive", "Hexpansions", "Settings", "About","Exit"] +MAIN_MENU_ITEMS = ["Line Follower","Motor Moves", "Servo Test", "PID Auto Tune", "Sensor Test", "Auto Drive", "Hexpansions", "Settings", "About","Exit"] MENU_ITEM_LINE_FOLLOWER = 0 MENU_ITEM_MOTOR_MOVES = 1 -MENU_ITEM_STEPPER_TEST = 2 -MENU_ITEM_SERVO_TEST = 3 -MENU_ITEM_PID_AUTOTUNE = 4 -MENU_ITEM_SENSOR_TEST = 5 -MENU_ITEM_AUTO_DRIVE = 6 -MENU_ITEM_HEXPANSION = 7 -MENU_ITEM_SETTINGS = 8 -MENU_ITEM_ABOUT = 9 -MENU_ITEM_EXIT = 10 +MENU_ITEM_SERVO_TEST = 2 +MENU_ITEM_PID_AUTOTUNE = 3 +MENU_ITEM_SENSOR_TEST = 4 +MENU_ITEM_AUTO_DRIVE = 5 +MENU_ITEM_HEXPANSION = 6 +MENU_ITEM_SETTINGS = 7 +MENU_ITEM_ABOUT = 8 +MENU_ITEM_EXIT = 9 # Front face direction labels (0=BtnA corner between slots 6 & 1, each step = 30° CW) _FRONT_FACE_LABELS = ( @@ -156,7 +154,6 @@ def _try_import(module_name, *attr_names): SettingsMgr, MySetting = _try_import('settings_mgr', 'SettingsMgr', 'MySetting') MotorMovesMgr, _motor_moves_init_settings = _try_import('motor_moves', 'MotorMovesMgr', 'init_settings') ServoTestMgr, _servo_test_init_settings = _try_import('servo_test', 'ServoTestMgr', 'init_settings') -StepperTestMgr, _stepper_test_init_settings = _try_import('stepper_test', 'StepperTestMgr', 'init_settings') LineFollowMgr, _line_follow_init_settings = _try_import('line_follow', 'LineFollowMgr', 'init_settings') (AutotuneMgr,) = _try_import('autotune_mgr', 'AutotuneMgr') SensorTestMgr, _sensor_test_init_settings = _try_import('sensor_test', 'SensorTestMgr', 'init_settings') @@ -248,15 +245,14 @@ def __init__(self): # Hexpansion related - SEE ALSO hexpansion_mgr to update _SINGLE_PORT_HEXPANSION_REFS # pid name vid eeprom total size eeprom page size app mpy name app mpy version app name motors servos sensors sub_type assert HexpansionType is not None - self.HEXPANSION_TYPES = [HexpansionType(0xCBCB, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, servos=4, steppers=1, sub_type="Uncommitted" ), - HexpansionType(0xCBCA, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, sub_type="2 Motor" ), - HexpansionType(0xCBCC, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", servos=4, sub_type="4 Servo" ), - HexpansionType(0xCBCD, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=1, servos=2, sub_type="1 Mot 2 Srvo" ), - HexpansionType(0xCBCE, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", steppers=1, sub_type="1 Stepper" ), - HexpansionType(0x0100, "HexSense", vid=0xCBCB, eeprom_total_size=65536, eeprom_page_size=128, sensors=2, sub_type="2 Line Sensors"), - HexpansionType(0x0200, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, servos=2, sub_type="Uncommitted" ), - HexpansionType(0x0201, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, sub_type="2 Motor" ), - HexpansionType(0x0202, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", servos=2, sub_type="2 Servo" ), + self.HEXPANSION_TYPES = [HexpansionType(0xCBCB, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, servos=4, sub_type="Uncommitted" ), + HexpansionType(0xCBCA, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, sub_type="2 Motor" ), + HexpansionType(0xCBCC, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", servos=4, sub_type="4 Servo" ), + HexpansionType(0xCBCD, "HexDrive", app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=1, servos=2, sub_type="1 Mot 2 Srvo" ), + HexpansionType(0x0100, "HexSense", vid=0xCBCB, eeprom_total_size=65536, eeprom_page_size=128, sensors=2, sub_type="2 Line Sensors"), + HexpansionType(0x0200, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, servos=2, sub_type="Uncommitted" ), + HexpansionType(0x0201, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", motors=2, sub_type="2 Motor" ), + HexpansionType(0x0202, "HexDriveV2", vid=0xCBCB, eeprom_total_size=32768, eeprom_page_size= 64, app_mpy_name="hexdrive.mpy", app_mpy_version=HEXDRIVE_APP_VERSION, app_name="HexDriveApp", servos=2, sub_type="2 Servo" ), HexpansionType(0x0300, "HexTest", vid=0xCBCB, eeprom_total_size=65536, eeprom_page_size=128), HexpansionType(0x0400, "HexDiag", vid=0xCBCB, eeprom_total_size=65536, eeprom_page_size=128), #HexpansionType(0x1295, "GPS", app_mpy_name="gps.mpy", app_mpy_version=1, app_name="GPSApp"), # eeprom_total_size= 2048, eeprom_page_size= 16), @@ -268,16 +264,16 @@ def __init__(self): self.HEXDRIVE_HEXPANSION_INDEX = 0 # Index in the HEXPANSION_TYPES list which corresponds to the basic HexDrive type self.HEXDRIVE_V2_HEXPANSION_INDEX = 6 # Index in the HEXPANSION_TYPES list which corresponds to the basic HexDrive V2 type - self.HEXSENSE_HEXPANSION_INDEX = 5 # Index in the HEXPANSION_TYPES list which corresponds to the HexSense type - self.HEXTEST_HEXPANSION_INDEX = 9 # Index in the HEXPANSION_TYPES list which corresponds to the HexTest type - self.HEXDIAG_HEXPANSION_INDEX = 10 # Index in the HEXPANSION_TYPES list which corresponds to the HexDiag type - #self.HEXGPS_HEXPANSION_INDEX = 11 # Index in the HEXPANSION_TYPES list which corresponds to the HexGPS type + self.HEXSENSE_HEXPANSION_INDEX = 4 # Index in the HEXPANSION_TYPES list which corresponds to the HexSense type + self.HEXTEST_HEXPANSION_INDEX = 8 # Index in the HEXPANSION_TYPES list which corresponds to the HexTest type + self.HEXDIAG_HEXPANSION_INDEX = 9 # Index in the HEXPANSION_TYPES list which corresponds to the HexDiag type + #self.HEXGPS_HEXPANSION_INDEX = 10 # Index in the HEXPANSION_TYPES list which corresponds to the HexGPS type self.UNRECOGNISED_HEXPANSION_INDEX = len(self.HEXPANSION_TYPES) - 2 # Index in the HEXPANSION_TYPES list which corresponds to unrecognised hexpansion types MUST BE LAST NON-BLANK ENTRY IN THE LIST self.BLANK_HEXPANSION_INDEX = len(self.HEXPANSION_TYPES) - 1 # Index in the HEXPANSION_TYPES list which corresponds to blank EEPROMs self.hexpansion_update_required: bool = False # flag from async to main loop - self.hexdrive_hexpansion_types = [0,1,2,3,4,6,7,8] # indices in the HEXPANSION_TYPES list which correspond to HexDrive variants - used to check if a detected hexpansion is a HexDrive and to set up the motor and servo counts accordingly + self.hexdrive_hexpansion_types = [0,1,2,3,5,6,7] # indices in the HEXPANSION_TYPES list which correspond to HexDrive variants - used to check if a detected hexpansion is a HexDrive and to set up the motor and servo counts accordingly # HexDrive hexpansion - has an app which we use to control the motors and servos self.hexdrive_ports = [] @@ -305,7 +301,6 @@ def __init__(self): self._hexpansion_mgr = HexpansionMgr(self, logging=self.logging) if HexpansionMgr is not None else None self._motor_moves_mgr = MotorMovesMgr(self, logging=self.logging) if MotorMovesMgr is not None else None self._servo_test_mgr = ServoTestMgr(self, logging=self.logging) if ServoTestMgr is not None else None - self._stepper_test_mgr = StepperTestMgr(self, logging=self.logging) if StepperTestMgr is not None else None self._settings_mgr = SettingsMgr(self, logging=self.logging) if SettingsMgr is not None else None self._line_follow_mgr = LineFollowMgr(self, logging=self.logging) if LineFollowMgr is not None else None self._autotune_mgr = AutotuneMgr(self, self._line_follow_mgr, logging=self.logging) if AutotuneMgr is not None else None @@ -322,7 +317,6 @@ def __init__(self): self._register_state_functions(STATE_FOLLOWER, self._line_follow_mgr) self._register_state_functions(STATE_AUTOTUNE, self._autotune_mgr) self._register_state_functions(STATE_SERVO, self._servo_test_mgr) - self._register_state_functions(STATE_STEPPER, self._stepper_test_mgr) self._register_state_functions(STATE_SETTINGS, self._settings_mgr) self._register_state_functions(STATE_SENSOR, self._sensor_test_mgr) self._register_state_functions(STATE_AUTODRIVE, self._autodrive_mgr) @@ -330,7 +324,6 @@ def __init__(self): # Motor Driver Hardware self.num_motors: int = 0 # initialised to 0 until we detect a HexDrive Hexpansion and can set this based on the actual number of motors it has - self.num_steppers: int = 0 # initialised to 0 until we detect a HexDrive Hexpansion and can set this based on the actual number of steppers it has # Line Sensors Hardware self.num_line_sensors: int = 0 # initialised to 0 until we detect a HexSense Hexpansion and can set this based on the actual number of sensors it has @@ -469,12 +462,6 @@ def enable_servo_test(self): return self.num_servos > 0 and self._servo_test_mgr is not None - @property - def enable_stepper_test(self): - """Whether the Stepper Test feature is enabled, based on whether we have detected stepper hardware and have the manager available.""" - return self.num_steppers > 0 and self._stepper_test_mgr is not None - - @property def enable_line_follow(self): """Whether the Line Follow feature is enabled, based on whether we have detected line sensors and have the manager available.""" @@ -509,8 +496,6 @@ def initialise_settings(self): _motor_moves_init_settings(self.settings, MySetting) if self.enable_servo_test and _servo_test_init_settings is not None: _servo_test_init_settings(self.settings, MySetting) - if self.enable_stepper_test and _stepper_test_init_settings is not None: - _stepper_test_init_settings(self.settings, MySetting) if self.enable_line_follow and _line_follow_init_settings is not None: _line_follow_init_settings(self.settings, MySetting) if self.enable_sensor_test and _sensor_test_init_settings is not None: @@ -990,8 +975,6 @@ def set_menu(self, menu_name: str | None = "main"): #: Literal["main"]): does i menu_items = MAIN_MENU_ITEMS.copy() if not self.enable_servo_test and MAIN_MENU_ITEMS[MENU_ITEM_SERVO_TEST] in menu_items: menu_items.remove(MAIN_MENU_ITEMS[MENU_ITEM_SERVO_TEST]) - if not self.enable_stepper_test and MAIN_MENU_ITEMS[MENU_ITEM_STEPPER_TEST] in menu_items: - menu_items.remove(MAIN_MENU_ITEMS[MENU_ITEM_STEPPER_TEST]) if not self.enable_motor_moves and MAIN_MENU_ITEMS[MENU_ITEM_MOTOR_MOVES] in menu_items: menu_items.remove(MAIN_MENU_ITEMS[MENU_ITEM_MOTOR_MOVES]) if not self.enable_line_follow and MAIN_MENU_ITEMS[MENU_ITEM_LINE_FOLLOWER] in menu_items: @@ -1062,15 +1045,6 @@ def _main_menu_select_handler(self, item: str, idx: int): self._autotune_mgr.logging = self.logging # update logging setting in autotune manager based on current app setting, in case it was changed if self._autotune_mgr.start(): self.current_state = STATE_AUTOTUNE - elif item == MAIN_MENU_ITEMS[MENU_ITEM_STEPPER_TEST]: # Stepper Test - # Check for required hardware and show message if not present, otherwise start the stepper test manager and switch to stepper test state - if self.num_steppers == 0: - self.notification = Notification("No Steppers") - else: - if self._stepper_test_mgr is not None: - self._stepper_test_mgr.logging = self.logging # update logging setting in stepper test manager based on current app setting, in case it was changed - if self._stepper_test_mgr.start(): - self.current_state = STATE_STEPPER elif item == MAIN_MENU_ITEMS[MENU_ITEM_SERVO_TEST]: # Servo Test # Check for required hardware and show message if not present, otherwise start the servo test manager and switch to servo test state if self.num_servos == 0: diff --git a/copilot_instructions.md b/copilot_instructions.md deleted file mode 100644 index 5eeb81c..0000000 --- a/copilot_instructions.md +++ /dev/null @@ -1,402 +0,0 @@ -# Copilot Development Instructions for BadgeBot - -This document provides essential context for AI assistants (GitHub Copilot, etc.) -working on the BadgeBot codebase. It was created during a comprehensive review of -code, comments, and documentation consistency. - ---- - -## Project Overview - -BadgeBot is a MicroPython robotics control application for the EMF Camp 2024 -Tildagon badge. It drives motors, servos, and stepper motors via the HexDrive -hexpansion, and can follow lines via the HexSense hexpansion with QTRX reflectance -sensors. It also supports autonomous obstacle-avoidance driving using ToF distance -sensors, I2C sensor testing/probing, and IMU-aided motor control (gyro turns, -accelerometer distance estimation). The app runs directly on an ESP32-S3 badge -(or in a desktop simulator). - -**Key facts:** -- Platform: MicroPython on ESP32-S3 (Tildagon badge) / Python 3.10+ simulator -- License: LGPL-3.0-only -- App version: `APP_VERSION` in `app.py` (major.minor format, e.g. "1.3") – this is the - definitive version. `tildagon.toml` `version` must always match `APP_VERSION`. -- HexDrive firmware version: `VERSION` in `hexdrive.py` – a separate integer - versioning the HexDrive public interface, independent of the app version. - ---- - -## Repository Structure - -### Runtime Modules (deployed to badge) - -| File | Purpose | -|------|---------| -| `__init__.py` | Package init – exports `BadgeBotApp` | -| `app.py` | Main app class (`BadgeBotApp`), state machine, menus, LED control, countdown timers | -| `hexdrive.py` | HexDrive hexpansion firmware – runs from EEPROM; PWM / motor / servo / stepper control | -| `hexpansion_mgr.py` | Hexpansion detection, EEPROM init, firmware programming, upgrade, erasure; creates `MotorController` | -| `motor_controller.py` | High-level async motor controller – gyro-aided turns, accelerometer distance drives, instruction replay | -| `motor_moves.py` | Logo/turtle-style motor programming (UP/DOWN/LEFT/RIGHT instruction sequences); delegates to `MotorController` when available | -| `servo_test.py` | Servo tester (position, trim, scanning modes; up to 4 servos) | -| `stepper_test.py` | Stepper motor tester (position and speed modes) | -| `line_follow.py` | Line follower with QTRX sensors and PID control; includes `LineSensor`/`LineSensors` drivers | -| `autotune.py` | PID auto-tuning algorithm (relay feedback / Åström-Hägglund / Ziegler-Nichols) | -| `autotune_mgr.py` | PID auto-tune UI manager (countdown integration, display rendering) | -| `sensor_manager.py` | `SensorManager` – opens an I2C port, probes for known sensors, manages sensor selection and reading | -| `sensor_test.py` | Sensor test UI – port selection, live reading display, sensor switching; uses `SensorManager` | -| `autodrive.py` | Autonomous drive mode – obstacle avoidance via ToF spin-scan with IMU gyro tracking | -| `settings_mgr.py` | `MySetting` class (bounded values with persistence) and `SettingsMgr` UI | -| `utils.py` | Helper functions: animated logo drawing, QR code rendering, version parsing, `chain()` | -| `uQR.py` | Micro QR code generator (third-party, for MicroPython) | - -### Sensor Drivers (`sensors/`) - -| File | Purpose | -|------|---------| -| `__init__.py` | Package init – exports `ALL_SENSOR_CLASSES` list for auto-discovery | -| `sensor_base.py` | `SensorBase` abstract base class defining the driver interface (`begin`, `read`, `reset`) | -| `vl53l0x.py` | VL53L0X Time-of-Flight distance sensor (I2C `0x29`, up to ~1200 mm) | -| `vl6180x.py` | VL6180X ToF proximity + ALS lux sensor (I2C `0x29`, 0–100 mm) | -| `tcs3472.py` | TCS3472 colour RGBC + CCT + lux sensor (I2C `0x29`) | -| `tcs3430.py` | TCS3430 colour CIE XYZ + lux sensor (I2C `0x39`) | -| `opt4048.py` | OPT4048 tristimulus XYZ colour sensor (I2C `0x44`) | -| `ina226.py` | INA226 current/voltage/power monitor (I2C `0x40`-`0x4F`, 100mΩ shunt default) | - -### Configuration - -| File | Purpose | -|------|---------| -| `tildagon.toml` | Badge app manifest (name, version, license, URL) | -| `metadata.json` | App metadata for badge menu (callable class, name, category) | -| `pyproject.toml` | Pylint configuration | - -### Development Tools (`dev/`) - -| File | Purpose | -|------|---------| -| `build_release.py` | Compile .py → .mpy and prune non-release files | -| `generate_qr_code.py` | Generate QR code bitfields; optionally update app.py | -| `check_qr_sync.py` | Validate `_QR_CODE` in app.py matches expected URL | -| `download_to_device.py` | Smart incremental deployment to badge (SHA256 change tracking) | -| `setup_dev_env.ps1` | Windows development environment setup | -| `setup_dev_env.sh` | Linux/macOS development environment setup | -| `dev_requirements.txt` | Python dev dependencies | - -### Tests (`tests/`) - -| File | Purpose | -|------|---------| -| `test_smoke.py` | Integration tests: imports, app init, version match, exports | -| `test_autotune.py` | PID auto-tuner unit tests (17 cases: error computation, lifecycle, tuning methods, quality) | - -**Running tests:** -``` -cd tests -python -m pytest test_smoke.py test_autotune.py -v -``` -Tests must be run from the `tests/` directory. Running from the repo root causes -import errors because `__init__.py` tries to import badge-platform-specific modules. -The CI workflow (`.github/workflows/tests.yml`) checks out the parent -`badge-2024-software` repo and runs BadgeBot tests as a submodule within that -structure. - -### Type Stubs (`typings/`) - -Stub `.pyi` files for IDE support (Pylance/mypy) covering badge-specific modules: -`app`, `app_components`, `asyncio`, `display`, `events`, `frontboards`, `machine`, -`ota`, `settings`, `system`, `tildagonos`, `time`, `ure`, `vfs`. - ---- - -## Architecture - -### App State Machine - -States are defined as module-level constants in `app.py`: - -``` -STATE_HEXPANSION = -1 Hexpansion detection / setup -STATE_MENU = 0 Main menu -STATE_MESSAGE = 1 Warning / error message display -STATE_LOGO = 2 About screen with animated logo + QR code -STATE_COUNTDOWN = 3 Shared 5-second countdown (Motor Moves & PID AutoTune) -STATE_SETTINGS = 4 Settings editor -STATE_MOTOR_MOVES = 5 Logo-style motor programming -STATE_SERVO = 6 Servo tester -STATE_STEPPER = 7 Stepper tester -STATE_FOLLOWER = 8 Line follower -STATE_AUTOTUNE = 9 PID auto-tuner -STATE_SENSOR = 10 Sensor test (I2C sensor probing and live display) -STATE_AUTODRIVE = 11 Autonomous drive (obstacle avoidance via ToF spin-scan) -``` - -### Main Menu - -Menu items are defined in `MAIN_MENU_ITEMS` in `app.py`. Items are dynamically -filtered based on detected hardware capabilities (e.g. Motor Moves, Line Follower, -PID Auto Tune, and Auto Drive are hidden when no motors are detected; Servo/Stepper -Test are hidden when no servos/steppers are available): - -| Index | Constant | Label | -|-------|----------|-------| -| 0 | `MENU_ITEM_LINE_FOLLOWER` | Line Follower | -| 1 | `MENU_ITEM_MOTOR_MOVES` | Motor Moves | -| 2 | `MENU_ITEM_STEPPER_TEST` | Stepper Test | -| 3 | `MENU_ITEM_SERVO_TEST` | Servo Test | -| 4 | `MENU_ITEM_PID_AUTOTUNE` | PID Auto Tune | -| 5 | `MENU_ITEM_SENSOR_TEST` | Sensor Test | -| 6 | `MENU_ITEM_AUTO_DRIVE` | Auto Drive | -| 7 | `MENU_ITEM_SETTINGS` | Settings | -| 8 | `MENU_ITEM_ABOUT` | About | -| 9 | `MENU_ITEM_EXIT` | Exit | - -### Manager Pattern - -Each functional area is encapsulated in a manager class with a consistent interface: -- `__init__(app)` – wire up to BadgeBotApp -- `start() -> bool` – enter the mode from the menu -- `update(delta)` – per-tick state machine update -- `draw(ctx)` – render the UI -- `background_update(delta)` – (optional) high-frequency motor control; returns `(int, int)` motor output or `None` - -The main app uses dispatch tables (`_state_update_dispatch`, `_state_draw_dispatch`, -`_state_background_dispatch`) to route to the correct manager based on `current_state`. - -| Manager class | Module | State | Has `background_update` | -|---------------|--------|-------|------------------------| -| `HexpansionMgr` | `hexpansion_mgr.py` | `STATE_HEXPANSION` | No | -| `MotorMovesMgr` | `motor_moves.py` | `STATE_MOTOR_MOVES` | Yes | -| `ServoTestMgr` | `servo_test.py` | `STATE_SERVO` | No | -| `StepperTestMgr` | `stepper_test.py` | `STATE_STEPPER` | No | -| `SettingsMgr` | `settings_mgr.py` | `STATE_SETTINGS` | No | -| `LineFollowMgr` | `line_follow.py` | `STATE_FOLLOWER` | Yes | -| `AutotuneMgr` | `autotune_mgr.py` | `STATE_AUTOTUNE` | Yes | -| `SensorTestMgr` | `sensor_test.py` | `STATE_SENSOR` | No | -| `AutoDriveMgr` | `autodrive.py` | `STATE_AUTODRIVE` | Yes | - -### MotorController - -`MotorController` (in `motor_controller.py`) provides high-level, async-friendly -motor commands with IMU feedback: - -- **Time-based**: `forward(ms)`, `backward(ms)`, `timed_turn(ms, dir)` -- **Sensor-aided**: `turn(degrees)` (gyro), `forward_mm(mm)` / `backward_mm(mm)` (accelerometer) -- **Instruction replay**: `run_instructions(list)` – executes recorded Logo-style sequences -- **Immediate**: `stop()`, `brake()` (async ramp-down) - -Created by `HexpansionMgr` when a HexDrive with motors is detected; stored as -`app.motor_controller`. Set to `None` when the HexDrive is removed. Uses the -on-board IMU gyroscope for accurate heading changes and double-integrates the -accelerometer for approximate distance measurement. - -The controller reads `max_power`, `acceleration`, `fwd_dir`, `front_face`, -`drive_step_ms`, `turn_step_ms` from the shared settings dict. It also -optionally reads `drive_mode` (0=time, 1=distance for instruction replay) and -`accel_scale` (calibration percentage) if those settings are registered. - -### SensorManager and Sensor Drivers - -`SensorManager` (in `sensor_manager.py`) opens a hexpansion I2C port, scans for -known sensor addresses, and initialises matching drivers from the `sensors/` package. - -Each sensor driver extends `SensorBase` and implements: -- `I2C_ADDR` (int) – 7-bit I²C address -- `NAME` (str) – display name (≤10 chars) -- `begin(i2c) -> bool` – initialise hardware -- `read() -> dict` – return `{label: value_string}` measurements -- `reset()` – low-power shutdown - -`SensorTestMgr` owns the `SensorManager` instance and provides public accessors -(`sensor_mgr`, `open_sensor_port()`) so other modules (e.g. `AutoDriveMgr`) can -share the same sensor connection. - -### Settings Registration - -Each module defines an `init_settings(s, MySetting)` function that registers its own -settings into the shared `settings` dict. These are called from `BadgeBotApp.__init__()`. - -Settings use the `MySetting` class which supports: -- `v` (current value), `d` (default), `_min`, `_max` -- `inc(v, level)` / `dec(v, level)` – level-based magnitude increments -- `persist()` – save to platform storage (deletes if equal to default) -- Types: `bool`, `int`, `float` - -### Countdown Mechanism - -`STATE_COUNTDOWN` is shared between Motor Moves and PID AutoTune. -`app.countdown_next_state` tracks which state to transition to after the countdown. -After the countdown finishes, `_update_state_countdown()` calls `begin_moves()` or -`begin_tuning()` on the respective manager. - ---- - -## Settings Reference (Comprehensive) - -### Motor Moves (registered in `motor_moves.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `acceleration` | 7500 | 1 | 65535 | PWM change limit per tick (prevents jerky motion) | -| `max_power` | 20000 | 1000 | 65535 | Maximum motor power level | -| `drive_step_ms` | 50 | 5 | 200 | Step duration for forward/backward (ms) | -| `turn_step_ms` | 20 | 5 | 200 | Step duration for turning (ms) | - -### Servo Test (registered in `servo_test.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `servo_step` | 10 | 1 | 100 | Servo pulse step (µs) | -| `servo_range` | 1000 | 100 | 1400 | Servo motion range ± from centre (µs) | -| `servo_period` | 20 | 5 | 50 | Servo control period (ms) | - -### Stepper Test (registered in `stepper_test.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `step_max_pos` | 3100 | 0 | 65535 | Maximum stepper position | - -### Line Follower (registered in `line_follow.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `line_threshold` | 500 | 0 | 65535 | Line sensor detection threshold | -| `pid_kp` | 20000 | 0 | 65536 | Proportional gain | -| `pid_ki` | 0 | 0 | 65535 | Integral gain | -| `pid_kd` | 0 | 0 | 65535 | Derivative gain | - -### Hexpansion Management (registered in `hexpansion_mgr.py`) -No dedicated settings currently; the `init_settings` hook exists for future use. - - -### General (registered in `app.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `brightness` | 0.1 | 0.1 | 1.0 | LED brightness scale factor | -| `logging` | False | False | True | Enable debug logging output | -| `fwd_dir` | 0 | 0 | 1 | Motor direction: 0 = Normal, 1 = Reverse (negates PWM outputs) | -| `front_face` | 0 | 0 | 11 | Badge orientation: 0 = 12 o'clock … 11 = 11 o'clock (30° steps); rotates LED positions and accelerometer axes | - -### Auto Drive (registered in `autodrive.py`) -| Key | Default | Min | Max | Description | -|-----|---------|-----|-----|-------------| -| `auto_speed` | 56000 | 1000 | 65535 | Motor PWM power for autonomous driving (~43% default) | -| `auto_obstacle` | 100 | 20 | 500 | Obstacle detection distance threshold (mm) | - -### Sensor Test (registered in `sensor_test.py`) -No dedicated settings currently; the `init_settings` hook exists for future use. - ---- - -## Coding Conventions - -- **Module headers**: Each `.py` file starts with a comment block describing the module - purpose and its public interface (functions/methods called by the main app). -- **Settings pattern**: Each module provides `init_settings(s, MySetting)`. -- **Manager pattern**: `__init__(app)`, `start()`, `update(delta)`, `draw(ctx)`, - optionally `background_update(delta)`. -- **Sensor driver pattern**: Extend `SensorBase`; set `I2C_ADDR` and `NAME` class attrs; - implement `_init()`, `_measure()`, `_shutdown()`; add to `ALL_SENSOR_CLASSES` in - `sensors/__init__.py`. -- **Alternative I2C addresses**: Drivers may also provide `I2C_ADDRS` for all - supported addresses. `SensorManager` will probe each address and may instantiate - multiple devices of the same driver on a single bus. -- **Power/current sensors**: Use integer fixed-point math only (no floats) and - report values in integer engineering units (`mA`, `mV`, etc.). -- **Typing expectations**: New sensor-manager/sensor-test changes should include - explicit type annotations that are compatible with both MicroPython runtime and - desktop linting/type-checking (Pylint/Pylance). -- **State constants**: Defined in `app.py` and imported by sub-modules via - `from .app import STATE_*`. -- **Logging**: Use `if self._logging:` guard before `print()` statements. -- **Import style**: Standard library first, then badge-specific, then relative imports. -- **Comments**: Use `#` line comments; docstrings for public classes/methods. -- **MicroPython compatibility**: Avoid features not available in MicroPython (e.g. some - typing features, `dataclasses`). Keep memory usage low. -- **Lazy imports**: Use lazy/deferred imports where possible to conserve badge RAM - (e.g. `SensorManager` is imported only when entering Sensor Test mode). - ---- - -## Known Issues to Review - -These issues were identified during the consistency review and should be addressed: - -### HexDrive types defined in two places - -`hexdrive.py` uses `HexDriveType` (for firmware-level type identification from EEPROM -PID byte) while `hexpansion_mgr.py` uses `HexpansionType` (for app-level detection -and firmware management). Both describe the same hardware variants. - -**Constraint:** `hexdrive.mpy` must fit in the 8 KB hexpansion EEPROM, so -`hexdrive.py` must not import from the main app or grow unnecessarily. - -**Current approach:** The main app cannot import `HexDriveType` definitions from -`hexdrive.py` without increasing binary size on the EEPROM. Instead, a test -(`test_hexdrive_type_pids_consistent`) in `test_smoke.py` validates that the PID -bytes and capability counts (motors, servos, steppers) are consistent between the -two definitions. **Any change to HexDrive variant definitions must update both -files and pass this test.** - -In future, `hexsense.py` (for HexSense hexpansions) will follow the same pattern: -it will have its own PID definitions, and `hexpansion_mgr.py` will have corresponding -`HexpansionType` entries. `hexdrive.py` should not include HexSense PIDs. - -### Servo test module: missing `background_update` in public interface comment - -The `servo_test.py` module header does not list `background_update` in the public -interface, but the `ServoTestMgr` class does not have a `background_update` method -either (servo updates are handled in `update`). This is actually consistent – just -note that servo test does not participate in the background dispatch table. Similarly, -`stepper_test.py` does not have `background_update`. - -### Sensor I2C address conflicts - -Multiple sensor drivers share the same I2C address: -- `0x29`: VL53L0X, VL6180X, TCS3472 -- `0x39`: TCS3430 -- `0x44`: OPT4048 - -Only one sensor at each address can be present on a given I2C bus. The -`SensorManager` initialises sensors in `ALL_SENSOR_CLASSES` order (VL53L0X first -for `0x29`), so address-conflicting sensors are handled on a first-match basis. - -### MotorController optional settings not yet registered - -`motor_controller.py` optionally reads `drive_mode` (0=time, 1=distance for -instruction replay) and `accel_scale` (distance calibration %) from the settings -dict, but neither is currently registered via any `init_settings` call. These are -future extension points – the controller gracefully falls back to defaults when -they are absent. - ---- - -## Development Workflow - -1. **Build**: No compile step needed for development – Python source files are used directly. - For release: `python dev/build_release.py` compiles `.py` → `.mpy`. -2. **Test**: `cd tests && python -m pytest -v` -3. **Lint**: `pylint` (config in `pyproject.toml`) -4. **Format**: `isort` for import ordering -5. **QR Code**: Regenerate with `python dev/generate_qr_code.py --url --write-app` -6. **Deploy to badge**: `python dev/download_to_device.py` - - By default only warnings, errors, and a final summary (including total bytes - uploaded) are printed. Pass `--verbose` to see every compile/upload action. - - Use `--app-dir :apps/` to deploy to a specific directory on the badge - (default: `:apps/LineFollower`). For example, to deploy as the main BadgeBot app: - `python dev/download_to_device.py --app-dir :apps/TeamRobotmad_BadgeBot` - -### Version Management - -`APP_VERSION` in `app.py` is the definitive version (major.minor format, e.g. "1.3"). -The release process must update `tildagon.toml` `version` to match `APP_VERSION`. -`VERSION` in `hexdrive.py` is a separate integer versioning the HexDrive -firmware public interface and is incremented independently when the HexDrive API changes. - ---- - -## CI/CD - -### tests.yml -- Triggers: push to `main`/`ci-test-*`, pull requests, manual dispatch -- Checks out `badge-2024-software` repo, updates BadgeBot submodule, runs pytest - -### release.yml -- Manual trigger only, on `main` branch -- Runs `dev/build_release.py` with `-f` flag to compile and prune diff --git a/dev/build_release.py b/dev/build_release.py index 575fc92..ee239df 100644 --- a/dev/build_release.py +++ b/dev/build_release.py @@ -16,7 +16,6 @@ "line_follow", "motor_moves", "servo_test", - "stepper_test", "utils", "motor_controller", "sensor_manager", diff --git a/dev/download_to_device.py b/dev/download_to_device.py index b77fdf6..0ca1f71 100644 --- a/dev/download_to_device.py +++ b/dev/download_to_device.py @@ -47,7 +47,6 @@ class ModuleSpec: ModuleSpec(Path("line_follow.py"), Path("line_follow.mpy")), ModuleSpec(Path("motor_moves.py"), Path("motor_moves.mpy")), ModuleSpec(Path("servo_test.py"), Path("servo_test.mpy")), - ModuleSpec(Path("stepper_test.py"), Path("stepper_test.mpy")), ModuleSpec(Path("motor_controller.py"), Path("motor_controller.mpy")), ModuleSpec(Path("sensor_manager.py"), Path("sensor_manager.mpy")), ModuleSpec(Path("sensor_test.py"), Path("sensor_test.mpy")), diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index 34171c0..ddbee5c 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -701,13 +701,11 @@ def _update_state_check(self, delta): # pylint: disable=unused-argument app.num_motors = 0 app.num_servos = 0 - app.num_steppers = 0 for port in app.hexdrive_ports: hexdrive_type_idx = self._hexpansion_type_by_slot[port - 1] if hexdrive_type_idx is not None and 0 <= hexdrive_type_idx < len(app.HEXPANSION_TYPES): app.num_motors += app.HEXPANSION_TYPES[hexdrive_type_idx].motors app.num_servos += app.HEXPANSION_TYPES[hexdrive_type_idx].servos - app.num_steppers += app.HEXPANSION_TYPES[hexdrive_type_idx].steppers if len(app.hexdrive_ports) != len (app.hexdrive_apps): hexdrive_apps = [] @@ -1390,7 +1388,7 @@ class HexpansionType: pid: the PID value to identify the hexpansion type from its EEPROM header name: human-friendly name of the hexpansion type (e.g. "HexDrive") vid: the VID value to identify the hexpansion type from its EEPROM header (default 0xCAFE) - motors, steppers, servos, sensors: the capabilities of this hexpansion type, used to configure the app when detected + motors, servos, sensors: the capabilities of this hexpansion type, used to configure the app when detected sub_type: a human-friendly string describing the specific variant of this hexpansion type app_mpy_name: the filename of the .mpy to copy to the hexpansion EEPROM for this type (if any) app_mpy_version: the version string to report for the .mpy copied to the hexpansion EEPROM for this type (if any) @@ -1398,7 +1396,7 @@ class HexpansionType: """ def __init__(self, pid: int, name: str, vid: int =0xCAFE, eeprom_page_size: int =_DEFAULT_EEPROM_PAGE_SIZE, eeprom_total_size: int =_DEFAULT_EEPROM_TOTAL_SIZE, - motors: int =0, steppers: int =0, servos: int =0, sensors: int =0, + motors: int =0, servos: int =0, sensors: int =0, sub_type: str | None =None, app_mpy_name: str | None =None, app_mpy_version: str | None =None, app_name: str | None =None): self.vid: int = vid @@ -1409,7 +1407,6 @@ def __init__(self, pid: int, name: str, vid: int =0xCAFE, self.sub_type: str | None = sub_type self.motors: int = motors self.servos: int = servos - self.steppers: int = steppers self.sensors: int = sensors self.app_mpy_name: str | None = app_mpy_name self.app_mpy_version: str | None = app_mpy_version diff --git a/stepper_test.py b/stepper_test.py deleted file mode 100644 index 6f423d5..0000000 --- a/stepper_test.py +++ /dev/null @@ -1,557 +0,0 @@ -# Stepper Tester Module for BadgeBot -# -# Handles the stepper motor tester functionality. -# Contains the Stepper motor driver class and the StepperMode helper. -# -# Public interface (called by the main app): -# __init__(app) – wire up to BadgeBotApp -# start() – enter stepper test from menu -# update(delta) – per-tick state machine update -# draw(ctx) – render stepper tester UI - -import time -from math import pi -from events.input import BUTTON_TYPES -from app_components.tokens import label_font_size, button_labels -from app_components.notification import Notification -try: - from machine import Timer -except ImportError: - Timer = None - -from .utils import inc_value, dec_value - - -# Stepper Tester - Defaults -_STEPPER_MAX_SPEED = 200 -_STEPPER_MAX_POSITION = 3100 -_STEPPER_DEFAULT_SPEED = 50 -_STEPPER_NUM_PHASES = 8 -_STEPPER_DEFAULT_SPR = 200 -_STEPPER_DEFAULT_STEP = 1 - - -# ---- Settings initialisation ----------------------------------------------- - -def init_settings(s, MySetting): - """Register stepper-test-specific settings in the shared settings dict.""" - s['step_max_pos'] = MySetting(s, _STEPPER_MAX_POSITION, 0, 65535) - - -class StepperMode: - OFF = 0 - POSITION = 1 - SPEED = 2 - stepper_modes = ["OFF", "POSITION", "SPEED"] - - def __init__(self, mode=OFF): - self._mode = mode - - @property - def mode(self): - return self._mode - - @mode.setter - def mode(self, mode): - self._mode = mode - - def inc(self): - self._mode = (self._mode + 1) % 3 - - def __eq__(self, other): - return self._mode == other - - def __str__(self): - return self.stepper_modes[self._mode] - - -# ---- Stepper Motor Class --------------------------------------------------- - -class Stepper: - def __init__(self, container, hexdrive_app, logging: bool = False, - step_size=1, - steps_per_rev=_STEPPER_DEFAULT_SPR, - speed_sps=_STEPPER_DEFAULT_SPEED, - max_sps=_STEPPER_MAX_SPEED, - max_pos=_STEPPER_MAX_POSITION, timer_id=0): - self._container = container - self._hexdrive_app = hexdrive_app - self._logging: bool = logging - self._phase = 0 - self._calibrated = False - self._timer = Timer(timer_id) if Timer is not None else None - self._timer_is_running = False - self._timer_mode = 0 - self._free_run_mode = 0 - self._enabled = False - self._target_pos = 0 - self._pos = 0 - self._max_sps = int(max_sps) - self._steps_per_sec = int(speed_sps) - self._steps_per_rev = int(steps_per_rev) - self._max_pos = 2 * int(max_pos) - self._freq = 0 - self._min_period = 0 - self._step_size = int(step_size) - self._last_step_time = 0 - self.track_target() - if self._logging: - print("Stepper initialised") - - - @property - def max_pos(self) -> int: - return self._max_pos - - - @max_pos.setter - def max_pos(self, mp: int): - """ Set the maximum position for the stepper (in full steps).""" - self._max_pos = 2 * int(mp) - - - @property - def step_size(self) -> int: - return self._step_size - - - @step_size.setter - def step_size(self, sz: int): - """ Set the step size (microstepping level) for the stepper. Valid values are 1 (full step) or 2 (half step).""" - if sz < 1: - sz = 1 - elif sz > 2: - sz = 2 - self._step_size = int(sz) - - - @property - def speed(self) -> int: - return self._steps_per_sec - - - @speed.setter - def speed(self, sps: int): - if self._free_run_mode == 1 and sps < 0: - self._free_run_mode = -1 - elif self._free_run_mode == -1 and sps > 0: - self._free_run_mode = 1 - if sps > self._max_sps: - sps = self._max_sps - elif sps < -self._max_sps: - sps = -self._max_sps - self._steps_per_sec = sps - self._update() - - - @property - def speed_rps(self) -> float: - return self._steps_per_sec / self._steps_per_rev - - - @speed_rps.setter - def speed_rps(self, rps: float): - self.speed = int(rps * self._steps_per_rev) - - - @property - def target(self) -> int: - return self._target_pos // 2 - - - @target.setter - def target(self, t: int): - if self._calibrated and t < 0: - self._target_pos = 0 - elif self._calibrated and (2 * int(t)) > self._max_pos: - self._target_pos = self._max_pos - else: - self._target_pos = 2 * int(t) - self._update() - - - @property - def target_deg(self) -> float: - return self._target_pos * 180.0 / self._steps_per_rev - - - @target_deg.setter - def target_deg(self, deg: float): - self.target = int(self._steps_per_rev * deg / 360.0) - - - @property - def target_rad(self) -> float: - return self._target_pos * pi / self._steps_per_rev - - - @target_rad.setter - def target_rad(self, rad: float): - self.target = int(self._steps_per_rev * rad / (2 * pi)) - - - @property - def pos(self) -> int: - """ Get the current position of the stepper in full steps. Note that the internal position is tracked in half-steps to allow for microstepping, so the returned value is the internal position divided by 2.""" - return (self._pos // 2) - - - @pos.setter - def pos(self, p: int): - self._pos = 2 * int(p) - self._update() - - - @property - def pos_deg(self) -> float: - return self._pos * 180.0 / self._steps_per_rev - - - @property - def pos_rad(self) -> float: - return self._pos * pi / self._steps_per_rev - - - def step(self, d: int = 0): - cur_time = time.ticks_ms() - if time.ticks_diff(cur_time, self._last_step_time) < self._min_period: - return - self._last_step_time = cur_time - if d > 0: - self._pos += self._step_size - self._phase = (self._phase - self._step_size) % _STEPPER_NUM_PHASES - elif d < 0: - self._pos -= self._step_size - self._phase = (self._phase + self._step_size) % _STEPPER_NUM_PHASES - if self._calibrated and self._pos < 0: - print("s/w min endstop") - self._pos = 0 - self.speed = 0 - return - elif self._calibrated and self._pos > self._max_pos: - print("s/w max endstop") - self._pos = self._max_pos - self.speed = 0 - return - try: - self._hexdrive_app.motor_step(self._phase) - except Exception as e: # pylint: disable=broad-except - print(f"step phase {self._phase} failed:{e}") - - - def _hit_endstop(self): - print("Endstop - hit") - if not self._calibrated: - self._calibrated = True - self.pos = 0 - if self._free_run_mode < 0: - self.speed = 0 - elif self._free_run_mode == 0 and self._target_pos < self._pos: - self.speed = 0 - - - def _timer_callback_fwd(self, t): # pylint: disable=unused-argument - self.step(1) - - - def _timer_callback_rev(self, t): # pylint: disable=unused-argument - self.step(-1) - - - def _timer_callback(self, t): # pylint: disable=unused-argument - if self._target_pos > self._pos: - self.step(1) - elif self._target_pos < self._pos: - self.step(-1) - - - def free_run(self, d=1): - """Run the stepper at the current speed in the given direction, ignoring position feedback.""" - self._free_run_mode = d - self._update() - - - def track_target(self): - """Run the stepper, using position feedback to maintain the target position.""" - self._free_run_mode = 0 - self._update() - - - def _update(self): - if (self._free_run_mode != 0) or (self._target_pos != self._pos): - self._update_timer((2 // self._step_size) * abs(self._steps_per_sec)) - else: - self._update_timer(0) - - - def _update_timer(self, freq: int): - if self._timer is None: - return - if self._timer_is_running and freq != self._freq: - try: - self._timer.deinit() - self._freq = 0 - self._timer_is_running = False - except Exception as e: # pylint: disable=broad-except - print(f"update_timer failed:{e}") - if 0 != freq and (freq != self._freq or self._free_run_mode != self._timer_mode): - try: - print(f"Timer: {freq}Hz") - if self._free_run_mode > 0: - self._timer.init(freq=freq, callback=self._timer_callback_fwd) - elif self._free_run_mode < 0: - self._timer.init(freq=freq, callback=self._timer_callback_rev) - else: - self._timer.init(freq=freq, callback=self._timer_callback) - self._freq = freq - self._min_period = (1000 // freq) - 1 - self._timer_is_running = True - self._timer_mode = self._free_run_mode - except Exception as e: # pylint: disable=broad-except - print(f"update_timer failed:{e}") - - - def stop(self): - '''Stop the motor and disable the coils.''' - self._update_timer(0) - try: - self._hexdrive_app.motor_release() - except Exception as e: # pylint: disable=broad-except - print(f"stop failed:{e}") - - - @property - def enable(self) -> bool: - '''Return True if the stepper is currently enabled (i.e. not in a power-saving state).''' - return self._enabled - - - @enable.setter - def enable(self, e: bool = True): - '''Enable or disable the stepper motor coils.''' - self._enabled = e - try: - if e: - if self._free_run_mode != 0: - self._update_timer((2 // self._step_size) * abs(self._steps_per_sec)) - self._hexdrive_app.motor_step(self._phase) - else: - self._update_timer(0) - self._hexdrive_app.motor_release() - except Exception as ex: # pylint: disable=broad-except - print(f"enable failed {ex}") - - - - -# ---- Stepper Tester Manager ------------------------------------------------ - -class StepperTestMgr: - """Manages the Stepper Tester functionality. - - Parameters - ---------- - app : BadgeBotApp - Reference to the main application instance. - """ - - def __init__(self, app, logging: bool = False): - self._app = app - self._logging: bool = logging - self.stepper = None - self.stepper_mode = StepperMode() - self.timeout_period: int = 120000 # ms (2 minutes - without any user input) - self.keep_alive_period: int = 500 # ms (half the value used in hexdrive.py) - self._time_since_last_input: int = 0 - self._time_since_last_update: int = 0 - if self._logging: - print("StepperTestMgr initialised") - - - # ------------------------------------------------------------------ - - @property - def logging(self) -> bool: - """Whether to print debug logs to the console.""" - return self._logging - - @logging.setter - def logging(self, value: bool): - self._logging = value - - - # ------------------------------------------------------------------ - # Entry point from menu - # ------------------------------------------------------------------ - - def start(self) -> bool: - """Enter stepper test from the main menu.""" - app = self._app - if self.stepper is None: - # Find the first available timer for the stepper - we need a timer to do the stepping in the background while we wait for user input. - for i in range(4): - try: - self.stepper = Stepper(app, - app.hexdrive_apps[0] if len(app.hexdrive_apps) > 0 else None, - step_size=1, - timer_id=i, - max_pos=self.step_max_pos) - break - except Exception: # pylint: disable=broad-except - pass - if self.stepper is None: - app.notification = Notification("No Free Timers") - return False - if len(app.hexdrive_apps) > 0: - if app.hexdrive_apps[0].initialise() and app.hexdrive_apps[0].set_power(True): - app.set_menu(None) - app.button_states.clear() - app.refresh = True - app.auto_repeat_clear() - self.stepper.enable = True - self._time_since_last_input = 0 - - if self._logging: - print("Entered Stepper Test mode") - return True - return False - - - @property - def step_max_pos(self) -> int: - """Get the maximum position for the stepper from settings.""" - return self._app.settings['step_max_pos'].v if 'step_max_pos' in self._app.settings else _STEPPER_MAX_POSITION - - - # ------------------------------------------------------------------ - # Per-tick update - # ------------------------------------------------------------------ - - def _mode_position(self): - if self.stepper_mode != StepperMode.POSITION: - self.stepper_mode.mode = StepperMode.POSITION - self.stepper.speed = _STEPPER_DEFAULT_SPEED - self.stepper.enable = True - self.stepper.track_target() - - - def update(self, delta) -> bool: - """Handle Stepper UI updates. Returns True if this module handled the state.""" - app = self._app - if app.button_states.get(BUTTON_TYPES["RIGHT"]): - if app.auto_repeat_check(delta, True): - if self.stepper_mode == StepperMode.SPEED: - speed = inc_value(self.stepper.speed, app.auto_repeat_level + 1) - if _STEPPER_MAX_SPEED < speed: - speed = _STEPPER_MAX_SPEED - self.stepper.speed = speed - else: # Off or already in Position mode - increase target position - self._mode_position() - self.stepper.target = inc_value(self.stepper.pos, app.auto_repeat_level + 1) - app.refresh = True - elif app.button_states.get(BUTTON_TYPES["LEFT"]): - if app.auto_repeat_check(delta, True): - if self.stepper_mode == StepperMode.SPEED: - speed = dec_value(self.stepper.speed, app.auto_repeat_level + 1) - if -_STEPPER_MAX_SPEED > speed: - speed = -_STEPPER_MAX_SPEED - self.stepper.speed = speed - else: # Off or already in Position mode - decrease target position - self._mode_position() - self.stepper.target = dec_value(self.stepper.pos, app.auto_repeat_level + 1) - app.refresh = True - else: - app.auto_repeat_clear() - if app.button_states.get(BUTTON_TYPES["CANCEL"]): - app.button_states.clear() - self.stepper.enable = False - if self._logging: - print("Stepper:Back to menu") - app.return_to_menu() - return True - elif app.button_states.get(BUTTON_TYPES["CONFIRM"]): #"Mode" button - app.button_states.clear() - self.stepper_mode.inc() - if self.stepper_mode == StepperMode.POSITION: - self.stepper.speed = 0 - self.stepper.target = self.stepper.pos - self.stepper.speed = _STEPPER_DEFAULT_SPEED - self.stepper.enable = True - self.stepper.track_target() - elif self.stepper_mode == StepperMode.SPEED: - self.stepper.speed = 0 - self.stepper.enable = True - self.stepper.free_run(1) - else: - # "Off" mode - stop the stepper and disable coils to save power - self.stepper.stop() - self.stepper.enable = False - self.stepper.speed = 0 - app.refresh = True - app.notification = Notification(f" Stepper:\n {self.stepper_mode}") - print(f"Stepper:{self.stepper_mode}") - if app.refresh: - self._time_since_last_input = 0 - else: - self._time_since_last_input += delta - if self._time_since_last_input > self.timeout_period: - self.stepper.stop() - self.stepper.speed = 0 - self.stepper.enable = False - app.return_to_menu() - app.notification = Notification(" Stepper:\n Timeout") - print("Stepper:Timeout") - elif self.stepper_mode == StepperMode.SPEED: - app.refresh = True - if self.stepper_mode != StepperMode.OFF: - # Keep Alive for HexDrive if we are stationary. - self._time_since_last_update += delta - if self._time_since_last_update > self.keep_alive_period: - self.stepper.step() - self._time_since_last_update = 0 - return True - - - # ------------------------------------------------------------------ - # Draw - # ------------------------------------------------------------------ - - def draw(self, ctx) -> bool: - """Render Stepper Tester UI. Returns True if handled.""" - app = self._app - - stepper_text = ["S"] * (1 + app.num_steppers) - stepper_text_colours = [(0.4, 0.0, 0.0)] * (1 + app.num_steppers) - stepper_text[0] = "Stepper Test" - stepper_text_colours[0] = (1, 1, 0) - if self.stepper is not None: - i = 0 - if self.stepper_mode == StepperMode.OFF: - body_colour = (0.2, 0.2, 0.2) - bar_colour = (0.4, 0.4, 0.4) - else: - body_colour = (0.1, 0.1, 0.5) - bar_colour = (0.1, 0.1, 1.0) - stepper_text_colours[1] = (0.4, 0.4, 0.0) - - ctx.save() - ctx.translate(0, (i - (app.num_steppers / 2) + 0.5) * label_font_size) - background_colour = (0.15, 0.15, 0.15) - ctx.rgb(*background_colour).rectangle(-100, 1, 200, label_font_size - 2).fill() - c = 0 - x = 200 * (self.stepper.pos / self.stepper.max_pos) - 100 - ctx.rgb(*bar_colour).rectangle(x - 2, 1, 5, label_font_size - 2).fill() - ctx.rgb(*body_colour) - if x > (c + 4): - ctx.rectangle(c + 1, 3, x - c - 4, label_font_size - 6).fill() - elif x < (c - 4): - ctx.rectangle(x + 4, 3, c - x - 4, label_font_size - 6).fill() - ctx.rgb(0, 0, 0).move_to(c, 0).line_to(c, label_font_size).stroke() - ctx.restore() - if self.stepper_mode == StepperMode.SPEED: - stepper_text[i + 1] = f"{int(self.stepper.speed):4}/s" - else: - stepper_text[i + 1] = "Off" if (self.stepper_mode == StepperMode.OFF) else f"{int(self.stepper.pos):+6} " - app.draw_message(ctx, stepper_text, stepper_text_colours, label_font_size) - button_labels(ctx, confirm_label="Mode", cancel_label="Back", left_label="<--", right_label="-->") - return True diff --git a/tests/conftest.py b/tests/conftest.py index 4b41deb..1cb440b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,10 +14,10 @@ def test_something(badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion assert app.num_motors == 2 - @pytest.mark.parametrize("hexdrive_pid", [0xCBCE]) - def test_stepper(badgebot_app_with_hexpansion): + @pytest.mark.parametrize("hexdrive_pid", [0xCBCC]) + def test_servo(badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion - assert app.num_steppers == 1 + assert app.num_servos == 4 The core helper :func:`install_fake_hexpansion` is deliberately generic – callers supply a ``(vid, pid)`` pair and a port number, and it takes care diff --git a/tests/test_hexpansion.py b/tests/test_hexpansion.py index 9ce5be8..8567e60 100644 --- a/tests/test_hexpansion.py +++ b/tests/test_hexpansion.py @@ -33,10 +33,6 @@ def test_servo_settings_absent_without_hexpansion(self, badgebot_app): f"Setting '{key}' should not be registered without a HexDrive" ) - def test_stepper_settings_absent_without_hexpansion(self, badgebot_app): - """Stepper-dependent settings must not exist without a HexDrive.""" - assert 'step_max_pos' not in badgebot_app.settings - def test_autodrive_settings_absent_without_hexpansion(self, badgebot_app): """Auto-drive settings must not exist without a HexDrive.""" for key in ('auto_speed', 'auto_obstacle'): @@ -48,7 +44,6 @@ def test_hardware_counts_zero(self, badgebot_app): """Without any hexpansion, all hardware counts must be zero.""" assert badgebot_app.num_motors == 0 assert badgebot_app.num_servos == 0 - assert badgebot_app.num_steppers == 0 # ===================================================================== @@ -66,7 +61,6 @@ def test_hardware_counts(self, badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion assert app.num_motors == 2 assert app.num_servos == 0 - assert app.num_steppers == 0 def test_reaches_menu(self, badgebot_app_with_hexpansion): from sim.apps.BadgeBot.app import STATE_MENU @@ -82,9 +76,6 @@ def test_servo_settings_absent(self, badgebot_app_with_hexpansion): for key in ('servo_step', 'servo_range', 'servo_period'): assert key not in s, f"Setting '{key}' should not exist for 2-Motor" - def test_stepper_settings_absent(self, badgebot_app_with_hexpansion): - assert 'step_max_pos' not in badgebot_app_with_hexpansion.settings - def test_autodrive_settings_registered(self, badgebot_app_with_hexpansion): s = badgebot_app_with_hexpansion.settings for key in ('auto_speed', 'auto_obstacle'): @@ -108,12 +99,6 @@ def test_menu_excludes_servo_test(self, badgebot_app_with_hexpansion): items = [item for item in app.menu.menu_items] assert "Servo Test" not in items - def test_menu_excludes_stepper_test(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Stepper Test" not in items - def test_menu_excludes_line_follower_without_hexsense(self, badgebot_app_with_hexpansion): """Line Follower needs both motors and line sensors.""" app = badgebot_app_with_hexpansion @@ -150,7 +135,6 @@ def test_hardware_counts(self, badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion assert app.num_motors == 0 assert app.num_servos == 4 - assert app.num_steppers == 0 def test_servo_settings_registered(self, badgebot_app_with_hexpansion): s = badgebot_app_with_hexpansion.settings @@ -162,9 +146,6 @@ def test_motor_settings_absent(self, badgebot_app_with_hexpansion): for key in ('acceleration', 'max_power', 'drive_step_ms', 'turn_step_ms'): assert key not in s, f"Setting '{key}' should not exist for 4-Servo" - def test_stepper_settings_absent(self, badgebot_app_with_hexpansion): - assert 'step_max_pos' not in badgebot_app_with_hexpansion.settings - def test_autodrive_settings_absent(self, badgebot_app_with_hexpansion): """Auto drive requires motors > 1.""" s = badgebot_app_with_hexpansion.settings @@ -189,61 +170,6 @@ def test_menu_excludes_auto_drive(self, badgebot_app_with_hexpansion): items = [item for item in app.menu.menu_items] assert "Auto Drive" not in items - def test_menu_excludes_stepper_test(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Stepper Test" not in items - - -# ===================================================================== -# Stepper HexDrive (PID 0xCBCE) -# ===================================================================== - -class TestStepperHexDrive: - """Tests with a fake Stepper HexDrive.""" - - @pytest.fixture - def hexdrive_pid(self): - return 0xCBCE - - def test_hardware_counts(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - assert app.num_motors == 0 - assert app.num_servos == 0 - assert app.num_steppers == 1 - - def test_stepper_settings_registered(self, badgebot_app_with_hexpansion): - assert 'step_max_pos' in badgebot_app_with_hexpansion.settings - - def test_motor_settings_absent(self, badgebot_app_with_hexpansion): - s = badgebot_app_with_hexpansion.settings - for key in ('acceleration', 'max_power', 'drive_step_ms', 'turn_step_ms'): - assert key not in s, f"Setting '{key}' should not exist for Stepper" - - def test_servo_settings_absent(self, badgebot_app_with_hexpansion): - s = badgebot_app_with_hexpansion.settings - for key in ('servo_step', 'servo_range', 'servo_period'): - assert key not in s, f"Setting '{key}' should not exist for Stepper" - - def test_menu_includes_stepper_test(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Stepper Test" in items - - def test_menu_excludes_motor_moves(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Motor Moves" not in items - - def test_menu_excludes_servo_test(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Servo Test" not in items - # ===================================================================== # 1 Motor 2 Servo HexDrive (PID 0xCBCD) @@ -260,7 +186,6 @@ def test_hardware_counts(self, badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion assert app.num_motors == 1 assert app.num_servos == 2 - assert app.num_steppers == 0 def test_servo_settings_registered(self, badgebot_app_with_hexpansion): s = badgebot_app_with_hexpansion.settings @@ -299,7 +224,7 @@ def test_menu_excludes_auto_drive(self, badgebot_app_with_hexpansion): # ===================================================================== class TestFullHexDrive: - """Tests with the full-capability HexDrive (2 motors, 4 servos, 1 stepper).""" + """Tests with the full-capability HexDrive (2 motors, 4 servos).""" @pytest.fixture def hexdrive_pid(self): @@ -309,7 +234,6 @@ def test_hardware_counts(self, badgebot_app_with_hexpansion): app = badgebot_app_with_hexpansion assert app.num_motors == 2 assert app.num_servos == 4 - assert app.num_steppers == 1 def test_all_hardware_settings_registered(self, badgebot_app_with_hexpansion): s = badgebot_app_with_hexpansion.settings @@ -320,19 +244,8 @@ def test_all_hardware_settings_registered(self, badgebot_app_with_hexpansion): 'acceleration', 'max_power', 'drive_step_ms', 'turn_step_ms', # servo test 'servo_step', 'servo_range', 'servo_period', - # stepper test - 'step_max_pos', # auto drive 'auto_speed', 'auto_obstacle', ) for key in expected: assert key in s, f"Missing setting: {key}" - - def test_menu_includes_motor_servo_stepper(self, badgebot_app_with_hexpansion): - app = badgebot_app_with_hexpansion - app.set_menu("main") - items = [item for item in app.menu.menu_items] - assert "Motor Moves" in items - assert "Servo Test" in items - assert "Stepper Test" in items - assert "Auto Drive" in items diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 9c21a05..7094fb0 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -40,7 +40,7 @@ def test_hexdrive_type_pids_consistent(): HexDriveType stores a single PID byte (low byte), while HexpansionType stores the full 16-bit PID. For every HexDrive-flavour HexpansionType the low byte of its PID must match exactly one HexDriveType entry, and - the motor/servo/stepper capability counts must agree. + the motor/servo capability counts must agree. """ from sim.apps.BadgeBot import BadgeBotApp from sim.apps.BadgeBot.EEPROM.hexdrive import _HEXDRIVE_TYPES @@ -75,10 +75,6 @@ def test_hexdrive_type_pids_consistent(): f"Servo count mismatch for PID 0x{pid_byte:02X}: " f"HexpansionType={ht.servos}, HexDriveType={hdt.servos}" ) - assert ht.steppers == hdt.steppers, ( - f"Stepper count mismatch for PID 0x{pid_byte:02X}: " - f"HexpansionType={ht.steppers}, HexDriveType={hdt.steppers}" - ) def test_new_states_exist(): From 5cfcdf6d93b9583db6125568aea61d7f75944ce8 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 26 Apr 2026 20:31:29 +0100 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index f80a252..f2bd957 100644 --- a/app.py +++ b/app.py @@ -263,7 +263,7 @@ def __init__(self): HexpansionType(0xFFFF, "Blank", sub_type="")] # Virtual type to represent blank EEPROMs self.HEXDRIVE_HEXPANSION_INDEX = 0 # Index in the HEXPANSION_TYPES list which corresponds to the basic HexDrive type - self.HEXDRIVE_V2_HEXPANSION_INDEX = 6 # Index in the HEXPANSION_TYPES list which corresponds to the basic HexDrive V2 type + self.HEXDRIVE_V2_HEXPANSION_INDEX = 5 # Index in the HEXPANSION_TYPES list which corresponds to the basic HexDrive V2 type self.HEXSENSE_HEXPANSION_INDEX = 4 # Index in the HEXPANSION_TYPES list which corresponds to the HexSense type self.HEXTEST_HEXPANSION_INDEX = 8 # Index in the HEXPANSION_TYPES list which corresponds to the HexTest type self.HEXDIAG_HEXPANSION_INDEX = 9 # Index in the HEXPANSION_TYPES list which corresponds to the HexDiag type From 3791537e0cc2e1c7dbe8abcc0cad7ec496974a5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:33:03 +0000 Subject: [PATCH 3/3] Remove motor_step from keep-alive docs in README Agent-Logs-Url: https://github.com/TeamRobotmad/BadgeBot/sessions/e46b1873-af28-4953-9b7e-4706b0e742ae Co-authored-by: Robotmad <3315650+Robotmad@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e080a96..e863449 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ You can use one motor and 1 or 2 servos simultaneously. You can adjust the PWM frequency, default 20000Hz for motors and 50Hz for servos by calling the ```set_freq()``` function. #### Keep Alive -To protect against most badge/software crashes causing the motors or servos to run out of control there is a keep alive mechanism which means that if you do not make a call to the ```set_pwm```, ```set_motors```, ```motor_step``` or ```set_servoposition``` functions the motors/servos will be turned off after 1000mS (default - which can be changed with a call to ```set_keep_alive()```). +To protect against most badge/software crashes causing the motors or servos to run out of control there is a keep alive mechanism which means that if you do not make a call to the ```set_pwm```, ```set_motors``` or ```set_servoposition``` functions the motors/servos will be turned off after 1000mS (default - which can be changed with a call to ```set_keep_alive()```). ### Developers setup This is to help develop the BadgeBot application using the Badge simulator.