From b6b5ca1d550e3a11b3441f526945d380d36286b9 Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Fri, 17 Jul 2026 16:14:27 -0600 Subject: [PATCH 1/3] Make PWM frequency configurable, raise default max duty, warn on high track voltage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Motor PWM frequency now comes from PWM_FREQ in secrets.ini instead of a hardcoded 20kHz, following the existing MAX_PWM build-flag pattern. - Add a compile-time ceiling (#error above 40kHz) since analogWrite on ESP8266 is software PWM sharing one timer across all 4 active channels (motor + R/G/B status LED) — too high starves the WiFi/WebSocket stack. - Bump MAX_PWM 500 -> 1000 in secrets.ini.example for full duty resolution. - Debug panel: bold/red Track Voltage when it exceeds 10V. --- firmware/z-duino/z-duino.ino | 12 ++++++++++-- frontend/src/components/DebugModal.vue | 4 ++-- frontend/src/composables/useTrainController.ts | 5 +++-- secrets.ini.example | 3 ++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/firmware/z-duino/z-duino.ino b/firmware/z-duino/z-duino.ino index 59c5b1a..3478be9 100644 --- a/firmware/z-duino/z-duino.ino +++ b/firmware/z-duino/z-duino.ino @@ -13,6 +13,14 @@ #define VERSION "2.3.0" +// analogWrite on ESP8266 is software PWM sharing one hardware timer across +// all 4 active channels (motor + R/G/B status LED). Above ~40kHz the +// interrupt load starts contending with the WiFi/WebSocket stack, risking +// jitter and dropped connections. +#if PWM_FREQ > 40000 +#error "PWM_FREQ exceeds the 40kHz safe ceiling for this board's shared software-PWM timer" +#endif + // WiFi credentials from secrets.ini char wifi_ssid[] = WIFI_SSID; char wifi_pass[] = WIFI_PASS; @@ -186,8 +194,8 @@ void setup() { pinMode(stby, OUTPUT); digitalWrite(stby, HIGH); - // PWM config: 20kHz (above audible range), 0-1000 range - analogWriteFreq(20000); + // PWM config: frequency from secrets.ini (above audible range), 0-1000 range + analogWriteFreq(PWM_FREQ); analogWriteRange(1000); // Initialize LittleFS diff --git a/frontend/src/components/DebugModal.vue b/frontend/src/components/DebugModal.vue index 0df4d69..6c87358 100644 --- a/frontend/src/components/DebugModal.vue +++ b/frontend/src/components/DebugModal.vue @@ -19,7 +19,7 @@ :class="i % 2 === 0 ? 'bg-neutral-800/50' : ''" > {{ row.label }} - {{ row.value }} + {{ row.value }} @@ -34,7 +34,7 @@ import { useTrainController } from '../composables/useTrainController' const { getDebugInfo } = useTrainController() const isOpen = ref(false) -const rows = ref<{ label: string; value: string }[]>([]) +const rows = ref<{ label: string; value: string; danger?: boolean }[]>([]) function open() { rows.value = getDebugInfo() diff --git a/frontend/src/composables/useTrainController.ts b/frontend/src/composables/useTrainController.ts index bdc4570..581e782 100644 --- a/frontend/src/composables/useTrainController.ts +++ b/frontend/src/composables/useTrainController.ts @@ -347,9 +347,10 @@ function togglePartyMode() { const WS_READY_STATES = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const -function getDebugInfo(): { label: string; value: string }[] { +function getDebugInfo(): { label: string; value: string; danger?: boolean }[] { const wsUrl = socket.value ? socket.value.url : `ws://${location.hostname || 'localhost'}:81/` const wsState = socket.value ? WS_READY_STATES[socket.value.readyState] : 'N/A' + const trackVoltage = 12 * currentSpeed.value return [ { label: 'App Version', value: __APP_VERSION__ }, @@ -360,7 +361,7 @@ function getDebugInfo(): { label: string; value: string }[] { { label: 'Reconnect Delay', value: `${reconnectDelay}ms` }, { label: 'Current Speed', value: currentSpeed.value === 0 ? 'stopped' : `${Math.round(currentSpeed.value * 100)}%` }, { label: 'Supply Voltage', value: '12V' }, - { label: 'Track Voltage', value: `${(12 * currentSpeed.value).toFixed(1)}V` }, + { label: 'Track Voltage', value: `${trackVoltage.toFixed(1)}V`, danger: trackVoltage > 10 }, { label: 'Direction', value: currentDirection.value ? 'FWD' : 'REV' }, { label: 'Direction Inverted', value: String(directionInverted.value) }, { label: 'Ramping', value: String(rampTimer !== null) }, diff --git a/secrets.ini.example b/secrets.ini.example index 54988f5..1bcebf5 100644 --- a/secrets.ini.example +++ b/secrets.ini.example @@ -10,4 +10,5 @@ build_flags = -D WIFI_PASS='"YourPasswordHere"' -D MDNS_HOSTNAME='"ztrain"' -D RAILROAD_NAME='"Z-Duino"' - -D MAX_PWM=500 + -D MAX_PWM=1000 + -D PWM_FREQ=20000 From 1ef144fb5bb5102010d4cfa966f80a143f3ead64 Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Fri, 17 Jul 2026 16:19:37 -0600 Subject: [PATCH 2/3] Bump to v2.4.0, document PWM_FREQ and MAX_PWM in BUILD.md --- docs/BUILD.md | 2 +- firmware/z-duino/z-duino.ino | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/BUILD.md b/docs/BUILD.md index ff9177a..b97df58 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -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. — 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. + Edit `secrets.ini` with your WiFi SSID and password. Optionally change the mDNS hostname (default: `ztrain`), `MAX_PWM` to tune top speed (default: `1000` — full voltage to the track), and `PWM_FREQ` to tune the motor's PWM switching frequency in Hz (default: `20000` — above the audible range, so the motor doesn't whine). `analogWrite` on the ESP8266 is software PWM sharing one hardware timer across all 4 active channels (motor + R/G/B status LED), so pushing `PWM_FREQ` too high risks starving the WiFi/WebSocket stack — the build fails outright above `40000` as a safety ceiling. 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/firmware/z-duino/z-duino.ino b/firmware/z-duino/z-duino.ino index 3478be9..9f85c1b 100644 --- a/firmware/z-duino/z-duino.ino +++ b/firmware/z-duino/z-duino.ino @@ -11,7 +11,7 @@ #include #include -#define VERSION "2.3.0" +#define VERSION "2.4.0" // analogWrite on ESP8266 is software PWM sharing one hardware timer across // all 4 active channels (motor + R/G/B status LED). Above ~40kHz the diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 28ea4e0..20a9b5f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "z-duino-frontend", - "version": "2.3.0", + "version": "2.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "z-duino-frontend", - "version": "2.3.0", + "version": "2.4.0", "dependencies": { "@nuxt/ui": "^4.5.1", "tailwindcss": "^4.2.1", diff --git a/frontend/package.json b/frontend/package.json index 2000aa0..c5ee93a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "z-duino-frontend", - "version": "2.3.0", + "version": "2.4.0", "private": true, "type": "module", "scripts": { From f0c64a9d8c42a92abaa98f127a44a35679d8ed5e Mon Sep 17 00:00:00 2001 From: Chad Francis Date: Fri, 17 Jul 2026 16:23:41 -0600 Subject: [PATCH 3/3] Show track voltage warning in main throttle UI, not just debug panel Most users won't open the debug view unless something's already wrong. Extracted trackVoltage/trackVoltageDanger as shared computeds in useTrainController so the "Track:" readout in SpeedController and the debug panel's row both derive from one source of truth instead of each reimplementing the 12V * speed and >10V threshold logic. --- frontend/src/components/SpeedController.vue | 4 +++- frontend/src/composables/useTrainController.ts | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/SpeedController.vue b/frontend/src/components/SpeedController.vue index c66d92c..06ab812 100644 --- a/frontend/src/components/SpeedController.vue +++ b/frontend/src/components/SpeedController.vue @@ -4,7 +4,7 @@
Speed: {{ currentSpeed === 0 ? 'stopped' : Math.round(currentSpeed * 100) + '%' }} - Track: {{ (12 * currentSpeed).toFixed(1) }}V + Track: {{ trackVoltage.toFixed(1) }}V