# MA-40 Firmware

Arduino sketches for the MA-40’s custom electronics. Three boards run firmware; they share one I2C bus and split responsibilities between a single **master** (E-Port) and two **slaves** (display driver and magazine).

Build/flash with the [Arduino-Pico](https://github.com/earlephilhower/arduino-pico) (Earle Philhower) core targeting an RP2350 board, unless a sketch says otherwise.

## System overview

```
                    I2C bus (SDA / SCL)
                           |
     +---------------------+---------------------+
     |                     |                     |
  E-Port              Display Driver          Magazine
  (master)            (slave 0x42)            (slave 0x08)
     |
     +-- GPIO14 / GPIO15  flashlight / load buttons (BUT1 / BUT2)
     +-- BNO055  (0x28)  compass (LaserCompass board)
     +-- GPIO10           flashlight / load MOSFET
     +-- ADC              battery voltage
```

The **LaserCompass** PCBA is on the same bus for the BNO055 and carries laser toggle hardware, but it has no sketch in this repo.

| Package | Board | I2C role | Address |
|---|---|---|---|
| [`eport`](eport/) | E-Port | Master | — |
| [`display`](display/) | Display Driver | Slave | `0x42` |
| [`magazine`](magazine/) | Magazine | Slave | `0x08` |

---

## `eport` — E-Port (system controller)

**Responsibility:** Own system-level logic. This is the I2C master that talks to every peripheral, drives the flashlight/load, and pushes HUD data to the display.

What it does:

- **Flashlight / load control** — Reads the ambidextrous panel buttons on GPIO 14 and GPIO 15 (active low). Single press toggles the load on GPIO 10; double-tap starts a strobe. Enforces a 60 s on-timeout and 30 s cooldown, and reports that timer state to the display.
- **Battery sensing** — Reads the pack through a divider on ADC0 (GPIO 26), maps millivolts to a 0–100% charge estimate, and sends both to the display.
- **Compass** — Initializes the BNO055 (`0x28`) in NDOF mode, polls Euler heading, and forwards degrees 0–359 to the display.
- **Ammo forwarding** — Polls the magazine slave for a 2-byte round count and relays it to the display. Treats a failed read as “no magazine” (`0xFFFF`). Bus transfers are time-bounded with auto-recovery so hot-plugging the mag cannot wedge the shared bus.
- **Magazine calibration orchestration** — Polls the display for button-3 events. Long press enters/cancels calibration; short press saves. Commands the magazine (`0xA1` start / `0xA2` save / `0xA3` abort) and drives the display’s calibration UI phases.

This is the sketch that should be flashed onto the E-Port board.

---

## `display` — Display Driver (HUD)

**Responsibility:** Render the on-weapon HUD and handle local display UI. It is an I2C slave; it does not invent sensor values — the E-Port writes them.

What it does:

- Drives a Waveshare 1.47″ 172×320 ST7789 TFT over SPI, compositing live elements onto a baked-in `background.h` image.
- Exposes a register map the master writes into:
  - battery mV / battery %
  - flashlight on/off
  - ammo count (or disconnected sentinel)
  - compass heading
  - load timeout / cooldown bar
  - magazine calibration UI phase + value
- Redraws only changed HUD regions (ammo, compass arrow, battery readouts, load bar).
- Local buttons: brightness up/down (with hold-to-repeat), plus an action button whose short/long presses are queued and read back by the master for magazine calibration.
- Shows an “Initializing” splash until the first valid packet; escalates to a SYSTEM ERROR screen if the bus stays silent.

This is the sketch that should be flashed onto the Display Driver board.

---

## `magazine` — Magazine (ammo sensor)

**Responsibility:** Sense rounds remaining in *this* magazine and report a ready-to-display ammo count over I2C. Each magazine board is self-contained so capacities and SoftPot curves can differ per mag.

What it does:

- Reads a SoftPot on ADC GPIO 26 (12-bit), moving-average filters it, and maps the reading to a round count via a per-capacity lookup table (nearest-band / midpoint thresholds).
- Answers I2C reads at `0x08` with a big-endian `uint16` ammo count in normal mode.
- Supports an in-device calibration mode driven by master command bytes:
  - records stable pot “plateaus” as rounds are fired from a full mag
  - on save, reverses that sequence into the capacity table and persists it to flash (EEPROM emulation)
  - while calibrating, reads return `[0xCA][plateaus recorded]` instead of ammo
- Falls back to a compiled-in default 10-round table if flash has no valid calibration record.

This is the sketch that should be flashed onto each Magazine board.

---

## `_dev` — development / tooling (not production)

These are helpers and earlier prototypes; they are not flashed for a normal build.

| Path | Purpose |
|---|---|
| `_dev/display_test` | Standalone SPI panel bring-up (colors, text, backlight fade). |
| `_dev/display_proto` | Earlier HUD prototype on Adafruit QT Py ESP32; protocol ancestor of `display`. |
| `_dev/flashlight_burndown` | Holds the E-Port load MOSFET on forever for battery / thermal tests. |
| `_dev/bgimage_maker` | Python tools to turn HUD artwork (`bg.png`) into the RGB565 `background.h` header. |

---

## How the pieces interact

1. E-Port boots, scans the bus, configures compass and button expander.
2. It continuously pushes battery, heading, load/flashlight state, and ammo to the display.
3. The magazine independently converts SoftPot position → round count; the E-Port only forwards that number.
4. For a new magazine capacity curve: long-press the display action button → load & insert mag → fire every round → short-press to save. The magazine stores the table; the display only shows the guided UI the E-Port drives.
