From d0878ae502a8a0b11ddf8e444991505fe4d7570e Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 21:42:55 -0600 Subject: [PATCH 1/9] Migrate firmware build to PlatformIO, restructure docs Replaces arduino-cli/mklittlefs/esptool with PlatformIO across build.sh and CI. PlatformIO parses the FS partition geometry directly from board_build.ldscript (eagle.flash.4m2m.ld) on every build, removing the hand-maintained mklittlefs size constant that caused a silent LittleFS reformat bug in the past (see docs/LITTLEFS.md). Also splits the README into a front page with a condensed quickstart, moving the full prerequisites/build-script reference to docs/BUILD.md and the WebSocket protocol spec to docs/PROTOCOL.md. Bumps version to 2.3.0. --- .github/workflows/ci.yml | 19 ++--- .gitignore | 1 + CONTRIBUTING.md | 14 ++-- README.md | 140 +++++------------------------------ build.sh | 52 +++---------- docs/BUILD.md | 86 +++++++++++++++++++++ docs/LITTLEFS.md | 6 +- docs/PROTOCOL.md | 22 ++++++ firmware/z-duino/z-duino.ino | 2 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- platformio.ini | 15 ++++ 12 files changed, 176 insertions(+), 187 deletions(-) create mode 100644 docs/BUILD.md create mode 100644 docs/PROTOCOL.md create mode 100644 platformio.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eabb897..aea886c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,21 +24,14 @@ jobs: firmware: runs-on: ubuntu-latest - env: - FQBN: "esp8266:esp8266:d1_mini:xtal=80,vt=flash,exception=disabled,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600" - ESP8266_INDEX: "https://arduino.esp8266.com/stable/package_esp8266com_index.json" steps: - uses: actions/checkout@v7 - - uses: arduino/setup-arduino-cli@v2 - - name: Install ESP8266 core - run: | - arduino-cli core update-index --additional-urls "$ESP8266_INDEX" - arduino-cli core install esp8266:esp8266 --additional-urls "$ESP8266_INDEX" - - name: Install libraries - run: | - arduino-cli lib install ArduinoJson - arduino-cli lib install WebSockets + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install PlatformIO + run: pip install -U platformio - name: Create secrets stub run: cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h - name: Compile - run: arduino-cli compile --fqbn "$FQBN" firmware/z-duino + run: pio run diff --git a/.gitignore b/.gitignore index e05ba03..2028b72 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ arduino_secrets.h # Build artifacts build/ +.pio/ # Temp / debug scripts tmp/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index deeea2c..d919036 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,9 @@ ### Prerequisites - [Node.js](https://nodejs.org/) v18+ -- [Arduino CLI](https://arduino.github.io/arduino-cli/) (for firmware work) -- ESP8266 board package: `arduino-cli core install esp8266:esp8266` -- Arduino libraries: - ```bash - arduino-cli lib install ArduinoJson - arduino-cli lib install WebSockets - ``` +- [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html) (for firmware work): `brew install platformio` + +Firmware dependencies (`espressif8266` platform, `ArduinoJson`, `WebSockets`) are declared in `platformio.ini` and resolved automatically by PlatformIO on first build — no manual install step needed. See [docs/BUILD.md](docs/BUILD.md) for the full setup walkthrough. ### Frontend Development @@ -51,6 +47,7 @@ Open `http://localhost:3000` in your browser. The throttle UI is fully functiona ``` z-duino/ +├── platformio.ini # PlatformIO project config (board, ldscript, lib_deps) ├── firmware/z-duino/ │ ├── z-duino.ino # Main sketch (WiFi, mDNS, HTTP, WebSocket) │ ├── Motor.h / Motor.cpp # TB6612FNG motor driver abstraction @@ -70,6 +67,9 @@ z-duino/ │ ├── tsconfig.json │ └── package.json ├── docs/ +│ ├── BUILD.md # Full software prerequisites & build script reference +│ ├── HARDWARE.md # Parts list & wiring diagram +│ ├── PROTOCOL.md # Full WebSocket protocol spec │ ├── LITTLEFS.md # LittleFS flash layout & mklittlefs parameters │ └── status-led-wiring.md # RGB LED wiring & resistor values └── build.sh # Build + deploy script diff --git a/README.md b/README.md index ca0f1fe..5468130 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![CI](https://github.com/yamanote1138/z-duino/actions/workflows/ci.yml/badge.svg)](https://github.com/yamanote1138/z-duino/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license) ![ESP8266](https://img.shields.io/badge/ESP8266-Arduino-00979D?logo=arduino&logoColor=white) +![PlatformIO](https://img.shields.io/badge/PlatformIO-Core-FF7F00?logo=platformio&logoColor=white) ![Vue 3](https://img.shields.io/badge/Vue-3-4FC08D?logo=vuedotjs&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-6-646CFF?logo=vite&logoColor=white) @@ -32,138 +33,37 @@ Discoverable via mDNS at `http://ztrain.local`. Wemos D1 Mini (ESP8266) → TB6612FNG H-bridge → track, plus an RGB status LED. Full parts list, wiring diagram, and pinout live in **[docs/HARDWARE.md](docs/HARDWARE.md)**. -## Software Prerequisites +## Quickstart -### macOS Setup - -1. **Arduino CLI** (via Homebrew): - ```bash - brew install arduino-cli - ``` - -2. **ESP8266 board package** — add the board manager URL, then install: - ```bash - arduino-cli config init # creates ~/Library/Arduino15/arduino-cli.yaml - arduino-cli config add board_manager.additional_urls \ - http://arduino.esp8266.com/stable/package_esp8266com_index.json - arduino-cli core update-index - arduino-cli core install esp8266:esp8266 - ``` - This installs the board definitions, toolchain, `mklittlefs`, and `esptool` under: - ``` - ~/Library/Arduino15/packages/esp8266/ - ``` - -3. **Arduino libraries:** - ```bash - arduino-cli lib install ArduinoJson WebSockets - ``` - Libraries install to `~/Arduino/libraries/`. - -4. **Node.js** (v18+) — via [nvm](https://github.com/nvm-sh/nvm) or [nodejs.org](https://nodejs.org/): - ```bash - nvm install 20 - ``` - -### Verify Installation - -```bash -arduino-cli version # should print version info -arduino-cli core list # should show esp8266:esp8266 -arduino-cli lib list # should show ArduinoJson and WebSockets -node --version # v18+ required -``` - -`mklittlefs` and `esptool` don't need to be on your PATH — `build.sh` finds them automatically inside `~/Library/Arduino15/`. - -## Setup - -1. **Clone the repo:** +1. Install [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html) (`brew install platformio`) and [Node.js](https://nodejs.org/) v18+. +2. Clone the repo and configure WiFi credentials: ```bash git clone https://github.com/yamanote1138/z-duino.git cd z-duino - ``` - -2. **Configure WiFi credentials:** - ```bash cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h + # edit arduino_secrets.h with your WiFi SSID/password ``` - Edit `arduino_secrets.h` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). - -3. **Install frontend dependencies:** +3. Install frontend dependencies and build + flash everything: ```bash - cd frontend - npm install - cd .. + cd frontend && npm install && cd .. + ./build.sh # select option 7 (Build + Upload All) ``` -4. **Build and flash:** - ```bash - ./build.sh - ``` - Select option **7** (Build + Upload All). This will: - - Build the frontend with Vite - - Compile the firmware with arduino-cli - - Flash the firmware to the Wemos - - Pack and flash the frontend filesystem (LittleFS) +For the full prerequisites walkthrough and every `build.sh` option, see **[docs/BUILD.md](docs/BUILD.md)**. ## Usage -1. Power up the Wemos D1 Mini (USB or external 5V) -2. Connect your 12V supply to the TB6612FNG -3. Open a browser and navigate to **http://ztrain.local** - - If mDNS isn't working on your network, check the serial monitor (115200 baud) for the device's IP address -4. Use the throttle: - - **Speed segments 1–10** — click to ramp to that speed level. Click a lit segment to ramp back down. - - **FWD / REV** — toggle direction. If the train is moving, it will smoothly ramp down, switch, and ramp back up. - - **Brake** — smooth ramp to stop - - **E-Stop** — immediate stop - -> **Z-scale note:** These locomotives are tiny and light. Don't be surprised if nothing happens at low power — it's normal for a Z-scale train to sit still until 40–50% throttle, then take off suddenly once it overcomes static friction. This isn't a bug; it's just how small motors behave at low voltage. You may want to start around segment 4 and work up from there. - -## Development - -For frontend development without hardware: - -```bash -cd frontend -npm run dev:all -``` - -This starts the Vite dev server on `http://localhost:3000` and a mock WebSocket server on port 81 that simulates the ESP8266's responses. - -## Build Script Options - -| Option | Action | -|---|---| -| 1 | Build All (frontend + firmware) | -| 2 | Build Frontend only | -| 3 | Build Firmware only | -| 4 | Upload Firmware only | -| 5 | Upload LittleFS only | -| 6 | Upload All (firmware + filesystem) | -| 7 | Build + Upload All | - -## WebSocket Protocol - -The ESP8266 runs a WebSocket server on port 81 (subprotocol `arduino`). - -**Client → Device:** -```json -{"cmd": "speed", "value": 0.0} // 0.0 to 1.0 -{"cmd": "direction", "value": true} // true = forward -{"cmd": "stop"} // immediate stop -{"cmd": "ping"} // keepalive -{"cmd": "invert", "value": true} // motor direction invert -{"cmd": "led", "r": 0, "g": 0, "b": 1000} // LED test mode (0–1000 per channel) -{"cmd": "led_auto"} // resume status LED behaviour -``` - -**Device → Client:** -```json -{"type": "status", "name": "Z-Duino", "speed": 0.0, "direction": true, "connected": true} -{"type": "pong"} -``` +Power up the Wemos, connect your 12V track supply, and open **http://ztrain.local** in a browser (check the serial monitor at 115200 baud for the IP if mDNS doesn't resolve). Speed segments 1–10 ramp to that throttle level, FWD/REV toggles direction (ramping down/up automatically if moving), Brake ramps to a stop, and E-Stop halts immediately. + +> **Z-scale note:** These locomotives are tiny and light. Don't be surprised if nothing happens at low power — it's normal for a Z-scale train to sit still until 40–50% throttle, then take off suddenly once it overcomes static friction. Start around segment 4 and work up from there. + +## Documentation + +- **[docs/BUILD.md](docs/BUILD.md)** — full prerequisites, setup, and `build.sh` reference +- **[docs/HARDWARE.md](docs/HARDWARE.md)** — parts list & wiring diagram +- **[docs/PROTOCOL.md](docs/PROTOCOL.md)** — WebSocket protocol spec +- **[docs/LITTLEFS.md](docs/LITTLEFS.md)** — LittleFS flash layout & partition details +- **[CONTRIBUTING.md](CONTRIBUTING.md)** — local development setup & project structure ## License diff --git a/build.sh b/build.sh index 72527f9..e39cbae 100755 --- a/build.sh +++ b/build.sh @@ -9,12 +9,9 @@ CYAN='\033[0;36m' NC='\033[0m' # Config -FQBN="esp8266:esp8266:d1_mini:xtal=80,vt=flash,exception=disabled,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600" -SKETCH_DIR="firmware/z-duino" FRONTEND_DIR="frontend" BUILD_DIR="build" DATA_DIR="$BUILD_DIR/data" -ARDUINO_DATA=$(arduino-cli config get directories.data 2>/dev/null) info() { echo -e "${CYAN}[INFO]${NC} $1"; } success() { echo -e "${GREEN}[OK]${NC} $1"; } @@ -22,16 +19,9 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } check_deps() { - local missing=() - if ! command -v arduino-cli &>/dev/null; then - error "arduino-cli not found. See: https://arduino.github.io/arduino-cli/" - exit 1 - fi - arduino-cli lib list 2>/dev/null | grep -q "ArduinoJson" || missing+=("ArduinoJson") - arduino-cli lib list 2>/dev/null | grep -q "WebSockets" || missing+=("WebSockets") - if [ ${#missing[@]} -gt 0 ]; then - error "Missing Arduino libraries: ${missing[*]}" - info "Install with: arduino-cli lib install ${missing[*]}" + if ! command -v pio &>/dev/null; then + error "PlatformIO not found. Install with: brew install platformio" + info "See: https://docs.platformio.org/en/latest/core/installation/index.html" exit 1 fi } @@ -78,15 +68,15 @@ build_frontend() { build_firmware() { check_deps info "Compiling firmware..." - mkdir -p "$BUILD_DIR" - arduino-cli compile --fqbn "$FQBN" --output-dir "$BUILD_DIR" "$SKETCH_DIR" + pio run success "Firmware compiled" } upload_firmware() { + check_deps detect_port || return 1 info "Uploading firmware..." - if ! arduino-cli upload --fqbn "$FQBN" --port "$SELECTED_PORT" --input-dir "$BUILD_DIR" "$SKETCH_DIR"; then + if ! pio run -t upload --upload-port "$SELECTED_PORT"; then error "Firmware upload failed. Check the connection and try again." return 1 fi @@ -94,6 +84,7 @@ upload_firmware() { } upload_littlefs() { + check_deps detect_port || return 1 if [ ! -d "$DATA_DIR" ]; then @@ -101,34 +92,11 @@ upload_littlefs() { return 1 fi - info "Packing LittleFS image..." - - # Find mklittlefs via arduino-cli's data directory - MKLITTLEFS=$(find "$ARDUINO_DATA" -name "mklittlefs" -type f 2>/dev/null | head -1) - if [ -z "$MKLITTLEFS" ]; then - error "mklittlefs not found. Install the ESP8266 board package:" - info " arduino-cli core install esp8266:esp8266" + info "Packing and flashing LittleFS image..." + if ! pio run -t uploadfs --upload-port "$SELECTED_PORT"; then + error "LittleFS upload failed. Check the connection and try again." return 1 fi - - # 4M2M layout: 2MB filesystem at offset 0x200000 - # Block size MUST be 8192 to match eagle.flash.4m2m.ld (_FS_block = 0x2000) - # Size = FS_PHYS_SIZE = _FS_end - _FS_start = 0x1FA000 = 2072576 = 253 blocks × 8192 - "$MKLITTLEFS" -c "$DATA_DIR" -b 8192 -p 256 -s 2072576 "$BUILD_DIR/littlefs.bin" - success "LittleFS image created" - - # Find esptool via arduino-cli's data directory - ESPTOOL=$(find "$ARDUINO_DATA" -name "esptool" -not -name "*.py" -type f 2>/dev/null | head -1) - if [ -z "$ESPTOOL" ]; then - ESPTOOL=$(which esptool 2>/dev/null || which esptool.py 2>/dev/null || true) - fi - if [ -z "$ESPTOOL" ]; then - error "esptool not found." - return 1 - fi - - info "Flashing LittleFS to ESP8266..." - "$ESPTOOL" --chip esp8266 --port "$SELECTED_PORT" --baud 460800 write-flash 0x200000 "$BUILD_DIR/littlefs.bin" success "LittleFS uploaded" } diff --git a/docs/BUILD.md b/docs/BUILD.md new file mode 100644 index 0000000..2aeeba9 --- /dev/null +++ b/docs/BUILD.md @@ -0,0 +1,86 @@ +# Build & Development Setup + +Full software prerequisites and the `build.sh` reference. For the condensed quickstart, see the main [README](../README.md). + +## Software Prerequisites + +### macOS Setup + +1. **PlatformIO Core** (via Homebrew): + ```bash + brew install platformio + ``` + This installs `pio`, the CLI PlatformIO uses to compile, upload, and manage dependencies. + +2. **Node.js** (v18+) — via [nvm](https://github.com/nvm-sh/nvm) or [nodejs.org](https://nodejs.org/): + ```bash + nvm install 20 + ``` + +That's it — the ESP8266 platform, toolchain, `mklittlefs`, `esptool`, and the `ArduinoJson`/`WebSockets` libraries are all declared in `platformio.ini` and resolved automatically the first time you run `pio run` or `./build.sh`. No separate board-package or library-install step. + +### Verify Installation + +```bash +pio --version # should print version info +node --version # v18+ required +``` + +## Setup + +1. **Clone the repo:** + ```bash + git clone https://github.com/yamanote1138/z-duino.git + cd z-duino + ``` + +2. **Configure WiFi credentials:** + ```bash + cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h + ``` + Edit `arduino_secrets.h` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). + +3. **Install frontend dependencies:** + ```bash + cd frontend + npm install + cd .. + ``` + +4. **Build and flash:** + ```bash + ./build.sh + ``` + Select option **7** (Build + Upload All). This will: + - Build the frontend with Vite + - Compile the firmware with PlatformIO + - Flash the firmware to the Wemos + - Pack and flash the frontend filesystem (LittleFS) + +## Build Script Options + +| Option | Action | +|---|---| +| 1 | Build All (frontend + firmware) | +| 2 | Build Frontend only | +| 3 | Build Firmware only | +| 4 | Upload Firmware only | +| 5 | Upload LittleFS only | +| 6 | Upload All (firmware + filesystem) | +| 7 | Build + Upload All | + +Under the hood, `build.sh` wraps: +- **Build Firmware** → `pio run` +- **Upload Firmware** → `pio run -t upload` +- **Upload LittleFS** → `pio run -t uploadfs` (packs `build/data/` into a LittleFS image sized from `platformio.ini`'s `board_build.ldscript`, then flashes it — see [LITTLEFS.md](LITTLEFS.md) for the partition math) + +## Development + +For frontend development without hardware: + +```bash +cd frontend +npm run dev:all +``` + +This starts the Vite dev server on `http://localhost:3000` and a mock WebSocket server on port 81 that simulates the ESP8266's responses. diff --git a/docs/LITTLEFS.md b/docs/LITTLEFS.md index 0d683f3..6d447e4 100644 --- a/docs/LITTLEFS.md +++ b/docs/LITTLEFS.md @@ -26,7 +26,11 @@ The runtime block count: block_count = 2,072,576 / 8,192 = 253 (exact — no remainder) ``` -## mklittlefs Parameters +## PlatformIO Handles This Automatically + +As of the PlatformIO migration, `pio run -t buildfs` / `-t uploadfs` parse `_FS_START`, `_FS_END`, `_FS_BLOCK`, and `_FS_PAGE` directly out of whichever `board_build.ldscript` is configured in `platformio.ini` (currently `eagle.flash.4m2m.ld`), and pass the derived values to `mklittlefs` internally. There's no separate size constant to keep in sync — the ldscript is the single source of truth. The manual invocation below is kept for reference (e.g. if you ever need to pack an image outside of PlatformIO), but day-to-day you shouldn't need to run it by hand. + +## mklittlefs Parameters (Reference) The `mklittlefs` tool packs a directory into a LittleFS image. The parameters **must** match the runtime configuration exactly: diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md new file mode 100644 index 0000000..56c406f --- /dev/null +++ b/docs/PROTOCOL.md @@ -0,0 +1,22 @@ +# WebSocket Protocol + +The ESP8266 runs a WebSocket server on port 81 (subprotocol `arduino`). + +**Client → Device:** +```json +{"cmd": "speed", "value": 0.0} // 0.0 to 1.0 +{"cmd": "direction", "value": true} // true = forward +{"cmd": "stop"} // immediate stop +{"cmd": "ping"} // keepalive +{"cmd": "invert", "value": true} // motor direction invert +{"cmd": "led", "r": 0, "g": 0, "b": 1000} // LED test mode (0–1000 per channel) +{"cmd": "led_auto"} // resume status LED behaviour +``` + +**Device → Client:** +```json +{"type": "status", "name": "Z-Duino", "speed": 0.0, "direction": true, "connected": true} +{"type": "pong"} +``` + +The mock server (`frontend/mock-server.ts`) implements the same protocol for local development without hardware. diff --git a/firmware/z-duino/z-duino.ino b/firmware/z-duino/z-duino.ino index ec7c12b..d557931 100644 --- a/firmware/z-duino/z-duino.ino +++ b/firmware/z-duino/z-duino.ino @@ -10,7 +10,7 @@ #include "Motor.h" #include "StatusLED.h" -#define VERSION "2.0.0" +#define VERSION "2.3.0" // WiFi credentials from arduino_secrets.h char wifi_ssid[] = WIFI_SSID; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1bb4d5a..ba1961a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "z-duino-frontend", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "z-duino-frontend", - "version": "2.2.0", + "version": "2.3.0", "dependencies": { "@nuxt/ui": "^4.5.1", "tailwindcss": "^4.2.1", diff --git a/frontend/package.json b/frontend/package.json index 4af0a01..db733c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "z-duino-frontend", - "version": "2.2.0", + "version": "2.3.0", "private": true, "type": "module", "scripts": { diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..c2baff2 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,15 @@ +[platformio] +src_dir = firmware/z-duino +data_dir = build/data + +[env:d1_mini] +platform = espressif8266 +board = d1_mini +framework = arduino +board_build.filesystem = littlefs +board_build.ldscript = eagle.flash.4m2m.ld +lib_deps = + bblanchon/ArduinoJson + links2004/WebSockets +monitor_speed = 115200 +upload_speed = 921600 From 69d9c38d4eed644ac90902f333fa516a42a77251 Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 22:08:46 -0600 Subject: [PATCH 2/9] Lower upload_speed to 460800 (921600 unreliable on hardware) Verified end-to-end on a real Wemos D1 Mini: firmware + LittleFS flash, boot, WiFi/mDNS, and the full WebSocket protocol (status/ping/speed/stop) all confirmed working via the PlatformIO pipeline. 921600 baud upload failed intermittently ("Timed out waiting for packet header" / "Invalid head of packet") on this USB-serial adapter; 460800 flashed reliably. --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index c2baff2..4bce0ba 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,4 +12,4 @@ lib_deps = bblanchon/ArduinoJson links2004/WebSockets monitor_speed = 115200 -upload_speed = 921600 +upload_speed = 460800 From 04834b1efa6857172d0ff7bae4452512350f6ad8 Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 22:26:14 -0600 Subject: [PATCH 3/9] Replace arduino_secrets.h with PlatformIO secrets.ini, skip redundant frontend builds Secrets: arduino_secrets.h (a gitignored C header, #include'd into the sketch) is replaced by secrets.ini, a gitignored PlatformIO config file merged in via extra_configs. Values become plain -D build_flags, so the firmware code needs no changes beyond dropping the #include. Verified end-to-end on real hardware: WiFi/mDNS/HTTP/WebSocket all work identically with credentials sourced from secrets.ini instead of the old header. Frontend builds: PlatformIO's own incremental compile already makes re-running `pio run` on unchanged firmware near-instant, but `npm run build` has no such skip. build.sh now checks mtimes of frontend source files against the existing build/data/ output and skips the Vite build for options 1 and 7 when nothing changed (option 2 stays an explicit, unconditional rebuild). Verified both the skip and rebuild paths. --- .github/workflows/ci.yml | 2 +- .gitignore | 1 + CLAUDE.md | 5 +++-- CONTRIBUTING.md | 8 ++++---- README.md | 4 ++-- build.sh | 19 +++++++++++++++++-- docs/BUILD.md | 6 ++++-- firmware/z-duino/arduino_secrets.h.example | 16 ---------------- firmware/z-duino/z-duino.ino | 1 - platformio.ini | 1 + secrets.ini.example | 13 +++++++++++++ 11 files changed, 46 insertions(+), 30 deletions(-) delete mode 100644 firmware/z-duino/arduino_secrets.h.example create mode 100644 secrets.ini.example diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aea886c..ad2ad0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,6 @@ jobs: - name: Install PlatformIO run: pip install -U platformio - name: Create secrets stub - run: cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h + run: cp secrets.ini.example secrets.ini - name: Compile run: pio run diff --git a/.gitignore b/.gitignore index 2028b72..c6ffc9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Secrets +secrets.ini arduino_secrets.h # Build artifacts diff --git a/CLAUDE.md b/CLAUDE.md index 189c334..5a43e09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,10 +32,11 @@ npm run dev:all # Vite dev server + mock WebSocket server ``` ## Key Files +- `platformio.ini` — PlatformIO project config (board, ldscript, lib_deps, secrets) +- `secrets.ini` — WiFi creds & build-time config (not committed, copy from `secrets.ini.example`) - `firmware/z-duino/z-duino.ino` — Main sketch - `firmware/z-duino/Motor.h/.cpp` — Motor abstraction - `firmware/z-duino/StatusLED.h/.cpp` — Status LED abstraction -- `firmware/z-duino/arduino_secrets.h` — WiFi creds (not committed, copy from .example) - `frontend/src/App.vue` — Root Vue component - `frontend/src/composables/useTrainController.ts` — Core controller logic (WebSocket, ramping, state) - `frontend/src/components/` — Vue SFCs (SpeedController, LedTestPanel, DebugModal, etc.) @@ -43,5 +44,5 @@ npm run dev:all # Vite dev server + mock WebSocket server - `docs/LITTLEFS.md` — LittleFS flash layout & mklittlefs parameters ## Dependencies -- Arduino: ESP8266 board package, ArduinoJson, WebSockets +- Firmware (PlatformIO, `espressif8266` platform): ArduinoJson, WebSockets — declared in `platformio.ini`, resolved automatically - Node: Vue 3, Vite, Nuxt UI, Tailwind CSS v4, TypeScript diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d919036..0233664 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,9 +29,9 @@ Open `http://localhost:3000` in your browser. The throttle UI is fully functiona 1. Copy the secrets template: ```bash - cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h + cp secrets.ini.example secrets.ini ``` -2. Edit `firmware/z-duino/arduino_secrets.h` with your WiFi credentials. +2. Edit `secrets.ini` with your WiFi credentials. These are PlatformIO `build_flags` merged in via `extra_configs` — gitignored, never committed. 3. Compile: ```bash @@ -48,11 +48,11 @@ Open `http://localhost:3000` in your browser. The throttle UI is fully functiona ``` z-duino/ ├── platformio.ini # PlatformIO project config (board, ldscript, lib_deps) +├── secrets.ini.example # WiFi creds template — copy to secrets.ini (gitignored) ├── firmware/z-duino/ │ ├── z-duino.ino # Main sketch (WiFi, mDNS, HTTP, WebSocket) │ ├── Motor.h / Motor.cpp # TB6612FNG motor driver abstraction -│ ├── StatusLED.h / StatusLED.cpp # RGB status LED abstraction -│ └── arduino_secrets.h.example +│ └── StatusLED.h / StatusLED.cpp # RGB status LED abstraction ├── frontend/ │ ├── src/ │ │ ├── App.vue # Root component diff --git a/README.md b/README.md index 5468130..0c490b2 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ Wemos D1 Mini (ESP8266) → TB6612FNG H-bridge → track, plus an RGB status LED ```bash git clone https://github.com/yamanote1138/z-duino.git cd z-duino - cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h - # edit arduino_secrets.h with your WiFi SSID/password + cp secrets.ini.example secrets.ini + # edit secrets.ini with your WiFi SSID/password ``` 3. Install frontend dependencies and build + flash everything: ```bash diff --git a/build.sh b/build.sh index e39cbae..1b11641 100755 --- a/build.sh +++ b/build.sh @@ -65,6 +65,21 @@ build_frontend() { success "Frontend built → $DATA_DIR/" } +frontend_needs_build() { + [ ! -f "$DATA_DIR/index.html" ] && return 0 + find "$FRONTEND_DIR/src" "$FRONTEND_DIR/public" "$FRONTEND_DIR/index.html" \ + "$FRONTEND_DIR/vite.config.ts" "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/package-lock.json" \ + -type f -newer "$DATA_DIR/index.html" 2>/dev/null | grep -q . +} + +build_frontend_if_needed() { + if frontend_needs_build; then + build_frontend + else + info "Frontend unchanged, skipping build ($DATA_DIR/ is up to date)" + fi +} + build_firmware() { check_deps info "Compiling firmware..." @@ -118,13 +133,13 @@ read -rp "Select option [7]: " option option=${option:-7} case $option in - 1) build_frontend; build_firmware ;; + 1) build_frontend_if_needed; build_firmware ;; 2) build_frontend ;; 3) build_firmware ;; 4) upload_firmware ;; 5) upload_littlefs ;; 6) upload_firmware; upload_littlefs ;; - 7) build_frontend; build_firmware; upload_firmware; upload_littlefs ;; + 7) build_frontend_if_needed; build_firmware; upload_firmware; upload_littlefs ;; *) error "Invalid option" ;; esac diff --git a/docs/BUILD.md b/docs/BUILD.md index 2aeeba9..aec1c41 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -36,9 +36,9 @@ node --version # v18+ required 2. **Configure WiFi credentials:** ```bash - cp firmware/z-duino/arduino_secrets.h.example firmware/z-duino/arduino_secrets.h + cp secrets.ini.example secrets.ini ``` - Edit `arduino_secrets.h` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). + Edit `secrets.ini` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). These are plain PlatformIO `build_flags` (`-D WIFI_SSID=\"...\"` etc.) merged in via `extra_configs` in `platformio.ini` — `secrets.ini` is gitignored, so nothing here ever gets committed. 3. **Install frontend dependencies:** ```bash @@ -70,6 +70,8 @@ node --version # v18+ required | 7 | Build + Upload All | Under the hood, `build.sh` wraps: +- **Build Frontend** (option 2) → `npm run build`, always runs unconditionally +- **Build All / Build + Upload All** (options 1, 7) → skip the frontend build automatically if nothing under `frontend/` (source, `public/`, `package.json`/`package-lock.json`, `vite.config.ts`) is newer than the existing `build/data/`, since PlatformIO's own incremental firmware compile already makes re-running `pio run` on unchanged firmware near-instant — only the frontend build previously had no such skip - **Build Firmware** → `pio run` - **Upload Firmware** → `pio run -t upload` - **Upload LittleFS** → `pio run -t uploadfs` (packs `build/data/` into a LittleFS image sized from `platformio.ini`'s `board_build.ldscript`, then flashes it — see [LITTLEFS.md](LITTLEFS.md) for the partition math) diff --git a/firmware/z-duino/arduino_secrets.h.example b/firmware/z-duino/arduino_secrets.h.example deleted file mode 100644 index 74d6356..0000000 --- a/firmware/z-duino/arduino_secrets.h.example +++ /dev/null @@ -1,16 +0,0 @@ -// WiFi credentials (required) -// The # is part of the syntax — do not remove it! -#define WIFI_SSID "YourSSIDHere" -#define WIFI_PASS "YourPasswordHere" - -// mDNS hostname — your device will be reachable at http://.local -#define MDNS_HOSTNAME "ztrain" - -// Display name shown in the throttle UI header and browser tab -#define RAILROAD_NAME "Z-Duino" - -// Maximum PWM output (0–1000). Controls the top speed of the train. -// 1000 = full 12V, 500 = ~6V, etc. Start low and increase to taste. -#define MAX_PWM 500 - -// WIFI_SSID and WIFI_PASS are required. The rest have sensible defaults. diff --git a/firmware/z-duino/z-duino.ino b/firmware/z-duino/z-duino.ino index d557931..0acdc1d 100644 --- a/firmware/z-duino/z-duino.ino +++ b/firmware/z-duino/z-duino.ino @@ -6,7 +6,6 @@ #include #include -#include "arduino_secrets.h" #include "Motor.h" #include "StatusLED.h" diff --git a/platformio.ini b/platformio.ini index 4bce0ba..bfa1a25 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,7 @@ [platformio] src_dir = firmware/z-duino data_dir = build/data +extra_configs = secrets.ini [env:d1_mini] platform = espressif8266 diff --git a/secrets.ini.example b/secrets.ini.example new file mode 100644 index 0000000..54988f5 --- /dev/null +++ b/secrets.ini.example @@ -0,0 +1,13 @@ +; Copy this file to secrets.ini and fill in your values. +; secrets.ini is gitignored — never commit real credentials. +; +; Values are quoted as '"..."' (single quotes wrapping a double-quoted +; string) so PlatformIO's flag tokenizer keeps multi-word values (e.g. +; a railroad name with spaces) as a single argument. +[env] +build_flags = + -D WIFI_SSID='"YourSSIDHere"' + -D WIFI_PASS='"YourPasswordHere"' + -D MDNS_HOSTNAME='"ztrain"' + -D RAILROAD_NAME='"Z-Duino"' + -D MAX_PWM=500 From e146dae599a847ee06313f77ee800ed3ea2f0775 Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 22:31:33 -0600 Subject: [PATCH 4/9] Fix stale doc references left over from the PlatformIO/secrets.ini migration - docs/LITTLEFS.md: replace FQBN/arduino-cli framing with the actual board_build.ldscript mechanism; fix the "find your linker script" instructions (arduino-cli command no longer applies) - docs/BUILD.md: correct the secrets.ini build_flags example to the working '"..."' quoting (was still showing the broken \"...\" form) - CONTRIBUTING.md: de-duplicate the WebSocket protocol table in favor of linking to docs/PROTOCOL.md, the single source of truth --- CONTRIBUTING.md | 21 +-------------------- docs/BUILD.md | 6 +++--- docs/LITTLEFS.md | 10 +++++----- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0233664..28abd91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,26 +84,7 @@ z-duino/ ### WebSocket Protocol -The device runs a WebSocket server on port 81 with the `arduino` subprotocol. - -**Client → Device:** -| Command | Payload | -|---|---| -| Set speed | `{"cmd": "speed", "value": 0.0}` (0.0–1.0) | -| Set direction | `{"cmd": "direction", "value": true}` (true=fwd) | -| Stop | `{"cmd": "stop"}` | -| Keepalive | `{"cmd": "ping"}` | -| Direction invert | `{"cmd": "invert", "value": true}` | -| LED test mode | `{"cmd": "led", "r": 0, "g": 0, "b": 1000}` (0–1000) | -| Resume status LED | `{"cmd": "led_auto"}` | - -**Device → Client:** -| Message | Payload | -|---|---| -| Status update | `{"type": "status", "name": "...", "speed": 0.0, "direction": true, "connected": true}` | -| Pong | `{"type": "pong"}` | - -The mock server (`frontend/mock-server.ts`) implements the same protocol. +See **[docs/PROTOCOL.md](docs/PROTOCOL.md)** for the full spec. The mock server (`frontend/mock-server.ts`) implements the same protocol. ### Submitting Changes diff --git a/docs/BUILD.md b/docs/BUILD.md index aec1c41..ff9177a 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -22,8 +22,8 @@ That's it — the ESP8266 platform, toolchain, `mklittlefs`, `esptool`, and the ### Verify Installation ```bash -pio --version # should print version info -node --version # v18+ required +pio --version # should print version info +node --version # v18+ required ``` ## Setup @@ -38,7 +38,7 @@ node --version # v18+ required ```bash cp secrets.ini.example secrets.ini ``` - Edit `secrets.ini` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). These are plain PlatformIO `build_flags` (`-D WIFI_SSID=\"...\"` etc.) merged in via `extra_configs` in `platformio.ini` — `secrets.ini` is gitignored, so nothing here ever gets committed. + Edit `secrets.ini` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`) and `MAX_PWM` to tune top speed (default: `500` — about half voltage to the track, which is plenty for Z-scale). These are plain PlatformIO `build_flags` (`-D WIFI_SSID='"..."'` etc. — note the single-quote wrapping, required so values with spaces survive PlatformIO's flag tokenizer) merged in via `extra_configs` in `platformio.ini` — `secrets.ini` is gitignored, so nothing here ever gets committed. 3. **Install frontend dependencies:** ```bash diff --git a/docs/LITTLEFS.md b/docs/LITTLEFS.md index 6d447e4..4e8279b 100644 --- a/docs/LITTLEFS.md +++ b/docs/LITTLEFS.md @@ -4,7 +4,7 @@ How the ESP8266 filesystem works on the Wemos D1 Mini with the 4M2M flash layout ## Flash Partition Layout -The FQBN option `eesz=4M2M` selects `eagle.flash.4m2m.ld`, which defines: +`platformio.ini` sets `board_build.ldscript = eagle.flash.4m2m.ld` (the same linker script the Arduino IDE's `eesz=4M2M` board-menu option selects), which defines: | Symbol | Value | Meaning | |---|---|---| @@ -102,14 +102,14 @@ The `mklittlefs` binary bundled with the ESP8266 Arduino core (v0.2.3) bundles i ## Other Flash Layouts -If you change the flash size option (e.g. `4M1M`, `4M3M`), the linker script and therefore all the values above will change. Always derive the mklittlefs parameters from the linker script for your selected layout: +If you change `board_build.ldscript` in `platformio.ini` (e.g. to `eagle.flash.4m1m.ld` or `eagle.flash.4m3m.ld`), the values above will change accordingly. Always derive the mklittlefs parameters from the linker script for your selected layout: ```bash -# Find your linker script -arduino-cli compile --show-properties ... | grep flash_ld +# Find the linker script PlatformIO is actually using +find ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/ld -iname "**" # Then check the values grep '_FS_' ``` -Or just print `FS_PHYS_SIZE` and `FS_PHYS_BLOCK` from the firmware at runtime — the definitive source of truth. +Or just print `FS_PHYS_SIZE` and `FS_PHYS_BLOCK` from the firmware at runtime — the definitive source of truth. (PlatformIO itself does exactly this automatically for `pio run -t buildfs`/`uploadfs` — see above.) From 8f8232ff18d0a2cc274ea565b5de41f27c276d7d Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 22:58:53 -0600 Subject: [PATCH 5/9] Firmware cleanup: fix stale comment, dedupe status JSON, silence switch warnings - z-duino.ino: fix comment referencing the old arduino_secrets.h - Extract buildStatusJson() so broadcastStatus() and the WStype_CONNECTED handler stop duplicating the same ~10-line JSON construction - Add default: break to both webSocketEvent's and StatusLED::setState's switches, eliminating all -Wswitch warnings on unhandled enum values - StatusLED now stores the active test color and handles LED_TEST in setState(), fixing a real (if minor) bug: previously, triggering an emergency stop while in LED test mode left the LED dark afterward instead of restoring the test color, since setState(LED_TEST) hit no matching case Verified: pio run compiles with zero warnings; reflashed to hardware, confirmed boot/WiFi/mDNS unaffected and the WebSocket protocol (connect status, ping/pong, speed, stop) behaves identically to before. --- firmware/z-duino/StatusLED.cpp | 11 ++++++++++- firmware/z-duino/StatusLED.h | 3 +++ firmware/z-duino/z-duino.ino | 22 +++++++++++----------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/firmware/z-duino/StatusLED.cpp b/firmware/z-duino/StatusLED.cpp index e2074f9..ca478d0 100644 --- a/firmware/z-duino/StatusLED.cpp +++ b/firmware/z-duino/StatusLED.cpp @@ -2,7 +2,8 @@ StatusLED::StatusLED(int pinR, int pinG, int pinB) : _pinR(pinR), _pinG(pinG), _pinB(pinB), _state(WIFI_CONNECTING), - _previousState(IDLE), _blinkCount(0), _blinkStartTime(0) + _previousState(IDLE), _blinkCount(0), _blinkStartTime(0), + _testR(0), _testG(0), _testB(0) { pinMode(_pinR, OUTPUT); pinMode(_pinG, OUTPUT); @@ -36,14 +37,22 @@ void StatusLED::setState(LEDState state) { case ACTIVE_REVERSE: setColor(1000, 0, 0); // Red break; + case LED_TEST: + setColor(_testR, _testG, _testB); + break; case EMERGENCY_STOP_BLINK: // Handled in update() break; + default: + break; } } void StatusLED::setTestColor(int r, int g, int b) { _state = LED_TEST; + _testR = r; + _testG = g; + _testB = b; setColor(r, g, b); } diff --git a/firmware/z-duino/StatusLED.h b/firmware/z-duino/StatusLED.h index dfdf261..4a43a0a 100644 --- a/firmware/z-duino/StatusLED.h +++ b/firmware/z-duino/StatusLED.h @@ -29,6 +29,9 @@ class StatusLED { LEDState _previousState; int _blinkCount; unsigned long _blinkStartTime; + int _testR; + int _testG; + int _testB; }; #endif diff --git a/firmware/z-duino/z-duino.ino b/firmware/z-duino/z-duino.ino index 0acdc1d..ff4fc89 100644 --- a/firmware/z-duino/z-duino.ino +++ b/firmware/z-duino/z-duino.ino @@ -11,7 +11,7 @@ #define VERSION "2.3.0" -// WiFi credentials from arduino_secrets.h +// WiFi credentials from secrets.ini char wifi_ssid[] = WIFI_SSID; char wifi_pass[] = WIFI_PASS; char mdns_hostname[] = MDNS_HOSTNAME; @@ -66,7 +66,7 @@ void applyMotorState() { } } -void broadcastStatus() { +String buildStatusJson() { JsonDocument doc; doc["type"] = "status"; doc["name"] = railroad_name; @@ -76,6 +76,11 @@ void broadcastStatus() { String json; serializeJson(doc, json); + return json; +} + +void broadcastStatus() { + String json = buildStatusJson(); socket.broadcastTXT(json); } @@ -150,15 +155,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) connectedClients++; applyLedStatus(); { - JsonDocument doc; - doc["type"] = "status"; - doc["name"] = railroad_name; - doc["speed"] = currentSpeed; - doc["direction"] = currentDirection; - doc["connected"] = true; - - String json; - serializeJson(doc, json); + String json = buildStatusJson(); socket.sendTXT(num, json); } break; @@ -166,6 +163,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) case WStype_TEXT: handleWebSocketMessage(num, payload, length); break; + + default: + break; } } From 993338ddffcbbf6cb7c34babcf41e1c7f02a7fab Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Thu, 16 Jul 2026 22:59:03 -0600 Subject: [PATCH 6/9] Frontend cleanup: dead tsconfig alias, merged import, Promise-based rampTo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tsconfig.json: remove the @/* path alias — nothing in the codebase uses it, every import is relative - SpeedController.vue: merge two separate import statements from the same useTrainController module into one - useTrainController.ts: rampTo() now returns a Promise that resolves when its own ramp completes (naturally or via emergency stop), replacing waitForRampComplete()'s 50ms polling loop that watched the shared rampTimer variable. toggleDirection() awaits rampTo() directly. Note: investigated removing the unused `vue-router` dependency (nothing in the app imports it) but reverted — @nuxt/ui's Link.vue override component still references it internally, and Vite's optional-peer-dep stubbing doesn't cleanly resolve its named imports (useRoute, RouterLink) during a production build with this @nuxt/ui/Vite version combo. Kept as a real (if only build-time) dependency. Verified: npm run build succeeds; exercised rampTo/toggleDirection at runtime via a stubbed-WebSocket harness (no test framework in this repo) — ramp-down, pause, direction flip, and ramp-up all sequence identically to the pre-refactor behavior, with the promise resolving at the correct point (ramp-down + DIRECTION_PAUSE). --- frontend/src/components/SpeedController.vue | 3 +- .../src/composables/useTrainController.ts | 70 ++++++++----------- frontend/tsconfig.json | 5 +- 3 files changed, 33 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/SpeedController.vue b/frontend/src/components/SpeedController.vue index ad801d1..c66d92c 100644 --- a/frontend/src/components/SpeedController.vue +++ b/frontend/src/components/SpeedController.vue @@ -55,8 +55,7 @@