Skip to content

Add complete WiThrottle portable throttle example for M5StickC Plus 2#1

Open
ismangil wants to merge 14 commits into
flash62au:masterfrom
ismangil:claude/m5stick-portable-throttle-GAOTk
Open

Add complete WiThrottle portable throttle example for M5StickC Plus 2#1
ismangil wants to merge 14 commits into
flash62au:masterfrom
ismangil:claude/m5stick-portable-throttle-GAOTk

Conversation

@ismangil
Copy link
Copy Markdown

Summary

This PR adds a complete, production-ready example sketch demonstrating the WiThrottleProtocol library on an M5StickC Plus 2 microcontroller with an M5Stack MiniEncoderC HAT. The example implements a fully functional portable DCC throttle with WiFi provisioning, server discovery, roster management, and real-time locomotive control.

Key Changes

  • Main sketch (WiThrottleProtocol_M5StickCPlus2.ino): Complete state machine implementing the throttle application with:

    • Boot → Provisioning → WiFi Connect → Server Discovery → Roster Selection → Drive workflow
    • Reconnection ladder with exponential backoff when server connection drops
    • Idle/dimming display management
    • Per-device preference storage (polarity, last locomotive)
  • AppDelegate (AppDelegate.h): WiThrottleProtocolDelegate subclass that:

    • Buffers roster entries from the server
    • Mirrors locomotive state (speed, direction, function states)
    • Provides thread-safe dirty flags for UI updates
  • EncoderHat (EncoderHat.h/cpp): I2C driver for the MiniEncoderC HAT exposing:

    • Rotary encoder with acceleration (slow/fast step multipliers based on rotation speed)
    • Debounced push-button with short/long-press detection
    • RGB LED control
  • Provision (Provision.h/cpp): Captive-portal WiFi setup with:

    • SoftAP mode with DNS catch-all for automatic portal detection
    • Web form for SSID selection, password entry, and optional manual server configuration
    • NVS persistence via Preferences API
  • UI (UI.h/cpp): TFT rendering layer providing:

    • Provisioning screen with AP details
    • Roster picker with scrolling
    • Drive view with centre-zero bipolar throttle slider, direction indicator, and function buttons
    • Function grid (F1–F12) with selection highlighting
    • Status screen showing WiFi, server, and device information
  • config.h: Compile-time configuration including:

    • HAT I2C bus pins and register map
    • Throttle behavior parameters (max speed, step sizes, acceleration window)
    • WiFi/server timeouts and reconnection backoff schedule
    • Display brightness and idle timeout settings
  • README.md: Complete build and usage documentation covering hardware setup, toolchain configuration (Arduino IDE and PlatformIO), first-boot provisioning flow, and driving controls.

Notable Implementation Details

  • Centre-zero throttle: Signed position in [-126..+126] with automatic snap-through-zero to prevent fighting the encoder at direction changes
  • Polarity flip: Per-device setting (stored in NVS) to invert throttle direction without changing sign convention
  • Acceleration: Encoder rotation speed is detected; quick spins use 4× step multiplier vs. slow turns
  • Roster persistence: Last selected locomotive address is saved and can be auto-acquired on reconnect
  • mDNS discovery: Automatic JMRI server discovery with manual host/port override option
  • Graceful degradation: Reconnection ladder with exponential backoff (1s → 2s → 5s → 10s → 30s) when server disappears
  • Battery monitoring: Real-time battery percentage display via M5Power API
  • Function support: F0 on BtnA (short press), F1–F12 in a 4×3 grid accessible via BtnB
  • E-stop: Encoder short press triggers emergency stop; long press is a gentle centre ramp

Co-authored by Claude Code

claude and others added 14 commits May 13, 2026 18:03
New examples/WiThrottleProtocol_M5StickCPlus2 sketch demonstrating
WiThrottleProtocol on an M5StickC Plus 2 with the M5Stack ENCODER HAT.

The encoder drives a centre-zero bipolar slider: clockwise from zero adds
forward speed, counter-clockwise adds reverse, and zero-crossings issue an
automatic setSpeed(0)/setDirection() sequence so the JMRI side stays in
step. The sketch also includes captive-portal WiFi provisioning, mDNS
server discovery, an on-device roster picker, F0-F12 controls and a
reconnect ladder.

No changes to the library itself.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
The M5StickC Plus 2 portable throttle example previously assumed the older
M5Stack ENCODER HAT (A031, I2C 0x5E). Switch the I2C address, register map
and LED byte order to match the MiniEncoderC HAT (SKU U157):

  * Address now 0x42
  * Incremental counter still int32 little-endian at 0x10
  * Absolute counter (int32) at 0x00 and reset register at 0x40 documented
  * Single RGB LED at 0x30, BGR byte order
  * setLeds(left, right) replaced with setLed(rgb888)

Updated README, splash text and source comments to reference U157.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Rotate the centre-zero slider 90 degrees so it runs along the right edge of
the TFT and grows upward for forward speed, downward for reverse. The big
signed speed number, direction arrow and F0-F9 indicators reflow into the
left content area. The slider's centre tick now runs horizontally across
the bar.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Set the TFT rotation to 0 (portrait) and swap TFT_W/TFT_H so the existing
layouts treat the display as 135 wide x 240 tall. The vertical slider on
the drive view now has the full portrait height to grow into.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Replace the cramped landscape-leaning UI with portrait-friendly layouts:

* Title bar is now 22 px tall with size-2 left text and size-1 right text;
  drawHeader truncates the right side to ~60% of the bar and then trims
  the left if needed so the loco name always shows.
* Add drawWrappedCentered/truncateToWidth helpers so any text that does
  not fit the 135 px portrait width wraps at word boundaries instead of
  clipping. Splash, provisioning and reconnect screens use it.
* Roster rows are 26 px tall with size-2 name/address and an
  ellipsis-by-truncation when the name is too long.
* Drive view: speed number dropped to size 3 to fit the ~95 px content
  column next to the slider; right-side status replaced with battery
  percentage (link state is implicit because outages drop to Reconnect).
* Functions reflows to a 4x3 portrait grid; status screen stacks label
  above value because labels and values no longer fit side by side.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Add updateThrottleLed() that drives the MiniEncoderC's RGB LED:
green when moving forward, red when moving in reverse, off at centre.

Called from applyDelta, the e-stop / soft-centre paths in tickDrive,
absorbMirroredState (so external speed changes from JMRI track too),
and enterDrive. The LED is explicitly turned off when entering Roster
or Reconnect since the throttle is not actively driving in those
states.

Uses dim levels (0x002000 / 0x200000) because the on-board LED is
otherwise unpleasantly bright.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
The MiniEncoderC firmware actually expects bytes in RGB order on the
wire (high byte of 0xRRGGBB first), not BGR as I'd mis-noted from the
upstream M5Unit driver. With BGR ordering the high byte was landing
in the blue channel, so dim-red (0x200000) lit up as dim-blue while
green still worked (G is the middle byte either way).

Rename ENCODER_REG_LED_BGR -> ENCODER_REG_LED and update the comment
in config.h to match.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Restructure the example README with a numbered Getting started flow that
walks from a bare M5StickC Plus 2 to driving a loco: install toolchain,
flash firmware, captive-portal WiFi setup, start JMRI's WiThrottle
server, pick a loco. Add a Troubleshooting section covering the most
common first-run failures (HAT not detected, 5 GHz/ch-11 WiFi, JMRI not
reachable).

Also fix a few stale details: TFT dimensions now say 135x240 (portrait),
the PlatformIO board id points to m5stick-c-plus2 with a fallback for
older platform-espressif32 versions, and the Controls table calls out
the encoder LED feedback added in earlier commits.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Insert a new Layout state between Functions and Status in the BtnB
cycle. The screen has two tabs (Turnouts, Routes) selected by encoder
long-press, with a scrollable size-2 list per tab.

Controls on Layout:
* Encoder rotate moves the highlight within the active tab.
* Encoder short-press activates the highlighted item: turnouts call
  setTurnout(sysName, TurnoutToggle) with an optimistic local flip
  (server confirms via receivedTurnoutAction); routes call
  setRoute(sysName) and flash a "Route activated" banner.
* Encoder long-press flips the tab.
* BtnA returns to Drive, BtnB cycles to Status.

AppDelegate gains TurnoutEntry/RouteEntry buffers and overrides for
receivedTurnoutEntries/Entry/Action plus the route equivalents; the
library does not keep these vectors for us.

README updates the controls table with the Layout row group and step 5
mentions the new screen.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Two issues seen on hardware: the Turnouts list arrived empty, and the
tab toggle to Routes was undiscoverable.

Cause for the empty list: the library dispatches the turnout/route
callbacks in the opposite order to the roster. For roster, the count
arrives first then entries; for turnouts and routes, each entry arrives
first and the count arrives last (PTL / PRL parsing in
WiThrottleProtocol.cpp). The delegate was clearing on the count call,
which wiped every entry that had just been appended.

Clear the vector when index == 0 of a fresh list (the natural "start of
a new list" signal) and treat receivedTurnoutEntries/receivedRouteEntries
as the "list finished" marker instead.

UX: the active tab now shows a leading ">" marker and a footer line
"hold push: switch tab" anchored to the bottom of the Layout screen, so
the encoder long-press toggle is discoverable even when the current tab
is empty.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
User reported it was too easy to slip onto the wrong loco while
reaching for the encoder push to confirm.

UI side: bump roster row height from 26 to 32 with 6 px of true gap
between rows; add a 5 px left-edge accent stripe on the selected row so
the selection is unmistakable; draw thin dividers between unselected
rows.

Input side: accumulate encoder detents and require two of them to move
the selection by one row. Single accidental nudges no longer change
the highlight. Reset the accumulator on every (re)entry to the picker.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
M5Stack's M5StickS3 shares the Plus 2's TFT (135x240 ST7789), the
8-pin HAT pinout (I2C on GPIO 0/26), and the M5Unified power/button
APIs. A single source builds for either Stick — the only changes
needed are cosmetic strings that no longer hardcode "Plus 2" plus a
documented PlatformIO env for the S3.

Sketch: generalise the file header and drop the Plus 2 model name out
of the boot splash and the WiThrottle device name prefix (now "M5Stick"
/ "M5Stick-<last4>"). The MAC suffix already disambiguates devices on
the JMRI client list.

README: title and intro mention both Sticks; the hardware table
acquires an M5StickS3 row; the PlatformIO section adds an
[env:m5sticks3] block alongside the plus2 one, with a note on board id
fallbacks; the Arduino IDE steps mention both board entries.

Folder name stays as WiThrottleProtocol_M5StickCPlus2 to avoid
breaking the merged PR and any external references — the README notes
the historical name covers both devices.

https://claude.ai/code/session_01VGvLbCWkXXnuCF6YNxPoR4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants