From cd0b7b0780fc6d74d4041dd8fb94da31a4fa5ec3 Mon Sep 17 00:00:00 2001 From: Greg Hanefeld Date: Tue, 1 Sep 2026 01:08:00 -0700 Subject: [PATCH 1/2] Add explicit local Dig2Go propagation trigger --- .../dig2go_peer_propagation_test.cpp | 17 ++++++++++++ usermods/Tubes/controller.h | 26 ++++++++++++++++++- usermods/Tubes/docs/PROTOCOL.md | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/test/tubes_mesh/dig2go_peer_propagation_test.cpp b/test/tubes_mesh/dig2go_peer_propagation_test.cpp index 2443498820..79731cc584 100644 --- a/test/tubes_mesh/dig2go_peer_propagation_test.cpp +++ b/test/tubes_mesh/dig2go_peer_propagation_test.cpp @@ -39,11 +39,27 @@ static void explicitPropagationCommandIsSeparateFromOtaSelection() { static void barePowerSaveCommandRemainsIntact() { const std::string controller = readSource("usermods/Tubes/controller.h"); + EXPECT(controller.find("key == 'P' && command[1] == '!' && !command[2]") + != std::string::npos); + EXPECT(controller.find("requestLocalPropagation();") != std::string::npos); EXPECT(controller.find("key == 'P' && strchr(command + 1, ',')") != std::string::npos); EXPECT(controller.find("else if (key == 'P')") != std::string::npos); } +static void localPropagationUsesTheExactExistingFleetOffer() { + const std::string controller = readSource("usermods/Tubes/controller.h"); + const auto begin = controller.find("void requestLocalPropagation()"); + const auto end = controller.find("void requestFleetUpdate", begin); + EXPECT(begin != std::string::npos && end != std::string::npos); + const std::string trigger = controller.substr(begin, end - begin); + EXPECT(trigger.find("makeModernPropagationServeCommand") != std::string::npos); + EXPECT(trigger.find("node.header.id") != std::string::npos); + EXPECT(trigger.find("applyCommand(COMMAND_FLEET_UPGRADE, &offer)") + != std::string::npos); + EXPECT(trigger.find("sendV3ControlCommand") == std::string::npos); +} + static void laptopFleetToolCannotStartPropagation() { const std::string tool = readSource("usermods/Tubes/fleet_pull_update.py"); EXPECT(tool.find("--propagate") == std::string::npos); @@ -129,6 +145,7 @@ static void failedPullKeepsRestoringUntilMeshIsStarted() { int main() { explicitPropagationCommandIsSeparateFromOtaSelection(); barePowerSaveCommandRemainsIntact(); + localPropagationUsesTheExactExistingFleetOffer(); laptopFleetToolCannotStartPropagation(); productionBuildHasNoBenchBootTriggers(); oneTurnAdvertisesToLegacyAndCurrentPeers(); diff --git a/usermods/Tubes/controller.h b/usermods/Tubes/controller.h index 87b5f1d172..3c7ac09b65 100644 --- a/usermods/Tubes/controller.h +++ b/usermods/Tubes/controller.h @@ -3572,7 +3572,7 @@ class PatternController : public MessageReceiver { // AI: below section was generated by an AI Serial.println(F("b###.# - set bpm\ns - start phrase\n\np### - preferred table pattern\npW,,,,,,,,,,,\npT,,,,\nm### - sync mode\nc### - colors\nc, - timed/held built-in palette\ne### - effects\nn - force next\n\ni### - set Control ID\nB/K/C### - set Beat/Pattern/Palette Channel ID\ng - become Palette Master\ng0:RRGGBB,...,255:RRGGBB - schedule a custom gradient\ngH,,0:RRGGBB,...,255:RRGGBB - timed/held gradient\nd0/1 - set local debugging\nD0/1 - set debugging on selected devices\nl### - brightness")); Serial.println(F("@ - set power saving mode\nU - begin auto-update\nP - toggle all power saves\nO - locally schedule sound toggle on Beat owner\nO0 - locally schedule sound off\nO1 - locally schedule rotating sound programs\nO### - locally schedule raw WLED effect on Beat owner\nO,,,,,,,,,[,] - locally schedule all overlay settings\nJ1/J0 - enter/exit audio workshop; J+/J- vote, J>/J< browse\nj0/1 - local automatic microphone tempo off/on\n==== wifi ====\na - turn on access point\nq - turn off access point\nt0/1 - Tubes mode off/on")); - Serial.println(F("==== selected actions ====\nD0/1 - set debugging\nU - begin auto-update\nX - restart\nf### - flash connected device\nF### - flash selected devices\nr/R### - set local/selected role\n==== mesh actions ====\n* - enter OTA select mode (double-click to Ready)\nP - explicitly start peer propagation\nA - turn on access point (Ready to update)\nW - forget WiFi client\nV### - auto-upgrade to version\nz - report all visible devices\nz############ - probe a device by MAC\nyhhhh - select one hexadecimal Device ID for update\n(hhhh/)hhhh - select/unselect a Device ID\nM - cancel manual pattern override")); + Serial.println(F("==== selected actions ====\nD0/1 - set debugging\nU - begin auto-update\nX - restart\nf### - flash connected device\nF### - flash selected devices\nr/R### - set local/selected role\n==== mesh actions ====\n* - enter OTA select mode (double-click to Ready)\nP! - make this connected Dig2Go serve its running image\nP - explicitly start peer propagation\nA - turn on access point (Ready to update)\nW - forget WiFi client\nV### - auto-upgrade to version\nz - report all visible devices\nz############ - probe a device by MAC\nyhhhh - select one hexadecimal Device ID for update\n(hhhh/)hhhh - select/unselect a Device ID\nM - cancel manual pattern override")); // AI: end } @@ -4585,6 +4585,10 @@ class PatternController : public MessageReceiver { // AI: below section was generated by an AI // Bare P is the deployed mesh power-save toggle. Only the structured, // comma-delimited P form belongs to the additive propagation command. + if (key == 'P' && command[1] == '!' && !command[2]) { + requestLocalPropagation(); + return; + } if (key == 'Y' || (key == 'P' && strchr(command + 1, ','))) { requestFleetUpdate(command + 1, key == 'P'); return; @@ -5003,6 +5007,26 @@ class PatternController : public MessageReceiver { } // AI: below section was generated by an AI + // Explicit USB/Electron action: ask only this connected Dig2Go to serve its + // running image through the existing exact-target FleetUpdateOffer path. + // Bare P remains the deployed power-save toggle; structured P remains + // the remote/operator form. + void requestLocalPropagation() { +#if TUBES_ENABLE_DIG2GO_PEER_PROPAGATION + FleetUpdateOffer offer; + uint32_t nonce = esp_random(); + if (!nonce) nonce = 1; + if (!makeModernPropagationServeCommand( + offer, RELEASE_VERSION, nonce, node.header.id)) { + Serial.println(F("TUBE_PROPAGATE_LOCAL rejected=invalid")); + return; + } + applyCommand(COMMAND_FLEET_UPGRADE, &offer); +#else + Serial.println(F("TUBE_PROPAGATE_LOCAL rejected=unsupported")); +#endif + } + // Parses one explicit LAN update offer and sends it through the Control tree. // Format: release,IPv4,port,start-window-ms,target-device-id,nonce,SSID,password. void requestFleetUpdate(char* text, bool propagate = false) { diff --git a/usermods/Tubes/docs/PROTOCOL.md b/usermods/Tubes/docs/PROTOCOL.md index fcddf4d4d1..e16b928657 100644 --- a/usermods/Tubes/docs/PROTOCOL.md +++ b/usermods/Tubes/docs/PROTOCOL.md @@ -1509,6 +1509,7 @@ activate field diagnostics as soon as it connects. | `y####` | Route an update-selection request to one four-digit hexadecimal Device ID. The matching device reports its stable MAC, then starts `WLED-UPDATE` without physical selection. | | `Y,,,,,,,` | Emit one gen1 parallel-pull offer. `target=0000` addresses every compatible pole; normal operation uses `fleet_pull_update.py` so secrets are not printed. | | `P` | Send an exact-target `Fleet Update Propagate` command. The chosen current Dig2Go immediately serves its running image; this never enters `WLED-UPDATE` selection or requires a button. | +| `P!` | Locally ask the USB-connected Dig2Go to serve its running image through the same exact-target `FleetUpdateOffer` path. This is the offline Easy Flash action; it does not broadcast a source-selection command. | | `O`, `O0`, `O1` | Locally ask the connected Beat owner to schedule toggle, disable, or rotating sound-overlay behavior. The resulting Beat state is the only wire message. | | `O,,,,,,,,,[,]` | Schedule an exact overlay on the connected Beat owner. The optional chance controls each pole's independent acceptance of an accent and defaults to 255 for the legacy ten-field form. | | `J1`, `J0` | Enter or leave the audio workshop. Entry holds a black base and makes the connected pole Beat Master; exit restores normal scheduled pattern, palette, and overlay behavior. | From 02e91e0d15fbdd1000583b25937a6a3a8aea0b24 Mon Sep 17 00:00:00 2001 From: Greg Hanefeld Date: Tue, 1 Sep 2026 02:38:39 -0700 Subject: [PATCH 2/2] Make propagation canonical in Dig2Go v52 --- platformio_tubes.ini | 16 +++------------- .../dig2go_peer_propagation_test.cpp | 4 +++- usermods/Tubes/DIG2GO_P2P_STEVE_HANDOFF.md | 19 ++++++++++--------- usermods/Tubes/updater.h | 2 +- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/platformio_tubes.ini b/platformio_tubes.ini index 6824d11305..b8994b42cd 100644 --- a/platformio_tubes.ini +++ b/platformio_tubes.ini @@ -107,6 +107,9 @@ build_flags = -D PIXEL_COUNTS=150 -D TUBES_HARDWARE_FAMILY=TubeHardwareDig2Go -D TUBES_FIRMWARE_VARIANT=TubeVariantStandard + -D TUBES_ENABLE_DIG2GO_PEER_PROPAGATION=1 + -D TUBES_DIG2GO_LEGACY_PULL_HOST=1 + -D TUBES_DIG2GO_DYNAMIC_ENROLLMENT=1 # OTA accepts only firmware from the same hardware family, so this identity # must override the generic ESP32 release inherited by the Dig2Go base build. -D WLED_RELEASE_NAME=\"DIG2GO_TUBES\" @@ -116,19 +119,6 @@ lib_ignore = lib_deps = ${env:esp32_quinled_dig2go.lib_deps} -# Explicitly triggered Dig2Go peer propagation. This carries the standard -# DIG2GO_TUBES identity and contains no bench auto-start, PRIME MAC, or -# test-only boot trigger. Its bounded production marker lets a just-migrated -# legacy receiver pass one baton; S3/Easy Flash start seed turns only after -# direct human input. -[env:esp32_quinled_dig2go_tubes_p2p] -extends = env:esp32_quinled_dig2go_tubes -build_flags = - ${env:esp32_quinled_dig2go_tubes.build_flags} - -D TUBES_ENABLE_DIG2GO_PEER_PROPAGATION=1 - -D TUBES_DIG2GO_LEGACY_PULL_HOST=1 - -D TUBES_DIG2GO_DYNAMIC_ENROLLMENT=1 - # Waveshare ESP32-S3-Touch-AMOLED-2.16 Tubes field target. DATA_PINS=255 keeps # WLED's generic one-pin config loader unchanged; BusTubesNull consumes the # target-scoped sentinel without allocating or touching a physical output. diff --git a/test/tubes_mesh/dig2go_peer_propagation_test.cpp b/test/tubes_mesh/dig2go_peer_propagation_test.cpp index 79731cc584..884ab10bfd 100644 --- a/test/tubes_mesh/dig2go_peer_propagation_test.cpp +++ b/test/tubes_mesh/dig2go_peer_propagation_test.cpp @@ -69,7 +69,7 @@ static void laptopFleetToolCannotStartPropagation() { static void productionBuildHasNoBenchBootTriggers() { const std::string config = readSource("platformio_tubes.ini"); - const auto begin = config.find("[env:esp32_quinled_dig2go_tubes_p2p]"); + const auto begin = config.find("[env:esp32_quinled_dig2go_tubes]"); const auto end = config.find("\n[env:", begin + 1); EXPECT(begin != std::string::npos && end != std::string::npos); const std::string environment = config.substr(begin, end - begin); @@ -79,6 +79,8 @@ static void productionBuildHasNoBenchBootTriggers() { EXPECT(environment.find("AUTO_TRIGGER") == std::string::npos); EXPECT(environment.find("PRIME_MAC") == std::string::npos); EXPECT(environment.find("BOOT_FALLBACK_TEST") == std::string::npos); + + EXPECT(config.find("[env:esp32_quinled_dig2go_tubes_p2p]") == std::string::npos); } static void oneTurnAdvertisesToLegacyAndCurrentPeers() { diff --git a/usermods/Tubes/DIG2GO_P2P_STEVE_HANDOFF.md b/usermods/Tubes/DIG2GO_P2P_STEVE_HANDOFF.md index c247ff4632..d7b51ee762 100644 --- a/usermods/Tubes/DIG2GO_P2P_STEVE_HANDOFF.md +++ b/usermods/Tubes/DIG2GO_P2P_STEVE_HANDOFF.md @@ -48,16 +48,18 @@ decision is local to the devices. The seed and its children discover eligible receivers, serve the image, persist continuation, recover, and stop without a laptop roster or server. -The production review environment is: +The canonical v52 Dig2Go environment is: ```sh -pio run -e esp32_quinled_dig2go_tubes_p2p +pio run -e esp32_quinled_dig2go_tubes ``` -It retains the standard `DIG2GO_TUBES` firmware identity. It enables the host -and dynamic Dig2Go enrollment, but contains no PRIME MAC, automatic source -trigger, or test-only boot fallback. It does retain the bounded production -first-boot marker described below for a just-migrated legacy receiver. +It retains the standard `DIG2GO_TUBES` firmware identity and now enables the +host and dynamic Dig2Go enrollment by default. A local `P!` command explicitly +starts one turn; merely carrying the capability does not advertise or serve. +The build contains no PRIME MAC, automatic source trigger, or test-only boot +fallback. The old `_p2p` environment is removed so v52 has one canonical +Dig2Go artifact and capability set. ## Evidence boundary @@ -98,8 +100,7 @@ or final S3/Easy Flash activation UX. bash test/tubes_mesh/run.sh node --test tools/fleet-update-protocol-test.js pio run -e esp32_quinled_dig2go_tubes -pio run -e esp32_quinled_dig2go_tubes_p2p ``` -The ordinary Dig2Go build remains a regression control with P2P disabled. -C3 family propagation and its device flow are intentionally deferred. +All canonical v52 Dig2Go images carry the same dormant propagation capability. +C3 family serving and its device flow are intentionally deferred. diff --git a/usermods/Tubes/updater.h b/usermods/Tubes/updater.h index 2069f7f374..abfd49cc43 100644 --- a/usermods/Tubes/updater.h +++ b/usermods/Tubes/updater.h @@ -12,7 +12,7 @@ #include "legacy_auto_update_wire.h" #ifndef RELEASE_VERSION -#define RELEASE_VERSION 51 +#define RELEASE_VERSION 52 #endif // AI: below section was generated by an AI