Add Zephyr RTOS port of OpenLaserTag remote control
Port the MicroPython firmware to a maintainable Zephyr C application under zephyr_app/, alongside the untouched original reference implementation. - OLT IR protocol in pure C (olt_proto) with a host unit test - buttons, piezo, RGB PWM, battery ADC, IR TX (software 56 kHz carrier), IR RX (edge-capture + timeout decode), and gated BLE Nordic UART service - uasyncio tasks mapped to Zephyr threads / workqueues / k_timer - rpi_pico overlay (full) plus esp32 and nucleo_f401re stubs - prj.conf / Kconfig / overlay-ble.conf build configuration - docs/migration-notes.md: hardware inventory, module/API maps, open questions Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f9bdd86e6d
commit
9149761c82
34
.claude/settings.json
Normal file
34
.claude/settings.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"permissions": {
|
||||
"defaultMode": "acceptEdits",
|
||||
"allow": [
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Grep",
|
||||
"Glob",
|
||||
"LS",
|
||||
"Bash(git status*)",
|
||||
"Bash(git diff*)",
|
||||
"Bash(ls *)",
|
||||
"Bash(find *)",
|
||||
"Bash(west build*)",
|
||||
"Bash(cmake *)",
|
||||
"Bash(ninja *)",
|
||||
"Bash(python3 -m compileall*)"
|
||||
],
|
||||
"ask": [
|
||||
"Bash(git commit*)",
|
||||
"Bash(west flash*)",
|
||||
"Bash(west debug*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf *)",
|
||||
"Bash(git push*)",
|
||||
"Bash(curl *)",
|
||||
"Bash(wget *)",
|
||||
"Bash(sudo *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
build/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
198
CLAUDE.md
Normal file
198
CLAUDE.md
Normal file
@ -0,0 +1,198 @@
|
||||
# Project: MicroPython to Zephyr port
|
||||
|
||||
## Goal
|
||||
|
||||
Port this existing MicroPython firmware to Zephyr RTOS for MCU boards similar to:
|
||||
|
||||
- Raspberry Pi Pico / RP2040 / RP2350
|
||||
- STM32
|
||||
- ESP32
|
||||
|
||||
The goal is a maintainable Zephyr C/C++ firmware, not a line-by-line translation.
|
||||
|
||||
## Primary strategy
|
||||
|
||||
Use the original MicroPython code as the reference implementation and create a clean Zephyr application beside it.
|
||||
|
||||
Do not directly mimic Python structure if a Zephyr-native structure is better. Preserve behavior, timing, hardware interactions, and safety assumptions.
|
||||
|
||||
## Required workflow
|
||||
|
||||
Claude must follow this order:
|
||||
|
||||
1. Inspect the repository structure.
|
||||
2. Identify all MicroPython entry points, usually:
|
||||
- `main.py`
|
||||
- `boot.py`
|
||||
- `lib/`
|
||||
- `src/`
|
||||
- board-specific files
|
||||
3. Create or update `docs/migration-notes.md`.
|
||||
4. Create a hardware/peripheral inventory.
|
||||
5. Create a module-by-module migration map.
|
||||
6. Create or update the Zephyr app under `zephyr_app/`.
|
||||
7. Port the smallest buildable slice first.
|
||||
8. Run `git diff` after each meaningful change.
|
||||
9. Run `west build` if Zephyr is available.
|
||||
10. If build fails, fix iteratively.
|
||||
11. If Zephyr is not installed locally, still produce correct source/config files and document exact setup steps.
|
||||
|
||||
## Strict rules
|
||||
|
||||
- Do not delete the original MicroPython implementation.
|
||||
- If restructuring is needed, preserve original code under `legacy_micropython/`.
|
||||
- Do not make one huge rewrite.
|
||||
- Prefer small, reviewable edits.
|
||||
- Do not run destructive commands.
|
||||
- Do not push to git remotes.
|
||||
- Do not flash hardware unless explicitly requested.
|
||||
- Do not assume pin mappings when they are unclear; document assumptions in `docs/migration-notes.md`.
|
||||
- Prefer Zephyr portable APIs over vendor-specific SDK calls.
|
||||
- Prefer devicetree overlays over hardcoded pin numbers.
|
||||
- Avoid dynamic allocation unless clearly justified.
|
||||
- Keep board-specific configuration separate.
|
||||
|
||||
## Hardware inventory checklist
|
||||
|
||||
Before implementation, identify:
|
||||
|
||||
- GPIO inputs/outputs
|
||||
- button polarity and pull-up/pull-down behavior
|
||||
- LEDs and active-high/active-low behavior
|
||||
- timers
|
||||
- sleep/delay timing
|
||||
- PWM channels
|
||||
- ADC channels and scaling assumptions
|
||||
- I2C buses, addresses, speeds
|
||||
- SPI buses, chip-selects, modes, speeds
|
||||
- UART ports and baud rates
|
||||
- Wi-Fi/BLE/networking usage
|
||||
- filesystem/storage usage
|
||||
- watchdog usage
|
||||
- interrupt-like callbacks
|
||||
- async/event-loop behavior
|
||||
- power-management assumptions
|
||||
|
||||
## MicroPython to Zephyr mapping
|
||||
|
||||
Use these mappings as the starting point:
|
||||
|
||||
- `machine.Pin` -> Zephyr GPIO API using `gpio_dt_spec`
|
||||
- `machine.PWM` -> Zephyr PWM API
|
||||
- `machine.ADC` -> Zephyr ADC API
|
||||
- `machine.I2C` -> Zephyr I2C API
|
||||
- `machine.SPI` -> Zephyr SPI API
|
||||
- `machine.UART` -> Zephyr UART API
|
||||
- `time.sleep_ms()` -> `k_msleep()`
|
||||
- `time.sleep_us()` -> `k_usleep()`
|
||||
- `Timer` callbacks -> Zephyr timers, work queues, or threads
|
||||
- polling loops -> Zephyr thread, timer, work queue, or explicit state machine
|
||||
- `uasyncio` -> Zephyr threads/events/work queues/state machine
|
||||
- `network` on ESP32 -> Zephyr networking/Wi-Fi stack if needed
|
||||
- `json`, `struct`, protocol code -> native C implementation or small helper modules
|
||||
- Python dictionaries/config -> C structs, Kconfig, devicetree, or generated config
|
||||
|
||||
## Target output structure
|
||||
|
||||
```text
|
||||
zephyr_app/
|
||||
CMakeLists.txt
|
||||
prj.conf
|
||||
README.md
|
||||
src/
|
||||
main.c
|
||||
app_config.h
|
||||
board_io.c
|
||||
board_io.h
|
||||
boards/
|
||||
rpi_pico.overlay
|
||||
esp32_devkitc_wroom.overlay
|
||||
nucleo_f401re.overlay
|
||||
docs/
|
||||
migration-notes.md
|
||||
migration-prompt.md
|
||||
.claude/
|
||||
settings.json
|
||||
legacy_micropython/
|
||||
...
|
||||
```
|
||||
|
||||
## Board priority
|
||||
|
||||
Default target order:
|
||||
|
||||
1. `rpi_pico`
|
||||
2. `esp32_devkitc_wroom`
|
||||
3. `nucleo_f401re` as generic STM32 reference unless another STM32 board is evident
|
||||
|
||||
If the project indicates a different STM32 board, use that board instead.
|
||||
|
||||
## Zephyr conventions
|
||||
|
||||
Use:
|
||||
|
||||
- `gpio_dt_spec` for GPIO
|
||||
- devicetree aliases for board-specific pins
|
||||
- `LOG_MODULE_REGISTER`
|
||||
- `CONFIG_LOG`
|
||||
- clear init functions
|
||||
- explicit error checking
|
||||
- `k_msleep` for simple periodic loops
|
||||
- work queues/timers for callback-style behavior
|
||||
- `prj.conf` for app features
|
||||
- board overlays for pin mapping
|
||||
|
||||
Avoid:
|
||||
|
||||
- hardcoded GPIO controller names
|
||||
- hardcoded peripheral instances unless unavoidable
|
||||
- busy waiting
|
||||
- large global mutable state without structure
|
||||
- vendor SDK calls unless no Zephyr API exists
|
||||
|
||||
## Build commands
|
||||
|
||||
Use these first:
|
||||
|
||||
```bash
|
||||
west build -b rpi_pico zephyr_app
|
||||
west build -b esp32_devkitc_wroom zephyr_app
|
||||
west build -b nucleo_f401re zephyr_app
|
||||
```
|
||||
|
||||
Clean rebuild examples:
|
||||
|
||||
```bash
|
||||
west build -p always -b rpi_pico zephyr_app
|
||||
```
|
||||
|
||||
Flash only when explicitly requested:
|
||||
|
||||
```bash
|
||||
west flash
|
||||
```
|
||||
|
||||
## Deliverables
|
||||
|
||||
Claude should produce:
|
||||
|
||||
- `zephyr_app/` with buildable Zephyr skeleton
|
||||
- migrated core logic where possible
|
||||
- board overlays
|
||||
- `prj.conf`
|
||||
- `zephyr_app/README.md`
|
||||
- `docs/migration-notes.md`
|
||||
- TODO list for hardware assumptions
|
||||
- notes about what was not ported yet and why
|
||||
|
||||
## Definition of done
|
||||
|
||||
A first pass is acceptable when:
|
||||
|
||||
- the Zephyr app has a valid structure
|
||||
- the build system files exist
|
||||
- board overlays exist
|
||||
- core MicroPython logic is mapped into C structure
|
||||
- unresolved hardware questions are documented
|
||||
- build commands are documented
|
||||
- no original MicroPython code was destroyed
|
||||
113
docs/migration-notes.md
Normal file
113
docs/migration-notes.md
Normal file
@ -0,0 +1,113 @@
|
||||
# Migration notes — OpenLaserTag remote control (MicroPython → Zephyr)
|
||||
|
||||
Status: first-pass full port. Source files live under `zephyr_app/`. The original
|
||||
MicroPython firmware (`main.py`, `lib/`) is untouched and remains the reference
|
||||
implementation.
|
||||
|
||||
## Source inventory
|
||||
|
||||
| MicroPython file | Responsibility |
|
||||
|---|---|
|
||||
| `main.py` | uasyncio app: device id, piezo, battery monitor, BLE RX handler, OLT command table, IR TX/RX wiring, buttons, heartbeat, main loop |
|
||||
| `lib/rgb.py` | `RGB` class: 3× `machine.PWM` (GPIO22/26/27) @100 Hz, color presets, global power scaling |
|
||||
| `lib/ble_uart/__init__.py` | `Ble_uart`: Nordic UART GATT service, RX append buffer, notify TX, advertise |
|
||||
| `lib/ble_uart/ble_advertising.py` | Advertising payload TLV encoder |
|
||||
| `lib/olt_lib/ir_tx/__init__.py` | `IR` base: builds µs mark/space array, ISR/RMT/PIO transmit |
|
||||
| `lib/olt_lib/ir_tx/olt.py` | `LT_24` encoder: 24-bit, bit-reversed, 2400/600/1200 µs timing, 56 kHz |
|
||||
| `lib/olt_lib/ir_tx/rp2_rmt.py` | RP2040 PIO carrier driver (platform-specific) |
|
||||
| `lib/olt_lib/ir_rx/__init__.py` | `IR_RX` base: pin IRQ records edge times, one-shot timer triggers decode |
|
||||
| `lib/olt_lib/ir_rx/olt.py` | `LT_24` decoder: validates edges, extracts 24-bit packet |
|
||||
| `lib/primitives/pushbutton.py` | debounced button: press/release/long(1000 ms)/double(400 ms) |
|
||||
| `lib/primitives/aadc.py` | async ADC polling wrapper |
|
||||
| `lib/logging.py` | custom logging (replaced by Zephyr `LOG`) |
|
||||
|
||||
## Hardware map
|
||||
|
||||
| Function | MicroPython source | Pin / peripheral | Zephyr mapping | Status |
|
||||
|---|---|---:|---|---|
|
||||
| Game buttons ×4 | main.py:371 | GPIO18/21/20/19, pull-up, active-low | `gpio-keys` node `buttons`, IRQ + workqueue debounce | Ported |
|
||||
| IR RX | main.py:200 | GPIO16, pull-up, active-low (TSOP4856) | alias `ir-rx`, GPIO IRQ + cycle timestamps | Ported |
|
||||
| IR TX | main.py:223 | GPIO17, active-high (PN2222A → IR LED) | alias `ir-tx`, software 56 kHz carrier | Ported (SW carrier) |
|
||||
| Piezo | main.py:44 | GPIO15, active-high (NPN) | alias `piezo`, GPIO toggle | Ported |
|
||||
| RGB LED | lib/rgb.py:17-19 | GPIO22/26/27, PWM 100 Hz, active-high | `pwm-leds` aliases `rgb-red/green/blue` | Ported |
|
||||
| Battery sense | main.py:67 | GPIO28 / ADC ch2 | `zephyr_user` io-channel, `adc_dt_spec` | Ported |
|
||||
| Heartbeat LED | main.py:255 | onboard "LED" | alias `heartbeat-led` (GPIO25 on plain Pico) | Ported |
|
||||
| BLE UART | lib/ble_uart | radio | custom NUS GATT service (`CONFIG_APP_BLE`) | Ported (Pico W/ESP32 only) |
|
||||
|
||||
## Module map
|
||||
|
||||
| MicroPython | Zephyr | Status |
|
||||
|---|---|---|
|
||||
| `main.py` glue / `asyncio.run` | `src/main.c` | Ported |
|
||||
| OLT encode/decode + tables | `src/olt_proto.{c,h}`, `src/app_config.h` | Ported |
|
||||
| piezo beeps | `src/piezo.{c,h}` | Ported |
|
||||
| `Pushbutton` tasks → IR | `src/buttons.{c,h}` | Ported |
|
||||
| `lib/rgb.py` | `src/rgb_led.{c,h}` | Ported |
|
||||
| `battery_monitor` + `AADC` | `src/battery.{c,h}` | Ported |
|
||||
| `ir_tx` (IR/LT_24) | `src/ir_tx.{c,h}` | Ported (SW carrier) |
|
||||
| `ir_rx` (IR_RX/LT_24) | `src/ir_rx.{c,h}` | Ported |
|
||||
| `ble_uart` | `src/ble_nus.{c,h}` | Ported (gated) |
|
||||
| onboard LED heartbeat / unique_id | `src/board_io.{c,h}` | Ported |
|
||||
|
||||
## Concurrency mapping (uasyncio → Zephyr)
|
||||
|
||||
| uasyncio | Zephyr |
|
||||
|---|---|
|
||||
| `alive_check` | `k_timer` toggling heartbeat LED |
|
||||
| `battery_monitor` (10 s) | `k_timer` → work item: ADC read + BLE notify |
|
||||
| `Pushbutton` events | GPIO IRQ → `k_work_delayable` debounce/long/double |
|
||||
| button → `olt_sent_ir` | button callback → IR TX request |
|
||||
| `ble_rx_handler` | NUS RX callback → parse → IR TX / RGB |
|
||||
| IR RX pin IRQ + timer | GPIO IRQ timestamps + frame-timeout `k_timer` → decode work |
|
||||
| `ir_tx.transmit` ISR/PIO | dedicated high-priority IR TX thread, `k_busy_wait` carrier |
|
||||
|
||||
## API mapping
|
||||
|
||||
| MicroPython API | Zephyr API | Notes |
|
||||
|---|---|---|
|
||||
| `machine.Pin` | `gpio_dt_spec` | devicetree aliases / `gpio-keys` |
|
||||
| `machine.PWM` | `pwm_dt_spec` | RGB via `pwm-leds` |
|
||||
| `machine.ADC` | `adc_dt_spec` | `zephyr_user` io-channel |
|
||||
| `time.sleep_ms` | `k_msleep` | — |
|
||||
| `time.sleep_us` / busy timing | `k_busy_wait` | only inside IR TX carrier |
|
||||
| `ticks_us` | `k_cycle_get_32` + `k_cyc_to_us_*` | IR RX edge timing |
|
||||
| `Timer` one-shot | `k_timer` | IR RX frame timeout |
|
||||
| `uasyncio` tasks | threads / workqueue / `k_timer` | see table above |
|
||||
| `bluetooth` NUS | Zephyr `bluetooth/gatt` | custom 128-bit service |
|
||||
| custom `logging` | `LOG_MODULE_REGISTER` / `LOG_*` | — |
|
||||
|
||||
## OLT protocol summary
|
||||
|
||||
- 24-bit payload `tx1<<16 | tx2<<8 | tx3`, transmitted bit-reversed, LSB-first on the wire.
|
||||
- Frame: lead mark 2400 µs + space 600 µs, then 24 bits: mark 1200 µs (1) or 600 µs (0),
|
||||
each followed by a 600 µs space. Carrier 56 kHz, ~40 % duty.
|
||||
- `bit23` (`0x800000`) = command packet; otherwise a shot packet.
|
||||
- Shot decode (`packet >> 10`): `id = v>>6`, `team = (v>>4)&0x3`, `damage = v&0xF`
|
||||
indexed into `{0,1,4,5,7,10,15,17,20,25,30,35,40,50,75,100}`; teams `RED/BLUE/YELLOW/GREEN`.
|
||||
- Commands: NewGame `0x8305E8`, Explode `0x830BE8`, TestShot `0x082400`, SensorTest `0x831500`.
|
||||
|
||||
## Open questions / TODO (hardware assumptions)
|
||||
|
||||
- **Battery divider ratio.** main.py:87 divides the measured voltage by 2 before the
|
||||
alkaline capacity lookup, implying a 1:2 divider on VSYS/GPIO28. Confirm the real PCB
|
||||
divider and update `BATTERY_DIVIDER_*` in `app_config.h`. ADC reference is assumed 3.3 V.
|
||||
- **RP2040 PWM channel numbers.** The overlay assumes GPIO22/26/27 map to PWM channels
|
||||
6/10/11 (slice·2+chan). Verify against the board's `&pwm` once Zephyr is available.
|
||||
- **IR TX carrier.** Implemented as a software-bit-banged 56 kHz carrier for portability.
|
||||
Recommended upgrade: RP2040 PIO or ESP32 RMT (the MicroPython code used both). The
|
||||
software carrier monopolises a high-priority thread for ~30 ms per frame.
|
||||
- **IR TX duty.** 40 % duty (ir_tx/olt.py) is approximated by the SW carrier; not tunable
|
||||
without PWM/PIO.
|
||||
- **Heartbeat LED on Pico W.** The Pico W onboard LED is behind the CYW43 chip, not a plain
|
||||
GPIO. The overlay uses GPIO25 (valid on the non-W Pico). Adjust for Pico W if needed.
|
||||
- **ESP32 / STM32 pin maps.** The `esp32_devkitc_wroom` and `nucleo_f401re` overlays are
|
||||
stubs with TODO pin assignments — verify before use.
|
||||
- **BLE board support.** BLE requires a radio: build the Pico W or ESP32 target with
|
||||
`-DEXTRA_CONF_FILE=overlay-ble.conf`.
|
||||
|
||||
## Build status
|
||||
|
||||
`west` / the Zephyr SDK is **not installed in the current environment**, so the application
|
||||
has not been compiled here. The source, overlays, `prj.conf`, `Kconfig`, and `CMakeLists.txt`
|
||||
follow Zephyr conventions and are intended to build per `zephyr_app/README.md`. Record
|
||||
`west build` results here once a Zephyr environment is available.
|
||||
26
migration-prompt.md
Normal file
26
migration-prompt.md
Normal file
@ -0,0 +1,26 @@
|
||||
Mám existující firmware v MicroPythonu. Přenes ho do Zephyr RTOS.
|
||||
|
||||
Postupuj podle CLAUDE.md v rootu projektu.
|
||||
|
||||
Nejdřív:
|
||||
1. Projdi strukturu projektu.
|
||||
2. Najdi MicroPython entrypointy, typicky main.py, boot.py, lib/.
|
||||
3. Udělej inventuru hardware použití.
|
||||
4. Vytvoř docs/migration-notes.md.
|
||||
5. Navrhni migrační mapu MicroPython modulů na Zephyr moduly.
|
||||
|
||||
Potom:
|
||||
1. Vytvoř nebo aktualizuj zephyr_app/.
|
||||
2. Připrav build pro rpi_pico jako první target.
|
||||
3. Přidej také výchozí overlay/config pro esp32_devkitc_wroom a nucleo_f401re, pokud není z projektu jasný jiný STM32 board.
|
||||
4. Portuj hlavní logiku, GPIO, časování a periferie.
|
||||
5. Používej devicetree overlays a Zephyr portable API.
|
||||
6. Po větších změnách spusť git diff.
|
||||
7. Pokud je dostupný Zephyr/west, spusť west build.
|
||||
8. Pokud Zephyr není instalovaný, napiš přesné setup kroky do README.
|
||||
|
||||
Důležité:
|
||||
- nemaž původní MicroPython kód
|
||||
- neflashuj hardware
|
||||
- nedělej git push
|
||||
- nehádej nejasné piny; zapiš je jako TODO do docs/migration-notes.md
|
||||
20
zephyr_app/CMakeLists.txt
Normal file
20
zephyr_app/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(openlasertag_remote)
|
||||
|
||||
target_sources(app PRIVATE
|
||||
src/main.c
|
||||
src/board_io.c
|
||||
src/olt_proto.c
|
||||
src/piezo.c
|
||||
src/buttons.c
|
||||
src/rgb_led.c
|
||||
src/battery.c
|
||||
src/ir_tx.c
|
||||
src/ir_rx.c
|
||||
)
|
||||
|
||||
target_sources_ifdef(CONFIG_APP_BLE app PRIVATE
|
||||
src/ble_nus.c
|
||||
)
|
||||
24
zephyr_app/Kconfig
Normal file
24
zephyr_app/Kconfig
Normal file
@ -0,0 +1,24 @@
|
||||
# Application-level Kconfig for the OpenLaserTag remote control.
|
||||
# This file becomes the Kconfig root for the application and therefore must
|
||||
# source the Zephyr Kconfig tree at the end.
|
||||
|
||||
menu "OpenLaserTag application"
|
||||
|
||||
config APP_BLE
|
||||
bool "Enable BLE Nordic UART service"
|
||||
default n
|
||||
select BT
|
||||
select BT_PERIPHERAL
|
||||
select BT_GATT_DYNAMIC_DB
|
||||
help
|
||||
Enables the Nordic UART Service used for wireless command/telemetry
|
||||
(src/ble_nus.c). Requires a board with a Bluetooth controller, e.g.
|
||||
the Raspberry Pi Pico W (rpi_pico/rp2040/w) or an ESP32. Leave this
|
||||
disabled for the plain rpi_pico, which has no radio.
|
||||
|
||||
Build with: west build -b rpi_pico/rp2040/w zephyr_app -- \
|
||||
-DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
|
||||
endmenu
|
||||
|
||||
source "Kconfig.zephyr"
|
||||
102
zephyr_app/README.md
Normal file
102
zephyr_app/README.md
Normal file
@ -0,0 +1,102 @@
|
||||
# OpenLaserTag remote control — Zephyr firmware
|
||||
|
||||
Zephyr C port of the MicroPython OpenLaserTag remote control (the original
|
||||
`main.py` + `lib/` remain in the repository root as the reference
|
||||
implementation). See `docs/migration-notes.md` for the full hardware inventory,
|
||||
module map and open questions.
|
||||
|
||||
## What it does
|
||||
|
||||
- Sends 24-bit OpenLaserTag (OLT) IR commands from four buttons.
|
||||
- Receives and decodes OLT IR (shots and commands).
|
||||
- RGB status LED, piezo feedback, periodic battery telemetry.
|
||||
- Optional BLE Nordic UART for wireless command/telemetry.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
zephyr_app/
|
||||
CMakeLists.txt prj.conf Kconfig overlay-ble.conf
|
||||
src/
|
||||
main.c app_config.h board_io.*
|
||||
olt_proto.* piezo.* buttons.* rgb_led.*
|
||||
battery.* ir_tx.* ir_rx.* ble_nus.*
|
||||
boards/
|
||||
rpi_pico.overlay esp32_devkitc_wroom.overlay nucleo_f401re.overlay
|
||||
tests/
|
||||
olt_proto_test.c # host unit test for the protocol layer
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This repository does **not** contain a Zephyr installation. Set up Zephyr +
|
||||
the SDK once (see the Zephyr "Getting Started" guide), e.g.:
|
||||
|
||||
```bash
|
||||
pip install west
|
||||
west init ~/zephyrproject
|
||||
cd ~/zephyrproject && west update && west zephyr-export
|
||||
west sdk install
|
||||
# then point west at this app's parent or run from here:
|
||||
source ~/zephyrproject/zephyr/zephyr-env.sh
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
From the repository root (after `west` and `ZEPHYR_BASE` are available):
|
||||
|
||||
```bash
|
||||
# Primary target (no BLE — the plain Pico has no radio)
|
||||
west build -b rpi_pico zephyr_app
|
||||
|
||||
# Clean rebuild
|
||||
west build -p always -b rpi_pico zephyr_app
|
||||
```
|
||||
|
||||
### With BLE (radio-capable boards)
|
||||
|
||||
```bash
|
||||
# Raspberry Pi Pico W
|
||||
west build -b rpi_pico/rp2040/w zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
|
||||
# ESP32 DevKitC
|
||||
west build -b esp32_devkitc_wroom/esp32/procpu zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
```
|
||||
|
||||
### Other (stub) boards
|
||||
|
||||
The ESP32 and STM32 overlays carry placeholder pin maps — verify them before
|
||||
use (see migration-notes.md):
|
||||
|
||||
```bash
|
||||
west build -b esp32_devkitc_wroom zephyr_app
|
||||
west build -b nucleo_f401re zephyr_app
|
||||
```
|
||||
|
||||
## Host test (no hardware)
|
||||
|
||||
The protocol layer is plain C and can be tested on a workstation:
|
||||
|
||||
```bash
|
||||
cd zephyr_app/tests
|
||||
cc -I ../src olt_proto_test.c ../src/olt_proto.c -o /tmp/olt_test && /tmp/olt_test
|
||||
# -> ALL OLT PROTOCOL TESTS PASSED
|
||||
```
|
||||
|
||||
## Flash (only when explicitly requested)
|
||||
|
||||
```bash
|
||||
west flash
|
||||
```
|
||||
|
||||
## Notes / known limitations
|
||||
|
||||
- **IR carrier is software-generated** (bit-banged 56 kHz, ~39% duty) for
|
||||
portability. Recommended upgrade: RP2040 PIO or ESP32 RMT. See
|
||||
migration-notes.md.
|
||||
- **Heartbeat LED** uses GPIO25 (valid on the plain Pico). The Pico W onboard
|
||||
LED is behind the CYW43 chip — adjust the overlay if you need it there.
|
||||
- **Battery divider ratio** and a few **PWM/ADC channel numbers** are
|
||||
assumptions flagged as TODO in migration-notes.md; confirm against hardware.
|
||||
- Board-specific pins live in `boards/*.overlay`; behaviour lives in `src/`;
|
||||
tunables (timings, command table) live in `src/app_config.h`.
|
||||
67
zephyr_app/boards/esp32_devkitc_wroom.overlay
Normal file
67
zephyr_app/boards/esp32_devkitc_wroom.overlay
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* ESP32-DevKitC (WROOM) overlay - STUB. Pin numbers are starting points taken
|
||||
* from the MicroPython platform branch (main.py) and MUST be verified against
|
||||
* your wiring before use. See docs/migration-notes.md.
|
||||
*
|
||||
* TODO (not yet provided here - modules degrade to no-ops with a warning):
|
||||
* - RGB LED PWM: requires ESP32 LEDC channel + pinctrl setup for the three
|
||||
* colour pins (rgb-red/green/blue aliases -> pwm-leds).
|
||||
* - Battery ADC: requires an ADC channel for the divider input
|
||||
* (zephyr_user io-channels).
|
||||
*
|
||||
* NOTE: main.py used GPIO23 for BOTH IR RX and IR TX on ESP32, which cannot be
|
||||
* correct for simultaneous use. IR RX is tentatively moved to GPIO22 here.
|
||||
*/
|
||||
|
||||
#include <zephyr/dt-bindings/gpio/gpio.h>
|
||||
|
||||
/ {
|
||||
aliases {
|
||||
heartbeat-led = &heartbeat_led;
|
||||
piezo = &piezo_out;
|
||||
ir-tx = &ir_tx_out;
|
||||
ir-rx = &ir_rx_in;
|
||||
};
|
||||
|
||||
heartbeat_led: heartbeat_led {
|
||||
/* Many WROOM boards use GPIO2 for the onboard LED. TODO verify. */
|
||||
gpios = <&gpio0 2 GPIO_ACTIVE_HIGH>;
|
||||
label = "Heartbeat LED";
|
||||
};
|
||||
|
||||
piezo_out: piezo_out {
|
||||
gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; /* main.py piezo pin */
|
||||
label = "Piezo";
|
||||
};
|
||||
|
||||
ir_tx_out: ir_tx_out {
|
||||
gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>; /* main.py ESP32 IR TX */
|
||||
label = "IR TX";
|
||||
};
|
||||
|
||||
ir_rx_in: ir_rx_in {
|
||||
/* TODO: confirm. GPIO23 collided with TX in the original. */
|
||||
gpios = <&gpio0 22 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "IR RX";
|
||||
};
|
||||
|
||||
buttons: buttons {
|
||||
compatible = "gpio-keys";
|
||||
btn0: button_0 {
|
||||
gpios = <&gpio0 18 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "NewGame";
|
||||
};
|
||||
btn1: button_1 {
|
||||
gpios = <&gpio0 21 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "Explode";
|
||||
};
|
||||
btn2: button_2 {
|
||||
gpios = <&gpio0 19 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "TestShot";
|
||||
};
|
||||
btn3: button_3 {
|
||||
gpios = <&gpio0 5 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "SensorTest";
|
||||
};
|
||||
};
|
||||
};
|
||||
63
zephyr_app/boards/nucleo_f401re.overlay
Normal file
63
zephyr_app/boards/nucleo_f401re.overlay
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Nucleo-F401RE overlay - STUB, used as a generic STM32 reference. All pins
|
||||
* are placeholders and MUST be verified against your wiring. The original
|
||||
* MicroPython firmware targeted a PyBoard ("X1"/"X2"/"X3") for STM32, so there
|
||||
* is no direct Nucleo pin map - these are reasonable Arduino-header choices.
|
||||
* See docs/migration-notes.md.
|
||||
*
|
||||
* TODO (not provided here - modules degrade to no-ops with a warning):
|
||||
* - RGB LED PWM via an STM32 timer (&pwm) + pinctrl.
|
||||
* - Battery ADC channel (zephyr_user io-channels).
|
||||
* - BLE is unavailable on this board (no radio).
|
||||
*/
|
||||
|
||||
#include <zephyr/dt-bindings/gpio/gpio.h>
|
||||
|
||||
/ {
|
||||
aliases {
|
||||
heartbeat-led = &heartbeat_led;
|
||||
piezo = &piezo_out;
|
||||
ir-tx = &ir_tx_out;
|
||||
ir-rx = &ir_rx_in;
|
||||
};
|
||||
|
||||
heartbeat_led: heartbeat_led {
|
||||
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; /* LD2 user LED */
|
||||
label = "Heartbeat LED";
|
||||
};
|
||||
|
||||
piezo_out: piezo_out {
|
||||
gpios = <&gpiob 5 GPIO_ACTIVE_HIGH>; /* TODO verify */
|
||||
label = "Piezo";
|
||||
};
|
||||
|
||||
ir_tx_out: ir_tx_out {
|
||||
gpios = <&gpiob 4 GPIO_ACTIVE_HIGH>; /* TODO verify */
|
||||
label = "IR TX";
|
||||
};
|
||||
|
||||
ir_rx_in: ir_rx_in {
|
||||
gpios = <&gpiob 3 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; /* TODO */
|
||||
label = "IR RX";
|
||||
};
|
||||
|
||||
buttons: buttons {
|
||||
compatible = "gpio-keys";
|
||||
btn0: button_0 {
|
||||
gpios = <&gpioa 8 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "NewGame";
|
||||
};
|
||||
btn1: button_1 {
|
||||
gpios = <&gpioa 9 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "Explode";
|
||||
};
|
||||
btn2: button_2 {
|
||||
gpios = <&gpioa 10 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "TestShot";
|
||||
};
|
||||
btn3: button_3 {
|
||||
gpios = <&gpiob 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "SensorTest";
|
||||
};
|
||||
};
|
||||
};
|
||||
143
zephyr_app/boards/rpi_pico.overlay
Normal file
143
zephyr_app/boards/rpi_pico.overlay
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Raspberry Pi Pico (RP2040) overlay for the OpenLaserTag remote control.
|
||||
*
|
||||
* Pin assignments are ported directly from the MicroPython firmware:
|
||||
* - buttons GPIO18/19/20/21 (main.py:371, input pull-up, active-low)
|
||||
* - IR RX GPIO16 (main.py:200, TSOP4856, pull-up, active-low)
|
||||
* - IR TX GPIO17 (main.py:223, gates IR LED via PN2222A)
|
||||
* - piezo GPIO15 (main.py:44, NPN gate, active-high)
|
||||
* - RGB R/G/B GPIO22/26/27 (lib/rgb.py:17-19, PWM, active-high)
|
||||
* - battery GPIO28 / ADC2 (main.py:67, 2xAA via divider)
|
||||
*
|
||||
* BLE: the plain `rpi_pico` board has no radio. Use the Pico W
|
||||
* (`rpi_pico/rp2040/w`) for the Bluetooth feature. See migration-notes.md.
|
||||
*/
|
||||
|
||||
#include <zephyr/dt-bindings/gpio/gpio.h>
|
||||
#include <zephyr/dt-bindings/pwm/pwm.h>
|
||||
#include <zephyr/dt-bindings/adc/adc.h>
|
||||
#include <dt-bindings/pinctrl/rpi-pico-rp2040-pinctrl.h>
|
||||
|
||||
/ {
|
||||
aliases {
|
||||
/* Heartbeat LED. On the plain Pico this is GPIO25; on the Pico W
|
||||
* the onboard LED is behind the CYW43 chip, so we keep a GPIO
|
||||
* alias for boards that expose one and fall back gracefully. */
|
||||
heartbeat-led = &heartbeat_led;
|
||||
piezo = &piezo_out;
|
||||
ir-tx = &ir_tx_out;
|
||||
ir-rx = &ir_rx_in;
|
||||
rgb-red = &rgb_red;
|
||||
rgb-green = &rgb_green;
|
||||
rgb-blue = &rgb_blue;
|
||||
};
|
||||
|
||||
/*
|
||||
* The following four pins are driven directly with the GPIO API, so they
|
||||
* are bare nodes exposing a `gpios` property (no led/key driver binding).
|
||||
*/
|
||||
heartbeat_led: heartbeat_led {
|
||||
gpios = <&gpio0 25 GPIO_ACTIVE_HIGH>;
|
||||
label = "Heartbeat LED";
|
||||
};
|
||||
|
||||
/* Piezo buzzer: active-high GPIO toggled for beeps. */
|
||||
piezo_out: piezo_out {
|
||||
gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>;
|
||||
label = "Piezo";
|
||||
};
|
||||
|
||||
/*
|
||||
* IR transmitter LED gate. The carrier (56 kHz) is generated in
|
||||
* software by bit-banging this GPIO during marks (see src/ir_tx.c).
|
||||
* This keeps the port board-portable; the recommended hardware upgrade
|
||||
* is RP2040 PIO / ESP32 RMT (documented in migration-notes.md).
|
||||
*/
|
||||
ir_tx_out: ir_tx_out {
|
||||
gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>;
|
||||
label = "IR TX";
|
||||
};
|
||||
|
||||
/* IR receiver (demodulated output of TSOP4856), active-low, pull-up. */
|
||||
ir_rx_in: ir_rx_in {
|
||||
gpios = <&gpio0 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "IR RX";
|
||||
};
|
||||
|
||||
/* Four game buttons, active-low with pull-ups. Index order matches
|
||||
* BUTTON_CMD_MAP in app_config.h (GPIO18,21,20,19). */
|
||||
buttons: buttons {
|
||||
compatible = "gpio-keys";
|
||||
btn0: button_0 {
|
||||
gpios = <&gpio0 18 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "NewGame";
|
||||
};
|
||||
btn1: button_1 {
|
||||
gpios = <&gpio0 21 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "Explode";
|
||||
};
|
||||
btn2: button_2 {
|
||||
gpios = <&gpio0 20 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "TestShot";
|
||||
};
|
||||
btn3: button_3 {
|
||||
gpios = <&gpio0 19 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||
label = "SensorTest";
|
||||
};
|
||||
};
|
||||
|
||||
/* RGB LED driven by hardware PWM at 100 Hz (lib/rgb.py). */
|
||||
rgb_leds: pwmleds {
|
||||
compatible = "pwm-leds";
|
||||
rgb_red: rgb_red {
|
||||
/* GPIO22 = PWM slice3 chan A -> channel 6 */
|
||||
pwms = <&pwm 6 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Red";
|
||||
};
|
||||
rgb_green: rgb_green {
|
||||
/* GPIO26 = PWM slice5 chan A -> channel 10 */
|
||||
pwms = <&pwm 10 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Green";
|
||||
};
|
||||
rgb_blue: rgb_blue {
|
||||
/* GPIO27 = PWM slice5 chan B -> channel 11 */
|
||||
pwms = <&pwm 11 PWM_HZ(100) PWM_POLARITY_NORMAL>;
|
||||
label = "RGB Blue";
|
||||
};
|
||||
};
|
||||
|
||||
/* Battery sense channel for ADC_DT_SPEC_GET (src/battery.c). */
|
||||
zephyr_user: zephyr_user {
|
||||
io-channels = <&adc 2>;
|
||||
};
|
||||
};
|
||||
|
||||
/* PWM pin routing for the RGB LED outputs (GPIO22/26/27). */
|
||||
&pinctrl {
|
||||
pwm_rgb_default: pwm_rgb_default {
|
||||
group1 {
|
||||
pinmux = <PWM_3A_P22>, <PWM_5A_P26>, <PWM_5B_P27>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
&pwm {
|
||||
status = "okay";
|
||||
pinctrl-0 = <&pwm_rgb_default>;
|
||||
pinctrl-names = "default";
|
||||
divider-int-0 = <255>;
|
||||
};
|
||||
|
||||
&adc {
|
||||
status = "okay";
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@2 {
|
||||
reg = <2>;
|
||||
zephyr,gain = "ADC_GAIN_1";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,resolution = <12>;
|
||||
};
|
||||
};
|
||||
16
zephyr_app/overlay-ble.conf
Normal file
16
zephyr_app/overlay-ble.conf
Normal file
@ -0,0 +1,16 @@
|
||||
# Extra config fragment to enable the BLE Nordic UART service.
|
||||
# Use on radio-capable boards only (Pico W, ESP32):
|
||||
# west build -b rpi_pico/rp2040/w zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
# west build -b esp32_devkitc_wroom/esp32/procpu zephyr_app -- -DEXTRA_CONF_FILE=overlay-ble.conf
|
||||
|
||||
CONFIG_APP_BLE=y
|
||||
|
||||
CONFIG_BT=y
|
||||
CONFIG_BT_PERIPHERAL=y
|
||||
CONFIG_BT_DEVICE_NAME="OpenLaserTag"
|
||||
CONFIG_BT_DEVICE_APPEARANCE=128
|
||||
CONFIG_BT_GATT_DYNAMIC_DB=y
|
||||
|
||||
# Nordic UART payloads can reach the ATT MTU; give the RX buffer room.
|
||||
CONFIG_BT_BUF_ACL_RX_SIZE=27
|
||||
CONFIG_BT_L2CAP_TX_MTU=23
|
||||
27
zephyr_app/prj.conf
Normal file
27
zephyr_app/prj.conf
Normal file
@ -0,0 +1,27 @@
|
||||
# OpenLaserTag remote control - base application config.
|
||||
# Peripheral pin mapping lives in the per-board overlays under boards/.
|
||||
# BLE is gated behind CONFIG_APP_BLE (see Kconfig + overlay-ble.conf) because
|
||||
# the plain rpi_pico board has no radio.
|
||||
|
||||
# Core drivers used on every board
|
||||
CONFIG_GPIO=y
|
||||
CONFIG_PWM=y
|
||||
CONFIG_ADC=y
|
||||
|
||||
# Concurrency primitives (k_timer / workqueue / events replace uasyncio)
|
||||
CONFIG_EVENTS=y
|
||||
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
|
||||
|
||||
# Logging (replaces lib/logging.py)
|
||||
CONFIG_LOG=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL=3
|
||||
CONFIG_PRINTK=y
|
||||
|
||||
# Stacks: main thread plus the dedicated IR TX thread need headroom.
|
||||
CONFIG_MAIN_STACK_SIZE=4096
|
||||
|
||||
# Cycle counter is used for microsecond IR edge timestamps.
|
||||
CONFIG_TIMING_FUNCTIONS=y
|
||||
|
||||
# Hardware unique id -> device id string (replaces machine.unique_id()).
|
||||
CONFIG_HWINFO=y
|
||||
91
zephyr_app/src/app_config.h
Normal file
91
zephyr_app/src/app_config.h
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* app_config.h - Central tunables for the OpenLaserTag Zephyr firmware.
|
||||
*
|
||||
* These values are ported 1:1 from the original MicroPython implementation
|
||||
* (main.py, lib/rgb.py, lib/olt_lib/, lib/primitives/pushbutton.py).
|
||||
* Keep hardware-independent constants here; pin mapping lives in the board
|
||||
* devicetree overlays.
|
||||
*/
|
||||
#ifndef APP_CONFIG_H
|
||||
#define APP_CONFIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- OLT IR protocol ------------------------------------------------- */
|
||||
|
||||
/* OLT carrier: "My decoder chip is 56KHz" (main.py:358). */
|
||||
#define OLT_CARRIER_HZ 56000U
|
||||
/* IR LED carrier duty while a mark is active (lib/olt_lib/ir_tx/olt.py:20). */
|
||||
#define OLT_CARRIER_DUTY_PCT 40U
|
||||
|
||||
/* SIRC-style mark/space timing in microseconds (ir_tx/olt.py). */
|
||||
#define OLT_LEAD_MARK_US 2400U
|
||||
#define OLT_LEAD_SPACE_US 600U
|
||||
#define OLT_BIT_ONE_MARK_US 1200U
|
||||
#define OLT_BIT_ZERO_MARK_US 600U
|
||||
#define OLT_BIT_SPACE_US 600U
|
||||
|
||||
#define OLT_BITS 24U
|
||||
|
||||
/* bit23 set => command packet, clear => shot packet (main.py:177). */
|
||||
#define OLT_COMMAND_MASK 0x800000U
|
||||
|
||||
/* ---- Button behaviour (lib/primitives/pushbutton.py) ----------------- */
|
||||
|
||||
#define BTN_DEBOUNCE_MS 50U
|
||||
#define BTN_LONG_MS 1000U
|
||||
#define BTN_DOUBLE_MS 400U
|
||||
|
||||
/* ---- Battery monitor (main.py:74-108) -------------------------------- */
|
||||
|
||||
#define BATTERY_PERIOD_MS 10000U
|
||||
/* ADC full-scale reference, volts. Pico VSYS sense is on a divider. */
|
||||
#define BATTERY_VREF_MV 3300
|
||||
/*
|
||||
* The MicroPython code computes capacity from voltage/2 (main.py:87),
|
||||
* i.e. it assumes a 1:2 divider so the measured rail maps to one alkaline
|
||||
* cell. TODO: confirm the real divider ratio on the PCB (see migration-notes).
|
||||
*/
|
||||
#define BATTERY_DIVIDER_NUM 1
|
||||
#define BATTERY_DIVIDER_DEN 2
|
||||
|
||||
/* ---- Heartbeat (main.py:alive_check) --------------------------------- */
|
||||
|
||||
#define HEARTBEAT_ON_MS 100U
|
||||
#define HEARTBEAT_OFF_MS 900U
|
||||
|
||||
/* ---- Piezo (main.py:piezo_beep / piezo_short_beep) ------------------- */
|
||||
|
||||
#define PIEZO_BEEP_MS 300U
|
||||
#define PIEZO_SHORT_BEEP_MS 100U
|
||||
|
||||
/* ---- OLT command table (main.py:228-236) ----------------------------- */
|
||||
|
||||
#define OLT_COMMAND_END 0xE8U
|
||||
|
||||
enum olt_command_id {
|
||||
OLT_CMD_NEW_GAME = 0, /* 0x83 0x05 0xE8 */
|
||||
OLT_CMD_EXPLODE, /* 0x83 0x0B 0xE8 */
|
||||
OLT_CMD_TEST_SHOT, /* 0x08 0x24 0x00 (ID8, RED, dmg 25) */
|
||||
OLT_CMD_SENSOR_TEST, /* 0x83 0x15 0xE8 */
|
||||
OLT_CMD_COUNT,
|
||||
};
|
||||
|
||||
struct olt_command {
|
||||
const char *name;
|
||||
uint8_t tx1;
|
||||
uint8_t tx2;
|
||||
uint8_t tx3;
|
||||
};
|
||||
|
||||
extern const struct olt_command olt_commands[OLT_CMD_COUNT];
|
||||
|
||||
/*
|
||||
* Button -> command mapping, preserving main.py:371
|
||||
* PINS = ((COMMANDS[0],18),(COMMANDS[1],21),(COMMANDS[2],20),(COMMANDS[3],19))
|
||||
* The buttons node in the overlay lists the four buttons in this same order;
|
||||
* index N of that node maps to olt_commands[BUTTON_CMD_MAP[N]].
|
||||
* The array itself is defined in main.c (its only user).
|
||||
*/
|
||||
|
||||
#endif /* APP_CONFIG_H */
|
||||
124
zephyr_app/src/battery.c
Normal file
124
zephyr_app/src/battery.c
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* battery.c - Battery monitor, port of main.py:battery_monitor.
|
||||
*
|
||||
* A k_timer fires every BATTERY_PERIOD_MS and submits a work item that samples
|
||||
* the ADC, converts to millivolts, derives the alkaline-cell capacity, logs it
|
||||
* and forwards a telemetry line to the registered sink (BLE in the app).
|
||||
*
|
||||
* Integer math is used throughout to avoid a floating-point printf dependency.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "battery.h"
|
||||
|
||||
LOG_MODULE_REGISTER(battery, LOG_LEVEL_INF);
|
||||
|
||||
#define ZEPHYR_USER_NODE DT_PATH(zephyr_user)
|
||||
|
||||
#if DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, io_channels)
|
||||
#define BATTERY_PRESENT 1
|
||||
static const struct adc_dt_spec adc_batt =
|
||||
ADC_DT_SPEC_GET_BY_IDX(ZEPHYR_USER_NODE, 0);
|
||||
#else
|
||||
#define BATTERY_PRESENT 0
|
||||
#warning "No battery io-channel configured in overlay"
|
||||
#endif
|
||||
|
||||
static battery_report_t report_cb;
|
||||
|
||||
int battery_capacity_pct(int mv)
|
||||
{
|
||||
/* main.py:voltage_to_capacity, in integer millivolts (one cell). */
|
||||
if (mv >= 1600) {
|
||||
return 100;
|
||||
} else if (mv >= 1500) {
|
||||
return 90 + (mv - 1500) / 10;
|
||||
} else if (mv >= 1400) {
|
||||
return 70 + (mv - 1400) / 5;
|
||||
} else if (mv >= 1300) {
|
||||
return 40 + (mv - 1300) * 3 / 10;
|
||||
} else if (mv >= 1200) {
|
||||
return 10 + (mv - 1200) * 3 / 10;
|
||||
} else if (mv >= 1000) {
|
||||
return (mv - 1000) / 20;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if BATTERY_PRESENT
|
||||
static void battery_sample(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
int16_t raw = 0;
|
||||
struct adc_sequence seq = {
|
||||
.buffer = &raw,
|
||||
.buffer_size = sizeof(raw),
|
||||
};
|
||||
(void)adc_sequence_init_dt(&adc_batt, &seq);
|
||||
|
||||
int ret = adc_read_dt(&adc_batt, &seq);
|
||||
if (ret) {
|
||||
LOG_WRN("adc_read failed: %d", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t mv = raw;
|
||||
ret = adc_raw_to_millivolts_dt(&adc_batt, &mv);
|
||||
if (ret) {
|
||||
/* Fall back to the configured reference if calibration data is
|
||||
* unavailable for this board. */
|
||||
mv = (int32_t)raw * BATTERY_VREF_MV / 4095;
|
||||
}
|
||||
|
||||
int cell_mv = mv * BATTERY_DIVIDER_NUM / BATTERY_DIVIDER_DEN;
|
||||
int pct = battery_capacity_pct(cell_mv);
|
||||
|
||||
char line[64];
|
||||
snprintf(line, sizeof(line), "BATTERY: Voltage: %d.%02dV, Capacity: %d%%\n",
|
||||
mv / 1000, (mv % 1000) / 10, pct);
|
||||
|
||||
LOG_INF("Battery %d.%02dV cell %d.%02dV cap %d%%",
|
||||
mv / 1000, (mv % 1000) / 10,
|
||||
cell_mv / 1000, (cell_mv % 1000) / 10, pct);
|
||||
|
||||
if (report_cb) {
|
||||
report_cb(line);
|
||||
}
|
||||
}
|
||||
static K_WORK_DEFINE(battery_work, battery_sample);
|
||||
|
||||
static void battery_timer_cb(struct k_timer *t)
|
||||
{
|
||||
ARG_UNUSED(t);
|
||||
k_work_submit(&battery_work);
|
||||
}
|
||||
static K_TIMER_DEFINE(battery_timer, battery_timer_cb, NULL);
|
||||
#endif /* BATTERY_PRESENT */
|
||||
|
||||
int battery_init(battery_report_t report)
|
||||
{
|
||||
report_cb = report;
|
||||
#if BATTERY_PRESENT
|
||||
if (!adc_is_ready_dt(&adc_batt)) {
|
||||
LOG_ERR("battery ADC not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = adc_channel_setup_dt(&adc_batt);
|
||||
if (ret) {
|
||||
LOG_ERR("ADC channel setup failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
/* First sample soon, then every period (main.py uses 10 s). */
|
||||
k_timer_start(&battery_timer, K_SECONDS(2), K_MSEC(BATTERY_PERIOD_MS));
|
||||
LOG_INF("battery monitor started");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WRN("battery monitor not configured");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
17
zephyr_app/src/battery.h
Normal file
17
zephyr_app/src/battery.h
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* battery.h - Periodic battery voltage / capacity monitor.
|
||||
* Ported from main.py:battery_monitor + voltage_to_capacity (ADC2 / GPIO28).
|
||||
*/
|
||||
#ifndef BATTERY_H
|
||||
#define BATTERY_H
|
||||
|
||||
/* Sink for a formatted telemetry line (e.g. BLE notify). May be NULL. */
|
||||
typedef void (*battery_report_t)(const char *line);
|
||||
|
||||
/* Configure the ADC channel and start the periodic monitor timer. */
|
||||
int battery_init(battery_report_t report);
|
||||
|
||||
/* Convert one alkaline cell's millivolts to a 0-100 capacity percentage. */
|
||||
int battery_capacity_pct(int cell_mv);
|
||||
|
||||
#endif /* BATTERY_H */
|
||||
140
zephyr_app/src/ble_nus.c
Normal file
140
zephyr_app/src/ble_nus.c
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* ble_nus.c - Nordic UART Service, port of lib/ble_uart.
|
||||
*
|
||||
* Implements the same GATT layout the MicroPython firmware advertised:
|
||||
* service 6E400001-..., TX notify 6E400003-..., RX write 6E400002-...
|
||||
* RX writes are forwarded (NUL-terminated) to the registered handler; TX
|
||||
* notifications carry telemetry text. Compiled only when CONFIG_APP_BLE=y.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/bluetooth/gatt.h>
|
||||
#include <zephyr/bluetooth/uuid.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ble_nus.h"
|
||||
|
||||
LOG_MODULE_REGISTER(ble_nus, LOG_LEVEL_INF);
|
||||
|
||||
/* Nordic UART UUIDs (lib/ble_uart/__init__.py:13-21). */
|
||||
#define NUS_SVC_UUID BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)
|
||||
#define NUS_RX_UUID BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)
|
||||
#define NUS_TX_UUID BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)
|
||||
|
||||
static struct bt_uuid_128 nus_svc_uuid = BT_UUID_INIT_128(NUS_SVC_UUID);
|
||||
static struct bt_uuid_128 nus_rx_uuid = BT_UUID_INIT_128(NUS_RX_UUID);
|
||||
static struct bt_uuid_128 nus_tx_uuid = BT_UUID_INIT_128(NUS_TX_UUID);
|
||||
|
||||
#define RX_BUF_SIZE 100 /* matches ble_uart rxbuf default */
|
||||
|
||||
static ble_rx_cb_t user_cb;
|
||||
static bool notify_enabled;
|
||||
static char dev_name[32];
|
||||
|
||||
static ssize_t on_rx(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
const void *buf, uint16_t len, uint16_t offset,
|
||||
uint8_t flags)
|
||||
{
|
||||
ARG_UNUSED(conn);
|
||||
ARG_UNUSED(attr);
|
||||
ARG_UNUSED(offset);
|
||||
ARG_UNUSED(flags);
|
||||
|
||||
static char rx[RX_BUF_SIZE + 1];
|
||||
uint16_t n = len < RX_BUF_SIZE ? len : RX_BUF_SIZE;
|
||||
|
||||
memcpy(rx, buf, n);
|
||||
rx[n] = '\0';
|
||||
|
||||
LOG_DBG("BLE RX: %s", rx);
|
||||
if (user_cb) {
|
||||
user_cb(rx);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void tx_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
||||
{
|
||||
ARG_UNUSED(attr);
|
||||
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
|
||||
}
|
||||
|
||||
/* attrs: [0]=svc [1]=TX chrc [2]=TX value [3]=CCC [4]=RX chrc [5]=RX value */
|
||||
BT_GATT_SERVICE_DEFINE(nus_svc,
|
||||
BT_GATT_PRIMARY_SERVICE(&nus_svc_uuid),
|
||||
BT_GATT_CHARACTERISTIC(&nus_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
|
||||
BT_GATT_PERM_NONE, NULL, NULL, NULL),
|
||||
BT_GATT_CCC(tx_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
|
||||
BT_GATT_CHARACTERISTIC(&nus_rx_uuid.uuid,
|
||||
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
|
||||
BT_GATT_PERM_WRITE, NULL, on_rx, NULL),
|
||||
);
|
||||
|
||||
void ble_nus_write(const char *data)
|
||||
{
|
||||
if (!notify_enabled) {
|
||||
return;
|
||||
}
|
||||
/* attrs[2] is the TX characteristic value; NULL conn => all subscribers. */
|
||||
(void)bt_gatt_notify(NULL, &nus_svc.attrs[2], data, strlen(data));
|
||||
}
|
||||
|
||||
static struct bt_data ad[2];
|
||||
static uint8_t ad_flags = BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR;
|
||||
|
||||
static int advertise(void)
|
||||
{
|
||||
ad[0].type = BT_DATA_FLAGS;
|
||||
ad[0].data_len = sizeof(ad_flags);
|
||||
ad[0].data = &ad_flags;
|
||||
ad[1].type = BT_DATA_NAME_COMPLETE;
|
||||
ad[1].data = (const uint8_t *)dev_name;
|
||||
ad[1].data_len = strlen(dev_name);
|
||||
|
||||
int err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), NULL, 0);
|
||||
if (err) {
|
||||
LOG_ERR("advertising failed: %d", err);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static void connected(struct bt_conn *conn, uint8_t err)
|
||||
{
|
||||
ARG_UNUSED(conn);
|
||||
if (err) {
|
||||
LOG_WRN("connection failed: %d", err);
|
||||
return;
|
||||
}
|
||||
LOG_INF("BLE connected");
|
||||
}
|
||||
|
||||
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
||||
{
|
||||
ARG_UNUSED(conn);
|
||||
LOG_INF("BLE disconnected (0x%02x), re-advertising", reason);
|
||||
advertise();
|
||||
}
|
||||
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
};
|
||||
|
||||
int ble_nus_init(const char *name, ble_rx_cb_t cb)
|
||||
{
|
||||
user_cb = cb;
|
||||
strncpy(dev_name, name, sizeof(dev_name) - 1);
|
||||
dev_name[sizeof(dev_name) - 1] = '\0';
|
||||
|
||||
int err = bt_enable(NULL);
|
||||
if (err) {
|
||||
LOG_ERR("bt_enable failed: %d", err);
|
||||
return err;
|
||||
}
|
||||
(void)bt_set_name(dev_name);
|
||||
|
||||
LOG_INF("BLE NUS init as \"%s\"", dev_name);
|
||||
return advertise();
|
||||
}
|
||||
20
zephyr_app/src/ble_nus.h
Normal file
20
zephyr_app/src/ble_nus.h
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* ble_nus.h - Nordic UART Service (BLE) for command/telemetry.
|
||||
* Ported from lib/ble_uart. Compiled only when CONFIG_APP_BLE=y (radio-capable
|
||||
* boards such as the Pico W or ESP32).
|
||||
*/
|
||||
#ifndef BLE_NUS_H
|
||||
#define BLE_NUS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Called from the BLE RX characteristic write, with a NUL-terminated string. */
|
||||
typedef void (*ble_rx_cb_t)(const char *msg);
|
||||
|
||||
/* Start advertising as "<name>" and register the RX handler. */
|
||||
int ble_nus_init(const char *name, ble_rx_cb_t cb);
|
||||
|
||||
/* Notify connected central(s) with telemetry text (no-op if not connected). */
|
||||
void ble_nus_write(const char *data);
|
||||
|
||||
#endif /* BLE_NUS_H */
|
||||
89
zephyr_app/src/board_io.c
Normal file
89
zephyr_app/src/board_io.c
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* board_io.c - Heartbeat LED and device id.
|
||||
* Heartbeat replaces main.py:alive_check; device id replaces the
|
||||
* machine.unique_id() handling in main.py:35-41.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "board_io.h"
|
||||
|
||||
LOG_MODULE_REGISTER(board_io, LOG_LEVEL_INF);
|
||||
|
||||
#define HEARTBEAT_NODE DT_ALIAS(heartbeat_led)
|
||||
|
||||
static char device_id[5] = "0000";
|
||||
|
||||
#if DT_NODE_HAS_STATUS(HEARTBEAT_NODE, okay)
|
||||
#define HEARTBEAT_PRESENT 1
|
||||
static const struct gpio_dt_spec heartbeat =
|
||||
GPIO_DT_SPEC_GET(HEARTBEAT_NODE, gpios);
|
||||
|
||||
static void hb_off_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
(void)gpio_pin_set_dt(&heartbeat, 0);
|
||||
}
|
||||
static K_WORK_DELAYABLE_DEFINE(hb_off, hb_off_fn);
|
||||
|
||||
static void hb_timer_fn(struct k_timer *t)
|
||||
{
|
||||
ARG_UNUSED(t);
|
||||
(void)gpio_pin_set_dt(&heartbeat, 1);
|
||||
k_work_reschedule(&hb_off, K_MSEC(HEARTBEAT_ON_MS));
|
||||
}
|
||||
static K_TIMER_DEFINE(hb_timer, hb_timer_fn, NULL);
|
||||
#endif
|
||||
|
||||
static void resolve_device_id(void)
|
||||
{
|
||||
uint8_t id[16];
|
||||
ssize_t len = hwinfo_get_device_id(id, sizeof(id));
|
||||
|
||||
if (len >= 2) {
|
||||
/* Last two bytes -> last 4 hex chars, as in main.py. */
|
||||
snprintf(device_id, sizeof(device_id), "%02X%02X",
|
||||
id[len - 2], id[len - 1]);
|
||||
} else {
|
||||
LOG_WRN("hwinfo device id unavailable (%d)", (int)len);
|
||||
}
|
||||
}
|
||||
|
||||
const char *board_device_id_str(void)
|
||||
{
|
||||
return device_id;
|
||||
}
|
||||
|
||||
int board_io_init(void)
|
||||
{
|
||||
resolve_device_id();
|
||||
LOG_INF("Device ID: %s", device_id);
|
||||
|
||||
#if HEARTBEAT_PRESENT
|
||||
if (!gpio_is_ready_dt(&heartbeat)) {
|
||||
LOG_ERR("heartbeat LED not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&heartbeat, GPIO_OUTPUT_INACTIVE);
|
||||
if (ret) {
|
||||
LOG_ERR("heartbeat configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
LOG_WRN("heartbeat LED not configured");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
void board_io_start_heartbeat(void)
|
||||
{
|
||||
#if HEARTBEAT_PRESENT
|
||||
k_timer_start(&hb_timer, K_NO_WAIT,
|
||||
K_MSEC(HEARTBEAT_ON_MS + HEARTBEAT_OFF_MS));
|
||||
#endif
|
||||
}
|
||||
15
zephyr_app/src/board_io.h
Normal file
15
zephyr_app/src/board_io.h
Normal file
@ -0,0 +1,15 @@
|
||||
#ifndef BOARD_IO_H
|
||||
#define BOARD_IO_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Configure the heartbeat LED and resolve the device id. */
|
||||
int board_io_init(void);
|
||||
|
||||
/* Start the heartbeat blink (100 ms on / 900 ms off), port of alive_check. */
|
||||
void board_io_start_heartbeat(void);
|
||||
|
||||
/* 4-hex-char device id derived from the hardware unique id (main.py:35-41). */
|
||||
const char *board_device_id_str(void);
|
||||
|
||||
#endif /* BOARD_IO_H */
|
||||
140
zephyr_app/src/buttons.c
Normal file
140
zephyr_app/src/buttons.c
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* buttons.c - Debounced buttons replacing lib/primitives/pushbutton.py.
|
||||
*
|
||||
* Each GPIO edge (re)arms a debounce work item. When the level is stable the
|
||||
* logical state is compared against the previous one to emit PRESS/RELEASE,
|
||||
* and long-press / double-click timers run exactly as the MicroPython
|
||||
* Pushbutton did.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "buttons.h"
|
||||
|
||||
LOG_MODULE_REGISTER(buttons, LOG_LEVEL_INF);
|
||||
|
||||
#define BUTTONS_NODE DT_NODELABEL(buttons)
|
||||
|
||||
struct button {
|
||||
struct gpio_dt_spec spec;
|
||||
struct gpio_callback gpio_cb;
|
||||
struct k_work_delayable debounce;
|
||||
struct k_work_delayable long_work;
|
||||
struct k_work_delayable double_work;
|
||||
int index;
|
||||
bool pressed; /* last debounced logical state */
|
||||
bool waiting_double; /* a single press is pending double-click timeout */
|
||||
};
|
||||
|
||||
#define BTN_ENTRY(node_id) { .spec = GPIO_DT_SPEC_GET(node_id, gpios) },
|
||||
|
||||
static struct button buttons[] = {
|
||||
#if DT_NODE_HAS_STATUS(BUTTONS_NODE, okay)
|
||||
DT_FOREACH_CHILD(BUTTONS_NODE, BTN_ENTRY)
|
||||
#endif
|
||||
};
|
||||
|
||||
static btn_cb_t user_cb;
|
||||
|
||||
int buttons_count(void)
|
||||
{
|
||||
return ARRAY_SIZE(buttons);
|
||||
}
|
||||
|
||||
static void emit(struct button *b, enum btn_event ev)
|
||||
{
|
||||
if (user_cb) {
|
||||
user_cb(b->index, ev);
|
||||
}
|
||||
}
|
||||
|
||||
static void debounce_work(struct k_work *work)
|
||||
{
|
||||
struct k_work_delayable *dw = k_work_delayable_from_work(work);
|
||||
struct button *b = CONTAINER_OF(dw, struct button, debounce);
|
||||
|
||||
bool now = gpio_pin_get_dt(&b->spec) > 0; /* logical: 1 == pressed */
|
||||
if (now == b->pressed) {
|
||||
return; /* bounce settled back to the same state */
|
||||
}
|
||||
b->pressed = now;
|
||||
|
||||
if (now) {
|
||||
emit(b, BTN_PRESS);
|
||||
k_work_reschedule(&b->long_work, K_MSEC(BTN_LONG_MS));
|
||||
if (b->waiting_double) {
|
||||
b->waiting_double = false;
|
||||
k_work_cancel_delayable(&b->double_work);
|
||||
emit(b, BTN_DOUBLE);
|
||||
} else {
|
||||
b->waiting_double = true;
|
||||
k_work_reschedule(&b->double_work, K_MSEC(BTN_DOUBLE_MS));
|
||||
}
|
||||
} else {
|
||||
k_work_cancel_delayable(&b->long_work);
|
||||
emit(b, BTN_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
static void long_work(struct k_work *work)
|
||||
{
|
||||
struct k_work_delayable *dw = k_work_delayable_from_work(work);
|
||||
struct button *b = CONTAINER_OF(dw, struct button, long_work);
|
||||
|
||||
if (b->pressed) {
|
||||
emit(b, BTN_LONG);
|
||||
}
|
||||
}
|
||||
|
||||
static void double_work(struct k_work *work)
|
||||
{
|
||||
struct k_work_delayable *dw = k_work_delayable_from_work(work);
|
||||
struct button *b = CONTAINER_OF(dw, struct button, double_work);
|
||||
|
||||
b->waiting_double = false; /* timed out: it was a single click */
|
||||
}
|
||||
|
||||
static void gpio_isr(const struct device *port, struct gpio_callback *cb,
|
||||
uint32_t pins)
|
||||
{
|
||||
ARG_UNUSED(port);
|
||||
ARG_UNUSED(pins);
|
||||
struct button *b = CONTAINER_OF(cb, struct button, gpio_cb);
|
||||
k_work_reschedule(&b->debounce, K_MSEC(BTN_DEBOUNCE_MS));
|
||||
}
|
||||
|
||||
int buttons_init(btn_cb_t cb)
|
||||
{
|
||||
user_cb = cb;
|
||||
|
||||
for (int i = 0; i < (int)ARRAY_SIZE(buttons); i++) {
|
||||
struct button *b = &buttons[i];
|
||||
b->index = i;
|
||||
|
||||
if (!gpio_is_ready_dt(&b->spec)) {
|
||||
LOG_ERR("button %d GPIO not ready", i);
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&b->spec, GPIO_INPUT);
|
||||
if (ret) {
|
||||
LOG_ERR("button %d configure failed: %d", i, ret);
|
||||
return ret;
|
||||
}
|
||||
ret = gpio_pin_interrupt_configure_dt(&b->spec,
|
||||
GPIO_INT_EDGE_BOTH);
|
||||
if (ret) {
|
||||
LOG_ERR("button %d irq config failed: %d", i, ret);
|
||||
return ret;
|
||||
}
|
||||
k_work_init_delayable(&b->debounce, debounce_work);
|
||||
k_work_init_delayable(&b->long_work, long_work);
|
||||
k_work_init_delayable(&b->double_work, double_work);
|
||||
gpio_init_callback(&b->gpio_cb, gpio_isr, BIT(b->spec.pin));
|
||||
gpio_add_callback(b->spec.port, &b->gpio_cb);
|
||||
}
|
||||
|
||||
LOG_INF("%d buttons initialized", (int)ARRAY_SIZE(buttons));
|
||||
return 0;
|
||||
}
|
||||
26
zephyr_app/src/buttons.h
Normal file
26
zephyr_app/src/buttons.h
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* buttons.h - Debounced game buttons with long/double detection.
|
||||
* Ported from lib/primitives/pushbutton.py (debounce 50 ms, long 1000 ms,
|
||||
* double 400 ms) and the button wiring in main.py:369-378.
|
||||
*/
|
||||
#ifndef BUTTONS_H
|
||||
#define BUTTONS_H
|
||||
|
||||
enum btn_event {
|
||||
BTN_PRESS = 0,
|
||||
BTN_RELEASE,
|
||||
BTN_LONG,
|
||||
BTN_DOUBLE,
|
||||
};
|
||||
|
||||
/* Callback invoked from the system workqueue (not ISR context). `index` is the
|
||||
* button's position in the devicetree `buttons` node (0..N-1). */
|
||||
typedef void (*btn_cb_t)(int index, enum btn_event event);
|
||||
|
||||
/* Configure all buttons and register the event callback. */
|
||||
int buttons_init(btn_cb_t cb);
|
||||
|
||||
/* Number of configured buttons. */
|
||||
int buttons_count(void);
|
||||
|
||||
#endif /* BUTTONS_H */
|
||||
124
zephyr_app/src/ir_rx.c
Normal file
124
zephyr_app/src/ir_rx.c
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* ir_rx.c - OpenLaserTag IR receiver.
|
||||
*
|
||||
* Mirrors lib/olt_lib/ir_rx/__init__.py: a GPIO interrupt timestamps every
|
||||
* edge into a buffer; the first edge arms a one-shot timer whose expiry marks
|
||||
* the end of the frame. Decoding (olt_decode) then runs in a work item, off
|
||||
* the interrupt path, exactly like the MicroPython asyncio callback.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "olt_proto.h"
|
||||
#include "ir_rx.h"
|
||||
|
||||
LOG_MODULE_REGISTER(ir_rx, LOG_LEVEL_INF);
|
||||
|
||||
#define IR_RX_NODE DT_ALIAS(ir_rx)
|
||||
|
||||
/* 24-bit frame is <= 50 edges; leave headroom plus one for overrun detect. */
|
||||
#define IR_RX_MAX_EDGES 64
|
||||
|
||||
/*
|
||||
* Frame block timeout. ir_rx/olt.py: t = int(3 + bits*1.8) + 1 for 24 bits
|
||||
* => ~47 ms after the leading edge.
|
||||
*/
|
||||
#define IR_RX_FRAME_MS 47
|
||||
|
||||
#if DT_NODE_HAS_STATUS(IR_RX_NODE, okay)
|
||||
#define IR_RX_PRESENT 1
|
||||
|
||||
static const struct gpio_dt_spec ir_rx = GPIO_DT_SPEC_GET(IR_RX_NODE, gpios);
|
||||
static struct gpio_callback ir_rx_cb_data;
|
||||
|
||||
static uint32_t edge_times[IR_RX_MAX_EDGES];
|
||||
static volatile uint32_t edge_count;
|
||||
|
||||
static uint32_t decode_times[IR_RX_MAX_EDGES];
|
||||
static uint32_t decode_count;
|
||||
|
||||
static ir_rx_cb_t user_cb;
|
||||
|
||||
static void decode_work_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
|
||||
struct olt_frame f;
|
||||
if (olt_decode(decode_times, decode_count, &f)) {
|
||||
LOG_DBG("RX 0x%06X %s", f.packet,
|
||||
f.is_command ? "command" : "shot");
|
||||
if (user_cb) {
|
||||
user_cb(&f);
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("RX bad frame (%u edges)", decode_count);
|
||||
}
|
||||
}
|
||||
static K_WORK_DEFINE(decode_work, decode_work_fn);
|
||||
|
||||
/* One-shot timer: fires once the inter-edge gap exceeds the frame window. */
|
||||
static void frame_timer_fn(struct k_timer *t)
|
||||
{
|
||||
ARG_UNUSED(t);
|
||||
|
||||
decode_count = edge_count;
|
||||
if (decode_count > IR_RX_MAX_EDGES) {
|
||||
decode_count = IR_RX_MAX_EDGES;
|
||||
}
|
||||
memcpy(decode_times, edge_times, decode_count * sizeof(edge_times[0]));
|
||||
edge_count = 0; /* ready for the next frame */
|
||||
k_work_submit(&decode_work);
|
||||
}
|
||||
static K_TIMER_DEFINE(frame_timer, frame_timer_fn, NULL);
|
||||
|
||||
static void ir_rx_isr(const struct device *port, struct gpio_callback *cb,
|
||||
uint32_t pins)
|
||||
{
|
||||
ARG_UNUSED(port);
|
||||
ARG_UNUSED(cb);
|
||||
ARG_UNUSED(pins);
|
||||
|
||||
uint32_t now = k_cyc_to_us_floor32(k_cycle_get_32());
|
||||
|
||||
if (edge_count == 0) {
|
||||
k_timer_start(&frame_timer, K_MSEC(IR_RX_FRAME_MS), K_NO_WAIT);
|
||||
}
|
||||
if (edge_count < IR_RX_MAX_EDGES) {
|
||||
edge_times[edge_count++] = now;
|
||||
}
|
||||
}
|
||||
#endif /* IR_RX_PRESENT */
|
||||
|
||||
int ir_rx_init(ir_rx_cb_t cb)
|
||||
{
|
||||
#if IR_RX_PRESENT
|
||||
user_cb = cb;
|
||||
|
||||
if (!gpio_is_ready_dt(&ir_rx)) {
|
||||
LOG_ERR("IR RX GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&ir_rx, GPIO_INPUT);
|
||||
if (ret) {
|
||||
LOG_ERR("IR RX configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
ret = gpio_pin_interrupt_configure_dt(&ir_rx, GPIO_INT_EDGE_BOTH);
|
||||
if (ret) {
|
||||
LOG_ERR("IR RX irq config failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
gpio_init_callback(&ir_rx_cb_data, ir_rx_isr, BIT(ir_rx.pin));
|
||||
gpio_add_callback(ir_rx.port, &ir_rx_cb_data);
|
||||
|
||||
LOG_INF("IR RX initialized");
|
||||
return 0;
|
||||
#else
|
||||
ARG_UNUSED(cb);
|
||||
LOG_WRN("IR RX alias not configured");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
17
zephyr_app/src/ir_rx.h
Normal file
17
zephyr_app/src/ir_rx.h
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* ir_rx.h - OpenLaserTag IR receiver.
|
||||
* Ported from lib/olt_lib/ir_rx. A GPIO edge interrupt timestamps every edge;
|
||||
* a one-shot timeout closes the frame and a work item decodes it.
|
||||
*/
|
||||
#ifndef IR_RX_H
|
||||
#define IR_RX_H
|
||||
|
||||
#include "olt_proto.h"
|
||||
|
||||
/* Called from the system workqueue with a successfully decoded frame. */
|
||||
typedef void (*ir_rx_cb_t)(const struct olt_frame *frame);
|
||||
|
||||
/* Configure the IR RX GPIO/interrupt and register the decode callback. */
|
||||
int ir_rx_init(ir_rx_cb_t cb);
|
||||
|
||||
#endif /* IR_RX_H */
|
||||
134
zephyr_app/src/ir_tx.c
Normal file
134
zephyr_app/src/ir_tx.c
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* ir_tx.c - OpenLaserTag IR transmitter with a software carrier.
|
||||
*
|
||||
* The original firmware used RP2040 PIO / ESP32 RMT to gate a hardware 56 kHz
|
||||
* carrier (lib/olt_lib/ir_tx). For a board-portable first pass we generate the
|
||||
* carrier in software: a dedicated cooperative thread toggles the IR-TX GPIO
|
||||
* at ~56 kHz during "mark" symbols and holds it low during "space" symbols.
|
||||
*
|
||||
* Trade-off (see migration-notes.md): this monopolises a high-priority thread
|
||||
* for the duration of a frame (~30 ms) and the carrier frequency/duty are
|
||||
* approximate. The recommended upgrade is PWM-gated PIO/RMT.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "olt_proto.h"
|
||||
#include "ir_tx.h"
|
||||
|
||||
LOG_MODULE_REGISTER(ir_tx, LOG_LEVEL_INF);
|
||||
|
||||
#define IR_TX_NODE DT_ALIAS(ir_tx)
|
||||
|
||||
/*
|
||||
* Carrier approximation. Target period is 1e6/56000 = 17.86 us. With 1 us
|
||||
* busy-wait granularity we use 7 us on + 11 us off = 18 us (~55.6 kHz, ~39%
|
||||
* duty), which the 56 kHz TSOP demodulator tolerates.
|
||||
*/
|
||||
#define CARRIER_ON_US 7U
|
||||
#define CARRIER_OFF_US 11U
|
||||
#define CARRIER_PERIOD_US (CARRIER_ON_US + CARRIER_OFF_US)
|
||||
|
||||
struct ir_tx_req {
|
||||
uint8_t tx1;
|
||||
uint8_t tx2;
|
||||
uint8_t tx3;
|
||||
};
|
||||
|
||||
K_MSGQ_DEFINE(ir_tx_q, sizeof(struct ir_tx_req), 8, 4);
|
||||
|
||||
#if DT_NODE_HAS_STATUS(IR_TX_NODE, okay)
|
||||
#define IR_TX_PRESENT 1
|
||||
static const struct gpio_dt_spec ir_tx = GPIO_DT_SPEC_GET(IR_TX_NODE, gpios);
|
||||
|
||||
/* Emit carrier for `us` microseconds (gpio active = LED on). */
|
||||
static void mark(uint32_t us)
|
||||
{
|
||||
uint32_t periods = (us + CARRIER_PERIOD_US / 2) / CARRIER_PERIOD_US;
|
||||
|
||||
for (uint32_t i = 0; i < periods; i++) {
|
||||
gpio_pin_set_dt(&ir_tx, 1);
|
||||
k_busy_wait(CARRIER_ON_US);
|
||||
gpio_pin_set_dt(&ir_tx, 0);
|
||||
k_busy_wait(CARRIER_OFF_US);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hold the carrier off for `us` microseconds. */
|
||||
static void space(uint32_t us)
|
||||
{
|
||||
gpio_pin_set_dt(&ir_tx, 0);
|
||||
k_busy_wait(us);
|
||||
}
|
||||
|
||||
static void transmit(const struct ir_tx_req *req)
|
||||
{
|
||||
uint16_t sym[OLT_MAX_SYMBOLS];
|
||||
size_t n = olt_encode(req->tx1, req->tx2, req->tx3, sym);
|
||||
|
||||
/* Even indices are marks (carrier on), odd indices are spaces. */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if ((i & 1U) == 0U) {
|
||||
mark(sym[i]);
|
||||
} else {
|
||||
space(sym[i]);
|
||||
}
|
||||
}
|
||||
gpio_pin_set_dt(&ir_tx, 0);
|
||||
}
|
||||
#endif /* IR_TX_PRESENT */
|
||||
|
||||
static void ir_tx_thread(void *a, void *b, void *c)
|
||||
{
|
||||
ARG_UNUSED(a);
|
||||
ARG_UNUSED(b);
|
||||
ARG_UNUSED(c);
|
||||
|
||||
struct ir_tx_req req;
|
||||
|
||||
for (;;) {
|
||||
k_msgq_get(&ir_tx_q, &req, K_FOREVER);
|
||||
#if IR_TX_PRESENT
|
||||
LOG_DBG("TX 0x%02X%02X%02X", req.tx1, req.tx2, req.tx3);
|
||||
transmit(&req);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/* Cooperative priority so the bit-banged carrier is not preempted. */
|
||||
K_THREAD_DEFINE(ir_tx_tid, 1024, ir_tx_thread, NULL, NULL, NULL,
|
||||
K_PRIO_COOP(4), 0, 0);
|
||||
|
||||
int ir_tx_send(uint8_t tx1, uint8_t tx2, uint8_t tx3)
|
||||
{
|
||||
struct ir_tx_req req = { tx1, tx2, tx3 };
|
||||
int ret = k_msgq_put(&ir_tx_q, &req, K_NO_WAIT);
|
||||
|
||||
if (ret) {
|
||||
LOG_WRN("IR TX queue full, dropping 0x%02X%02X%02X",
|
||||
tx1, tx2, tx3);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int ir_tx_init(void)
|
||||
{
|
||||
#if IR_TX_PRESENT
|
||||
if (!gpio_is_ready_dt(&ir_tx)) {
|
||||
LOG_ERR("IR TX GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&ir_tx, GPIO_OUTPUT_INACTIVE);
|
||||
if (ret) {
|
||||
LOG_ERR("IR TX configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
LOG_INF("IR TX initialized (software 56 kHz carrier)");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WRN("IR TX alias not configured");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
18
zephyr_app/src/ir_tx.h
Normal file
18
zephyr_app/src/ir_tx.h
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* ir_tx.h - OpenLaserTag IR transmitter.
|
||||
* Ported from lib/olt_lib/ir_tx (LT_24 / IR). Queues a 24-bit command for a
|
||||
* dedicated thread that bit-bangs the 56 kHz carrier on the `ir-tx` GPIO.
|
||||
*/
|
||||
#ifndef IR_TX_H
|
||||
#define IR_TX_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Initialize the IR TX GPIO and start the transmit thread. */
|
||||
int ir_tx_init(void);
|
||||
|
||||
/* Queue a 24-bit OLT command (tx1 = most significant byte). Non-blocking;
|
||||
* safe to call from thread context. Returns 0 on success. */
|
||||
int ir_tx_send(uint8_t tx1, uint8_t tx2, uint8_t tx3);
|
||||
|
||||
#endif /* IR_TX_H */
|
||||
166
zephyr_app/src/main.c
Normal file
166
zephyr_app/src/main.c
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* main.c - OpenLaserTag remote control, Zephyr port of main.py.
|
||||
*
|
||||
* Wiring of the event-driven subsystems (uasyncio tasks in the original):
|
||||
* - buttons -> send the mapped OLT command over IR, beep, flash green
|
||||
* - IR RX -> log + BLE telemetry + flash blue then back to red
|
||||
* - battery -> periodic BLE telemetry
|
||||
* - BLE RX -> parse bomb/led_on/led_off/hex command
|
||||
* - heartbeat + piezo startup beep
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "board_io.h"
|
||||
#include "olt_proto.h"
|
||||
#include "piezo.h"
|
||||
#include "buttons.h"
|
||||
#include "rgb_led.h"
|
||||
#include "battery.h"
|
||||
#include "ir_tx.h"
|
||||
#include "ir_rx.h"
|
||||
#if defined(CONFIG_APP_BLE)
|
||||
#include "ble_nus.h"
|
||||
#endif
|
||||
|
||||
LOG_MODULE_REGISTER(app_main, LOG_LEVEL_INF);
|
||||
|
||||
/* Button index -> OLT command (see app_config.h / main.py:371). */
|
||||
static const enum olt_command_id BUTTON_CMD_MAP[4] = {
|
||||
OLT_CMD_NEW_GAME, /* button 0 (GPIO18) */
|
||||
OLT_CMD_EXPLODE, /* button 1 (GPIO21) */
|
||||
OLT_CMD_TEST_SHOT, /* button 2 (GPIO20) */
|
||||
OLT_CMD_SENSOR_TEST, /* button 3 (GPIO19) */
|
||||
};
|
||||
|
||||
/* Forward telemetry to BLE when available, otherwise just log. */
|
||||
static void telemetry(const char *line)
|
||||
{
|
||||
#if defined(CONFIG_APP_BLE)
|
||||
ble_nus_write(line);
|
||||
#endif
|
||||
LOG_INF("%s", line);
|
||||
}
|
||||
|
||||
/* Restore the idle (red) colour a while after an RX flash (main.py:324-326). */
|
||||
static void rgb_idle_fn(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
rgb_red();
|
||||
}
|
||||
static K_WORK_DELAYABLE_DEFINE(rgb_idle, rgb_idle_fn);
|
||||
|
||||
static void on_button(int index, enum btn_event event)
|
||||
{
|
||||
if (event != BTN_PRESS) {
|
||||
return; /* original acts on press; release/long/double unused */
|
||||
}
|
||||
if (index < 0 || index >= (int)ARRAY_SIZE(BUTTON_CMD_MAP)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const struct olt_command *cmd = &olt_commands[BUTTON_CMD_MAP[index]];
|
||||
LOG_INF("Button %d -> %s (0x%02X%02X%02X)", index, cmd->name,
|
||||
cmd->tx1, cmd->tx2, cmd->tx3);
|
||||
|
||||
piezo_short_beep();
|
||||
rgb_green();
|
||||
ir_tx_send(cmd->tx1, cmd->tx2, cmd->tx3);
|
||||
}
|
||||
|
||||
static void on_ir_rx(const struct olt_frame *f)
|
||||
{
|
||||
char line[64];
|
||||
|
||||
if (f->is_command) {
|
||||
snprintf(line, sizeof(line), "IR RX Command 0x%06X\n", f->packet);
|
||||
telemetry(line);
|
||||
} else {
|
||||
snprintf(line, sizeof(line), "IR RX Shot 0x%06X\n", f->packet);
|
||||
telemetry(line);
|
||||
snprintf(line, sizeof(line),
|
||||
"Shot: player ID:%u, team %s, demage %u\n",
|
||||
f->shot.player_id, olt_team_name(f->shot.team),
|
||||
f->shot.damage);
|
||||
telemetry(line);
|
||||
}
|
||||
|
||||
rgb_blue();
|
||||
k_work_reschedule(&rgb_idle, K_MSEC(500));
|
||||
}
|
||||
|
||||
#if defined(CONFIG_APP_BLE)
|
||||
static void on_ble_rx(const char *msg)
|
||||
{
|
||||
char line[64];
|
||||
|
||||
if (strstr(msg, "bomb")) {
|
||||
LOG_INF("BLE: Bomb Explode send to IR");
|
||||
const struct olt_command *ex = &olt_commands[OLT_CMD_EXPLODE];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ir_tx_send(ex->tx1, ex->tx2, ex->tx3);
|
||||
}
|
||||
piezo_beep();
|
||||
rgb_red();
|
||||
telemetry("BLE: Bomb Explode send to IR\n");
|
||||
} else if (strstr(msg, "led_on")) {
|
||||
rgb_red();
|
||||
telemetry("BLE: LED ON\n");
|
||||
} else if (strstr(msg, "led_off")) {
|
||||
rgb_off();
|
||||
telemetry("BLE: LED OFF\n");
|
||||
} else {
|
||||
/* Hex string -> 24-bit custom command (main.py:155-165). */
|
||||
char *end = NULL;
|
||||
unsigned long v = strtoul(msg, &end, 16);
|
||||
|
||||
if (end == msg) {
|
||||
LOG_DBG("BLE: unparsable message");
|
||||
return;
|
||||
}
|
||||
uint8_t tx1 = (v >> 16) & 0xFF;
|
||||
uint8_t tx2 = (v >> 8) & 0xFF;
|
||||
uint8_t tx3 = v & 0xFF;
|
||||
ir_tx_send(tx1, tx2, tx3);
|
||||
snprintf(line, sizeof(line), "BLE: uart recive 0x%06lX\n",
|
||||
v & 0xFFFFFFUL);
|
||||
telemetry(line);
|
||||
}
|
||||
}
|
||||
#endif /* CONFIG_APP_BLE */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
LOG_INF("OpenLaserTag remote control starting");
|
||||
|
||||
board_io_init();
|
||||
|
||||
piezo_init();
|
||||
rgb_init();
|
||||
rgb_red(); /* idle colour, as main.py sets red before the button loop */
|
||||
|
||||
battery_init(telemetry);
|
||||
|
||||
ir_tx_init();
|
||||
ir_rx_init(on_ir_rx);
|
||||
|
||||
buttons_init(on_button);
|
||||
|
||||
#if defined(CONFIG_APP_BLE)
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "OpenLaserTag %s", board_device_id_str());
|
||||
ble_nus_init(name, on_ble_rx);
|
||||
#else
|
||||
LOG_INF("BLE disabled (build with overlay-ble.conf on a radio board)");
|
||||
#endif
|
||||
|
||||
board_io_start_heartbeat();
|
||||
piezo_short_beep();
|
||||
|
||||
LOG_INF("Init complete");
|
||||
return 0;
|
||||
}
|
||||
108
zephyr_app/src/olt_proto.c
Normal file
108
zephyr_app/src/olt_proto.c
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* olt_proto.c - OpenLaserTag IR protocol encode/decode.
|
||||
* Ported from lib/olt_lib/ir_tx/olt.py and lib/olt_lib/ir_rx/olt.py.
|
||||
*/
|
||||
#include "olt_proto.h"
|
||||
|
||||
/* Command table, ported from main.py:228-236. */
|
||||
const struct olt_command olt_commands[OLT_CMD_COUNT] = {
|
||||
[OLT_CMD_NEW_GAME] = { "NewGame", 0x83, 0x05, OLT_COMMAND_END },
|
||||
[OLT_CMD_EXPLODE] = { "Explode", 0x83, 0x0B, OLT_COMMAND_END },
|
||||
[OLT_CMD_TEST_SHOT] = { "TestShot", 0x08, 0x24, 0x00 },
|
||||
[OLT_CMD_SENSOR_TEST] = { "SensorTest", 0x83, 0x15, OLT_COMMAND_END },
|
||||
};
|
||||
|
||||
/* Damage lookup, ported from main.py:178 (DEMAGE). */
|
||||
static const uint8_t damage_lut[16] = {
|
||||
0, 1, 4, 5, 7, 10, 15, 17, 20, 25, 30, 35, 40, 50, 75, 100,
|
||||
};
|
||||
|
||||
static const char *const team_names[4] = { "RED", "BLUE", "YELLOW", "GREEN" };
|
||||
|
||||
const char *olt_team_name(uint8_t team)
|
||||
{
|
||||
return team < 4 ? team_names[team] : "?";
|
||||
}
|
||||
|
||||
/* Bit-reverse a 32-bit value (ir_tx/olt.py:rbit32). */
|
||||
static uint32_t rbit32(uint32_t v)
|
||||
{
|
||||
v = (v & 0x0000FFFFu) << 16 | (v & 0xFFFF0000u) >> 16;
|
||||
v = (v & 0x00FF00FFu) << 8 | (v & 0xFF00FF00u) >> 8;
|
||||
v = (v & 0x0F0F0F0Fu) << 4 | (v & 0xF0F0F0F0u) >> 4;
|
||||
v = (v & 0x33333333u) << 2 | (v & 0xCCCCCCCCu) >> 2;
|
||||
return (v & 0x55555555u) << 1 | (v & 0xAAAAAAAAu) >> 1;
|
||||
}
|
||||
|
||||
void olt_frame_from_packet(uint32_t packet, struct olt_frame *frame)
|
||||
{
|
||||
frame->packet = packet & 0xFFFFFFu;
|
||||
frame->byte1 = (packet >> 16) & 0xFF;
|
||||
frame->byte2 = (packet >> 8) & 0xFF;
|
||||
frame->byte3 = packet & 0xFF;
|
||||
frame->is_command = (packet & OLT_COMMAND_MASK) != 0;
|
||||
|
||||
/* Shot fields (main.py:312-316). Meaningful only for shot packets. */
|
||||
uint32_t v = (packet >> 10) & 0x3FFF;
|
||||
frame->shot.player_id = (v >> 6) & 0xFF;
|
||||
frame->shot.team = (v >> 4) & 0x3;
|
||||
frame->shot.damage = damage_lut[v & 0xF];
|
||||
}
|
||||
|
||||
size_t olt_encode(uint8_t tx1, uint8_t tx2, uint8_t tx3, uint16_t *out)
|
||||
{
|
||||
uint32_t v = ((uint32_t)tx1 << 16) | ((uint32_t)tx2 << 8) | tx3;
|
||||
|
||||
v = rbit32(v);
|
||||
v >>= 8; /* keep the 24 reversed bits in the low word */
|
||||
|
||||
size_t n = 0;
|
||||
out[n++] = OLT_LEAD_MARK_US; /* carrier ON */
|
||||
out[n++] = OLT_LEAD_SPACE_US; /* carrier OFF */
|
||||
|
||||
for (uint32_t i = 0; i < OLT_BITS; i++) {
|
||||
out[n++] = (v & 1u) ? OLT_BIT_ONE_MARK_US : OLT_BIT_ZERO_MARK_US;
|
||||
out[n++] = OLT_BIT_SPACE_US;
|
||||
v >>= 1;
|
||||
}
|
||||
|
||||
return n; /* == OLT_MAX_SYMBOLS */
|
||||
}
|
||||
|
||||
bool olt_decode(const uint32_t *times, size_t nedges, struct olt_frame *frame)
|
||||
{
|
||||
/* Edge-count gate (ir_rx/olt.py:38). For 24-bit frames nedges == 50. */
|
||||
if (nedges != 30 && nedges != 50 && nedges != 58) {
|
||||
return false;
|
||||
}
|
||||
size_t bits = (nedges - 2) / 2;
|
||||
if (bits > OLT_BITS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Leading 2.4 ms mark. */
|
||||
uint32_t width = times[1] - times[0];
|
||||
if (!(width > 1800 && width < 3000)) {
|
||||
return false;
|
||||
}
|
||||
/* Leading 600 µs space. */
|
||||
width = times[2] - times[1];
|
||||
if (!(width > 350 && width < 1000)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t val = 0;
|
||||
uint32_t bit = 1;
|
||||
for (size_t x = 2; x <= nedges - 2; x += 2) {
|
||||
if ((times[x + 1] - times[x]) > 900) { /* 1200 µs mark => 1 */
|
||||
val |= bit;
|
||||
}
|
||||
bit <<= 1;
|
||||
}
|
||||
|
||||
val = rbit32(val);
|
||||
val >>= 8;
|
||||
|
||||
olt_frame_from_packet(val & 0xFFFFFFu, frame);
|
||||
return true;
|
||||
}
|
||||
67
zephyr_app/src/olt_proto.h
Normal file
67
zephyr_app/src/olt_proto.h
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* olt_proto.h - OpenLaserTag IR protocol (pure C, hardware independent).
|
||||
*
|
||||
* Ported from lib/olt_lib/ir_tx/olt.py and lib/olt_lib/ir_rx/olt.py.
|
||||
* The encoder produces the µs mark/space timing sequence; the decoder turns a
|
||||
* captured edge-time sequence back into a 24-bit packet.
|
||||
*/
|
||||
#ifndef OLT_PROTO_H
|
||||
#define OLT_PROTO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "app_config.h"
|
||||
|
||||
/* Max symbols emitted by olt_encode():
|
||||
* lead mark + lead space + 24 * (bit mark + bit space) = 2 + 48 = 50. */
|
||||
#define OLT_MAX_SYMBOLS (2U + OLT_BITS * 2U)
|
||||
|
||||
/* team identifiers, index into olt_team_name(). */
|
||||
enum olt_team {
|
||||
OLT_TEAM_RED = 0,
|
||||
OLT_TEAM_BLUE,
|
||||
OLT_TEAM_YELLOW,
|
||||
OLT_TEAM_GREEN,
|
||||
};
|
||||
|
||||
/* Decoded shot packet (only valid when !is_command). */
|
||||
struct olt_shot {
|
||||
uint8_t player_id; /* 7-bit */
|
||||
uint8_t team; /* enum olt_team */
|
||||
uint8_t damage; /* resolved hit-points from the damage LUT */
|
||||
};
|
||||
|
||||
/* Decoded frame. */
|
||||
struct olt_frame {
|
||||
uint32_t packet; /* 24-bit raw payload */
|
||||
uint8_t byte1; /* (packet >> 16) & 0xFF */
|
||||
uint8_t byte2; /* (packet >> 8) & 0xFF */
|
||||
uint8_t byte3; /* packet & 0xFF */
|
||||
bool is_command; /* bit23 set */
|
||||
struct olt_shot shot;
|
||||
};
|
||||
|
||||
const char *olt_team_name(uint8_t team);
|
||||
|
||||
/*
|
||||
* Encode a 24-bit command (tx1=MSB byte) into an on/off duration sequence in
|
||||
* microseconds. out[0] is the first carrier-ON (mark) duration, out[1] the
|
||||
* following carrier-OFF (space), alternating. Returns the number of entries
|
||||
* written (always OLT_MAX_SYMBOLS). out must hold at least OLT_MAX_SYMBOLS.
|
||||
*/
|
||||
size_t olt_encode(uint8_t tx1, uint8_t tx2, uint8_t tx3, uint16_t *out);
|
||||
|
||||
/*
|
||||
* Decode an IR frame from absolute edge timestamps (microseconds).
|
||||
* `times` holds `nedges` timestamps captured on every signal edge, oldest
|
||||
* first (same model as lib/olt_lib/ir_rx/__init__.py). Returns true and fills
|
||||
* `frame` on a valid 24-bit packet; returns false on a malformed frame.
|
||||
*/
|
||||
bool olt_decode(const uint32_t *times, size_t nedges, struct olt_frame *frame);
|
||||
|
||||
/* Fill `frame` (shot fields too) from an already-known 24-bit packet. */
|
||||
void olt_frame_from_packet(uint32_t packet, struct olt_frame *frame);
|
||||
|
||||
#endif /* OLT_PROTO_H */
|
||||
72
zephyr_app/src/piezo.c
Normal file
72
zephyr_app/src/piezo.c
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* piezo.c - Non-blocking piezo buzzer.
|
||||
* The MicroPython version awaited inside the beep coroutine; here we turn the
|
||||
* pin on and let a delayed work item turn it off, so callers never block.
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "app_config.h"
|
||||
#include "piezo.h"
|
||||
|
||||
LOG_MODULE_REGISTER(piezo, LOG_LEVEL_INF);
|
||||
|
||||
#define PIEZO_NODE DT_ALIAS(piezo)
|
||||
|
||||
#if DT_NODE_HAS_STATUS(PIEZO_NODE, okay)
|
||||
static const struct gpio_dt_spec piezo = GPIO_DT_SPEC_GET(PIEZO_NODE, gpios);
|
||||
static bool ready;
|
||||
|
||||
static void piezo_off_work(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
(void)gpio_pin_set_dt(&piezo, 0);
|
||||
}
|
||||
static K_WORK_DELAYABLE_DEFINE(piezo_off, piezo_off_work);
|
||||
#endif
|
||||
|
||||
int piezo_init(void)
|
||||
{
|
||||
#if DT_NODE_HAS_STATUS(PIEZO_NODE, okay)
|
||||
if (!gpio_is_ready_dt(&piezo)) {
|
||||
LOG_ERR("piezo GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
int ret = gpio_pin_configure_dt(&piezo, GPIO_OUTPUT_INACTIVE);
|
||||
if (ret) {
|
||||
LOG_ERR("piezo configure failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
ready = true;
|
||||
LOG_INF("piezo initialized");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WRN("piezo alias not configured");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void piezo_beep_ms(uint32_t ms)
|
||||
{
|
||||
#if DT_NODE_HAS_STATUS(PIEZO_NODE, okay)
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
(void)gpio_pin_set_dt(&piezo, 1);
|
||||
/* Reschedule: the latest request wins, matching overlapping beeps. */
|
||||
(void)k_work_reschedule(&piezo_off, K_MSEC(ms));
|
||||
#else
|
||||
ARG_UNUSED(ms);
|
||||
#endif
|
||||
}
|
||||
|
||||
void piezo_beep(void)
|
||||
{
|
||||
piezo_beep_ms(PIEZO_BEEP_MS);
|
||||
}
|
||||
|
||||
void piezo_short_beep(void)
|
||||
{
|
||||
piezo_beep_ms(PIEZO_SHORT_BEEP_MS);
|
||||
}
|
||||
20
zephyr_app/src/piezo.h
Normal file
20
zephyr_app/src/piezo.h
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* piezo.h - Non-blocking piezo buzzer control.
|
||||
* Ported from main.py:piezo_beep / piezo_short_beep (GPIO15, active-high).
|
||||
*/
|
||||
#ifndef PIEZO_H
|
||||
#define PIEZO_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Configure the piezo GPIO. Returns 0 on success, negative errno otherwise. */
|
||||
int piezo_init(void);
|
||||
|
||||
/* Drive the piezo high and schedule it off after `ms` (non-blocking). */
|
||||
void piezo_beep_ms(uint32_t ms);
|
||||
|
||||
/* Convenience wrappers matching the MicroPython helpers. */
|
||||
void piezo_beep(void); /* PIEZO_BEEP_MS */
|
||||
void piezo_short_beep(void); /* PIEZO_SHORT_BEEP_MS */
|
||||
|
||||
#endif /* PIEZO_H */
|
||||
116
zephyr_app/src/rgb_led.c
Normal file
116
zephyr_app/src/rgb_led.c
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* rgb_led.c - PWM RGB LED, port of lib/rgb.py.
|
||||
* Uses pwm_dt_spec from the `rgb-red/green/blue` aliases (pwm-leds nodes).
|
||||
*/
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/pwm.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include "rgb_led.h"
|
||||
|
||||
LOG_MODULE_REGISTER(rgb_led, LOG_LEVEL_INF);
|
||||
|
||||
#define RGB_R_NODE DT_ALIAS(rgb_red)
|
||||
#define RGB_G_NODE DT_ALIAS(rgb_green)
|
||||
#define RGB_B_NODE DT_ALIAS(rgb_blue)
|
||||
|
||||
#if DT_NODE_HAS_STATUS(RGB_R_NODE, okay) && \
|
||||
DT_NODE_HAS_STATUS(RGB_G_NODE, okay) && \
|
||||
DT_NODE_HAS_STATUS(RGB_B_NODE, okay)
|
||||
#define RGB_PRESENT 1
|
||||
static const struct pwm_dt_spec led_r = PWM_DT_SPEC_GET(RGB_R_NODE);
|
||||
static const struct pwm_dt_spec led_g = PWM_DT_SPEC_GET(RGB_G_NODE);
|
||||
static const struct pwm_dt_spec led_b = PWM_DT_SPEC_GET(RGB_B_NODE);
|
||||
#else
|
||||
#define RGB_PRESENT 0
|
||||
#warning "RGB aliases (rgb-red/green/blue) not found in overlay"
|
||||
#endif
|
||||
|
||||
static float rgb_pwr = 1.0f; /* lib/rgb.py default */
|
||||
|
||||
static float clampf(float v)
|
||||
{
|
||||
if (v < 0.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
if (v > 1.0f) {
|
||||
return 1.0f;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
#if RGB_PRESENT
|
||||
static void set_channel(const struct pwm_dt_spec *led, float level)
|
||||
{
|
||||
uint32_t pulse = (uint32_t)(clampf(level) * (float)led->period);
|
||||
int ret = pwm_set_pulse_dt(led, pulse);
|
||||
if (ret) {
|
||||
LOG_WRN("pwm set failed: %d", ret);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int rgb_init(void)
|
||||
{
|
||||
#if RGB_PRESENT
|
||||
if (!pwm_is_ready_dt(&led_r) || !pwm_is_ready_dt(&led_g) ||
|
||||
!pwm_is_ready_dt(&led_b)) {
|
||||
LOG_ERR("RGB PWM not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
rgb_off();
|
||||
LOG_INF("RGB LED initialized");
|
||||
return 0;
|
||||
#else
|
||||
LOG_WRN("RGB LED not configured");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void rgb_set(float r, float g, float b)
|
||||
{
|
||||
#if RGB_PRESENT
|
||||
set_channel(&led_r, r * rgb_pwr);
|
||||
set_channel(&led_g, g * rgb_pwr);
|
||||
set_channel(&led_b, b * rgb_pwr);
|
||||
#else
|
||||
ARG_UNUSED(r);
|
||||
ARG_UNUSED(g);
|
||||
ARG_UNUSED(b);
|
||||
#endif
|
||||
}
|
||||
|
||||
void rgb_set_power(float pwr)
|
||||
{
|
||||
rgb_pwr = clampf(pwr);
|
||||
}
|
||||
|
||||
float rgb_get_power(void)
|
||||
{
|
||||
return rgb_pwr;
|
||||
}
|
||||
|
||||
void rgb_off(void)
|
||||
{
|
||||
rgb_set(0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void rgb_red(void)
|
||||
{
|
||||
rgb_set(1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
void rgb_green(void)
|
||||
{
|
||||
rgb_set(0.0f, 1.0f, 0.0f);
|
||||
}
|
||||
|
||||
void rgb_blue(void)
|
||||
{
|
||||
rgb_set(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void rgb_white(void)
|
||||
{
|
||||
rgb_set(0.8f, 0.8f, 0.8f);
|
||||
}
|
||||
25
zephyr_app/src/rgb_led.h
Normal file
25
zephyr_app/src/rgb_led.h
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* rgb_led.h - RGB status LED via PWM.
|
||||
* Ported from lib/rgb.py (GPIO22/26/27, 100 Hz, active-high, global power
|
||||
* scaling 0.0-1.0). Channel values are 0.0-1.0 like the Python API.
|
||||
*/
|
||||
#ifndef RGB_LED_H
|
||||
#define RGB_LED_H
|
||||
|
||||
int rgb_init(void);
|
||||
|
||||
/* Set the three channels (0.0-1.0); scaled by the current power level. */
|
||||
void rgb_set(float r, float g, float b);
|
||||
|
||||
/* Global power multiplier (lib/rgb.py rgb_pwr), clamped to 0.0-1.0. */
|
||||
void rgb_set_power(float pwr);
|
||||
float rgb_get_power(void);
|
||||
|
||||
/* Presets matching lib/rgb.py. */
|
||||
void rgb_off(void);
|
||||
void rgb_red(void);
|
||||
void rgb_green(void);
|
||||
void rgb_blue(void);
|
||||
void rgb_white(void);
|
||||
|
||||
#endif /* RGB_LED_H */
|
||||
91
zephyr_app/tests/olt_proto_test.c
Normal file
91
zephyr_app/tests/olt_proto_test.c
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Host unit test for olt_proto.c (no Zephyr dependency).
|
||||
*
|
||||
* Build & run on a workstation:
|
||||
* cc -I ../src olt_proto_test.c ../src/olt_proto.c -o /tmp/olt_test && /tmp/olt_test
|
||||
*
|
||||
* Verifies encode timing, encode->decode round-trips, and shot decoding.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "olt_proto.h"
|
||||
|
||||
static int failures;
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL: "); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* Build the absolute edge-time array the RX would capture from an encoded
|
||||
* frame (see decode model in olt_proto.c / ir_rx). Returns nedges. */
|
||||
static size_t synth_edges(const uint16_t *sym, uint32_t *times)
|
||||
{
|
||||
/* 50 symbols produce 50 usable edge timestamps (0..49). */
|
||||
times[0] = 0;
|
||||
for (size_t i = 0; i < OLT_MAX_SYMBOLS - 1; i++) {
|
||||
times[i + 1] = times[i] + sym[i];
|
||||
}
|
||||
return OLT_MAX_SYMBOLS; /* 50 */
|
||||
}
|
||||
|
||||
static void test_roundtrip(uint8_t b1, uint8_t b2, uint8_t b3)
|
||||
{
|
||||
uint16_t sym[OLT_MAX_SYMBOLS];
|
||||
uint32_t times[OLT_MAX_SYMBOLS + 2];
|
||||
struct olt_frame f;
|
||||
|
||||
size_t n = olt_encode(b1, b2, b3, sym);
|
||||
CHECK(n == OLT_MAX_SYMBOLS, "encode count %zu", n);
|
||||
CHECK(sym[0] == OLT_LEAD_MARK_US, "lead mark %u", sym[0]);
|
||||
CHECK(sym[1] == OLT_LEAD_SPACE_US, "lead space %u", sym[1]);
|
||||
|
||||
size_t ne = synth_edges(sym, times);
|
||||
bool ok = olt_decode(times, ne, &f);
|
||||
CHECK(ok, "decode 0x%02X%02X%02X failed", b1, b2, b3);
|
||||
|
||||
uint32_t expect = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3;
|
||||
CHECK(f.packet == expect, "roundtrip got 0x%06X want 0x%06X",
|
||||
f.packet, expect);
|
||||
CHECK(f.byte1 == b1 && f.byte2 == b2 && f.byte3 == b3,
|
||||
"bytes %02X %02X %02X", f.byte1, f.byte2, f.byte3);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Round-trip the standard command set. */
|
||||
for (int i = 0; i < OLT_CMD_COUNT; i++) {
|
||||
const struct olt_command *c = &olt_commands[i];
|
||||
test_roundtrip(c->tx1, c->tx2, c->tx3);
|
||||
}
|
||||
/* A spread of arbitrary 24-bit values. */
|
||||
test_roundtrip(0x00, 0x00, 0x00);
|
||||
test_roundtrip(0xFF, 0xFF, 0xFF);
|
||||
test_roundtrip(0x12, 0x34, 0x56);
|
||||
test_roundtrip(0xAA, 0x55, 0xA5);
|
||||
|
||||
/* Command vs shot classification. */
|
||||
struct olt_frame f;
|
||||
olt_frame_from_packet(0x830BE8, &f); /* Explode */
|
||||
CHECK(f.is_command, "Explode should be a command packet");
|
||||
|
||||
/* TestShot 0x082400: id 8, RED, damage 25 (main.py:234 comment). */
|
||||
olt_frame_from_packet(0x082400, &f);
|
||||
CHECK(!f.is_command, "TestShot should be a shot packet");
|
||||
CHECK(f.shot.player_id == 8, "player id %u", f.shot.player_id);
|
||||
CHECK(f.shot.team == OLT_TEAM_RED, "team %s", olt_team_name(f.shot.team));
|
||||
CHECK(f.shot.damage == 25, "damage %u", f.shot.damage);
|
||||
|
||||
if (failures == 0) {
|
||||
printf("ALL OLT PROTOCOL TESTS PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
printf("%d FAILURE(S)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user