Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion platformio_tubes.ini
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ build_flags = ${env:esp32s3dev_16MB_opi.build_flags} ${tubes_no_mic.build_flags}
-D WLED_RELEASE_NAME=\"WAVESHARE_S3_TUBES_REMOTE\"
-D WAVESHARE_S3_TUBES_REMOTE
-D TUBES_S3_FIELD_OS
-D TUBES_HARDWARE_FAMILY=TubeHardwareMatrixM1
-D TUBES_NULL_OUTPUT
-D LED_TYPES=TYPE_TUBES_NULL
-D DATA_PINS=255
Expand All @@ -158,7 +159,7 @@ lib_deps = ${env:esp32s3dev_16MB_opi.lib_deps}
[env:waveshare_s3_tubes_carrier]
extends = env:waveshare_s3_tubes_remote
board_build.embed_files =
build_output/s3_vault/esp32_quinled_dig2go_tubes.bin
build_output/s3_vault/esp32_quinled_dig2go_tubes_p2p.bin
build_output/s3_vault/esp32-c3-athom_tubes.bin
build_unflags =
${env:waveshare_s3_tubes_remote.build_unflags}
Expand Down
2 changes: 1 addition & 1 deletion tools/build_s3_carrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
OTA_SLOT = 0x600000
REQUIRED_HEADROOM = 0x40000
PROFILES = (
("esp32_quinled_dig2go_tubes", "esp32_quinled_dig2go_tubes.bin", "dig2go"),
("esp32_quinled_dig2go_tubes_p2p", "esp32_quinled_dig2go_tubes_p2p.bin", "dig2go"),
("esp32-c3-athom_tubes", "esp32-c3-athom_tubes.bin", "athom-c3"),
)

Expand Down
18 changes: 18 additions & 0 deletions tools/fleet-update-protocol-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <set>

#include "../usermods/Tubes/fleet_update_protocol.h"
#include "../usermods/Tubes/modern_propagation_lease.h"

// AI: below section was generated by an AI

Expand Down Expand Up @@ -84,6 +85,23 @@ int main() {
}
require(delays.size() >= 45, "stable scheduling clustered fifty devices too tightly");

// The proven P2P seed is an exact-target, equal-release serve command with
// no OTA server, credentials, wildcard, or force flag.
FleetUpdateOffer propagation;
require(makeModernPropagationServeCommand(
propagation, 49, 0x10203040, 0x1234),
"exact propagation command was rejected");
require(propagation.flags == FleetUpdatePropagate,
"propagation command lost explicit opt-in");
require(propagation.tubesVersion == 49 && propagation.targetDeviceId == 0x1234,
"propagation command lost artifact or target identity");
require(propagation.serverPort == 0 && propagation.ssidLength == 0
&& propagation.passwordLength == 0,
"propagation command invented OTA transport or credentials");
propagation.targetDeviceId = 0;
require(!isValidFleetUpdateOffer(propagation),
"wildcard serve-current propagation was accepted");

printf("fleet update protocol scenarios passed\n");
return 0;
}
Expand Down
43 changes: 43 additions & 0 deletions tools/s3-carrier-runtime-contract-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const path = require('node:path');
const repository = path.resolve(__dirname, '..');
const source = fs.readFileSync(path.join(repository,
'usermods/WaveshareS3TubesRemote/S3FirmwareCarrier.cpp'), 'utf8');
const ui = fs.readFileSync(path.join(repository,
'usermods/WaveshareS3TubesRemote/WaveshareS3TubesRemote.cpp'), 'utf8');
const protocol = fs.readFileSync(path.join(repository,
'usermods/Tubes/fleet_update_protocol.h'), 'utf8');
const propagation = fs.readFileSync(path.join(repository,
'usermods/Tubes/modern_propagation_lease.h'), 'utf8');

it('serves the exact fleet pull endpoint with integrity headers', () => {
assert.match(source, /"\/tubes\/firmware\.bin"/);
Expand Down Expand Up @@ -50,3 +56,40 @@ it('arms explicitly and broadcasts Steve fleet offer vocabulary', () => {
assert.match(source, /WiFi\.softAPIP\(\)/);
assert.match(source, /WiFi\.softAPdisconnect\(true\)/);
});

it('seeds proven Dig2Go propagation only from explicit exact-artifact input', () => {
assert.match(ui, /DIG2GO \/ TAP TO SEED P2P/);
assert.match(ui, /artifact\.peerPropagation/);
assert.match(source, /artifact\.peerPropagation = true/);
assert.match(source, /artifact\.peerPropagation = false/);
assert.match(ui, /tubesS3SeedDig2GoPropagation/);
assert.match(ui, /isSeedableDig2GoTarget/);
assert.match(ui, /tubesS3ReadCarrierArtifact\(index, artifact\)/);
assert.match(ui, /artifact\.release == target\.release/);
assert.match(source, /target\.family == TubeHardwareDig2Go/);
assert.match(source, /target\.variant == TubeVariantStandard/);
assert.match(source, /target\.release == CARRIER_RELEASE/);
assert.match(source, /carrierCatalogReady/);
assert.match(source, /catalog\.select/);
assert.match(source, /embeddedSize != S3_VAULT_DIG2GO_SIZE/);
assert.match(source, /makeModernPropagationServeCommand/);
assert.match(source, /tubesS3BroadcastFleetOffer/);
assert.match(propagation, /command\.flags = FleetUpdatePropagate/);
assert.match(propagation, /command\.serverPort = 0/);
assert.match(propagation, /command\.targetDeviceId = targetDeviceId/);
assert.doesNotMatch(source, /MODERN_PROPAGATION_LEASE_PATH|CURRENT_RELEASE_MARKER_PATH/);
});

it('keeps current Dig2Go visible for seeding without admitting current C3 targets', () => {
assert.match(source, /report\.tubesVersion > CARRIER_RELEASE/);
assert.match(source, /report\.tubesVersion == CARRIER_RELEASE[\s\S]*report\.hardwareFamily != TubeHardwareDig2Go/);
assert.match(ui, /SEED REQUEST SENT/);
assert.match(ui, /SEED REQUEST FAILED/);
assert.doesNotMatch(ui, /propagationSeedSent = tubesS3SeedDig2GoPropagation[\s\S]{0,160}drawUpdateContent/);
});

it('keeps propagation seed commands out of the S3 receiver path', () => {
const controller = fs.readFileSync(path.join(repository,
'usermods/Tubes/controller.h'), 'utf8');
assert.match(controller, /#ifdef TUBES_S3_FIELD_OS[\s\S]*if \(offer\.flags & FleetUpdatePropagate\)[\s\S]*return true;[\s\S]*updater\.startFleet\(offer\)/);
});
3 changes: 2 additions & 1 deletion tools/s3-platformio-contract-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ test('Waveshare effective environment has unique artifact identity, one enabled
for (const library of ['Arduino_GFX', 'SensorLib', 'XPowersLib'])
assert.doesNotMatch(dependencies, new RegExp(`${library}\\.git#v\\d`));
assert.doesNotMatch(flags, /TUBES_S3_FIRMWARE_CARRIER/);
assert.match(flags, /TUBES_HARDWARE_FAMILY=TubeHardwareMatrixM1/);
assert.ok(!env['board_build.embed_files'], 'base S3 unexpectedly requires generated vault files');
const carrier = sections.get('env:waveshare_s3_tubes_carrier');
assert.ok(carrier, 'missing effective carrier environment');
const carrierFlags = [].concat(carrier.build_flags).join(' ');
assert.match(carrierFlags, /TUBES_S3_FIRMWARE_CARRIER/);
assert.match(carrierFlags, /WLED_RELEASE_NAME=\\"WAVESHARE_S3_TUBES_CARRIER\\"/);
assert.match([].concat(carrier['board_build.embed_files']).join(' '), /esp32_quinled_dig2go_tubes\.bin/);
assert.match([].concat(carrier['board_build.embed_files']).join(' '), /esp32_quinled_dig2go_tubes_p2p\.bin/);
assert.match([].concat(carrier['board_build.embed_files']).join(' '), /esp32-c3-athom_tubes\.bin/);
});
6 changes: 6 additions & 0 deletions usermods/Tubes/controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -4865,6 +4865,12 @@ class PatternController : public MessageReceiver {
offer.ssidLength, offer.passwordLength);
if (!valid)
return false;
#ifdef TUBES_S3_FIELD_OS
if (offer.flags & FleetUpdatePropagate) {
Serial.println(F("FLEET_RX propagation=rejected mode=s3_controller"));
return true;
}
#endif
const bool serveCurrent = (offer.flags & FleetUpdatePropagate)
&& offer.serverPort == 0;
const bool legacyBootstrapBaton = (offer.flags & FleetUpdatePropagate)
Expand Down
2 changes: 2 additions & 0 deletions usermods/Tubes/s3_field_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ struct TubesS3CarrierTarget {
struct TubesS3CarrierArtifact {
uint8_t family = 0;
uint8_t variant = 0;
bool peerPropagation = false;
uint16_t release = 0;
uint32_t size = 0;
};
Expand Down Expand Up @@ -103,3 +104,4 @@ size_t tubesS3CarrierTargetCount();
bool tubesS3ReadCarrierTarget(size_t index, TubesS3CarrierTarget &target);
size_t tubesS3CarrierArtifactCount();
bool tubesS3ReadCarrierArtifact(size_t index, TubesS3CarrierArtifact &artifact);
bool tubesS3SeedDig2GoPropagation(uint16_t nodeId);
48 changes: 45 additions & 3 deletions usermods/WaveshareS3TubesRemote/S3FirmwareCarrier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
#include "wled.h"
#include "../Tubes/s3_field_api.h"
#include "../Tubes/s3_firmware_vault.h"
#include "../Tubes/dig2go_peer_config.h"
#include "../Tubes/modern_propagation_lease.h"
#include "s3_vault_artifacts.h"

extern const uint8_t dig2goStart[] asm("_binary_build_output_s3_vault_esp32_quinled_dig2go_tubes_bin_start");
extern const uint8_t dig2goEnd[] asm("_binary_build_output_s3_vault_esp32_quinled_dig2go_tubes_bin_end");
#if TUBES_ENABLE_DIG2GO_PEER_PROPAGATION
#error "The S3 may seed Dig2Go propagation but must not enable the peer receiver/host"
#endif

extern const uint8_t dig2goStart[] asm("_binary_build_output_s3_vault_esp32_quinled_dig2go_tubes_p2p_bin_start");
extern const uint8_t dig2goEnd[] asm("_binary_build_output_s3_vault_esp32_quinled_dig2go_tubes_p2p_bin_end");
extern const uint8_t athomC3Start[] asm("_binary_build_output_s3_vault_esp32_c3_athom_tubes_bin_start");
extern const uint8_t athomC3End[] asm("_binary_build_output_s3_vault_esp32_c3_athom_tubes_bin_end");

Expand All @@ -29,6 +35,7 @@ constexpr uint32_t TARGET_MAX_AGE_MS = 60000;

S3FirmwareVaultPolicy policy;
S3FirmwareVaultCatalog catalog;
bool carrierCatalogReady = false;
S3VaultObservedDevice armedDevice;
bool probePending = false;
uint8_t probeMac[6] = {0};
Expand Down Expand Up @@ -76,7 +83,9 @@ void responseFinished(bool acknowledged, const uint8_t mac[6]) {
void rememberTarget(const DeviceReportMessage& report) {
if (!S3FirmwareVaultPolicy::isSupportedProfile(report.hardwareFamily,
report.firmwareVariant)
|| report.tubesVersion >= CARRIER_RELEASE) return;
|| report.tubesVersion > CARRIER_RELEASE
|| (report.tubesVersion == CARRIER_RELEASE
&& report.hardwareFamily != TubeHardwareDig2Go)) return;
size_t slot = targetCount;
for (size_t index = 0; index < targetCount; index++)
if (!memcmp(targets[index].mac, report.mac, 6)) { slot = index; break; }
Expand Down Expand Up @@ -175,6 +184,7 @@ void sendError(AsyncWebServerRequest* request, int code, const char* message) {
class S3FirmwareCarrier : public Usermod {
public:
void setup() override {
carrierCatalogReady = false;
S3VaultArtifact dig2go;
dig2go.family = TubeHardwareDig2Go;
dig2go.variant = TubeVariantStandard;
Expand All @@ -193,6 +203,7 @@ class S3FirmwareCarrier : public Usermod {
policy.arm(0, 0, millis());
return;
}
carrierCatalogReady = true;

server.on(ARM_PATH, HTTP_POST, [](AsyncWebServerRequest* request) {
static const char* const names[] = {"mac", "family", "variant", "current"};
Expand Down Expand Up @@ -405,19 +416,50 @@ size_t tubesS3CarrierArtifactCount() { return 2; }

bool tubesS3ReadCarrierArtifact(size_t index, TubesS3CarrierArtifact& artifact) {
artifact = TubesS3CarrierArtifact{};
if (!carrierCatalogReady) return false;
artifact.variant = TubeVariantStandard;
artifact.release = CARRIER_RELEASE;
if (index == 0) {
artifact.family = TubeHardwareDig2Go;
artifact.peerPropagation = true;
artifact.size = S3_VAULT_DIG2GO_SIZE;
return true;
}
if (index == 1) {
artifact.family = TubeHardwareAthomC3;
artifact.peerPropagation = false;
artifact.size = S3_VAULT_ATHOM_C3_SIZE;
return true;
}
return false;
}

bool tubesS3SeedDig2GoPropagation(uint16_t nodeId) {
if (!nodeId || !carrierCatalogReady) return false;
TubesS3CarrierTarget target;
bool exactTarget = false;
for (size_t index = 0; index < tubesS3CarrierTargetCount(); index++) {
if (!tubesS3ReadCarrierTarget(index, target)) continue;
if (target.nodeId == nodeId && target.family == TubeHardwareDig2Go
&& target.variant == TubeVariantStandard
&& target.release == CARRIER_RELEASE) {
exactTarget = true;
break;
}
}
if (!exactTarget) return false;
const S3VaultArtifact* catalogArtifact = catalog.select(
target.family, target.variant, target.release);
size_t embeddedSize = 0;
const uint8_t* embeddedData = artifactData(target.family, embeddedSize);
if (!catalogArtifact || !embeddedData || embeddedSize != catalogArtifact->size
|| embeddedSize != S3_VAULT_DIG2GO_SIZE) return false;
uint32_t nonce = esp_random();
if (!nonce) nonce = 1;
FleetUpdateOffer command;
return makeModernPropagationServeCommand(
command, catalogArtifact->tubesVersion, nonce, nodeId)
&& tubesS3BroadcastFleetOffer(command);
}

#endif
39 changes: 35 additions & 4 deletions usermods/WaveshareS3TubesRemote/WaveshareS3TubesRemote.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class WaveshareS3FieldOs : public Usermod {
bool touchReady = false;
bool nextSendFailed = false;
bool touchDown = false;
bool propagationSeedAttempted = false;
bool propagationSeedSent = false;

static constexpr uint16_t COLOR_BACKGROUND = 0x0863;
static constexpr uint16_t COLOR_SURFACE = 0x10E7;
Expand Down Expand Up @@ -254,6 +256,18 @@ class WaveshareS3FieldOs : public Usermod {
return value;
}

bool isSeedableDig2GoTarget(const TubesS3CarrierTarget &target) {
if (!target.nodeId || target.family != TubeHardwareDig2Go
|| target.variant != TubeVariantStandard) return false;
for (size_t index = 0; index < tubesS3CarrierArtifactCount(); index++) {
TubesS3CarrierArtifact artifact;
if (!tubesS3ReadCarrierArtifact(index, artifact)) continue;
if (artifact.family == target.family && artifact.variant == target.variant
&& artifact.release == target.release && artifact.size > 0) return true;
}
return false;
}

void drawDeviceCard(int16_t y, const DeviceCard &device,
uint16_t color = COLOR_SURFACE_RAISED) {
display.fillRoundRect(20, y, 440, 62, 10, color);
Expand Down Expand Up @@ -389,7 +403,8 @@ class WaveshareS3FieldOs : public Usermod {
if (artifact.family == TubeHardwareDig2Go) display.print(F("DIG2GO"));
else if (artifact.family == TubeHardwareAthomC3) display.print(F("ATHOM C3"));
else display.print(F("UNKNOWN TARGET"));
if (artifact.variant == TubeVariantStandard) display.print(F(" | STANDARD | v"));
if (artifact.peerPropagation) display.print(F(" | P2P | v"));
else if (artifact.variant == TubeVariantStandard) display.print(F(" | STANDARD | v"));
else display.print(F(" | VARIANT UNKNOWN | v"));
display.print(artifact.release);
}
Expand All @@ -401,6 +416,11 @@ class WaveshareS3FieldOs : public Usermod {
display.setTextColor(COLOR_MUTED);
display.setCursor(190, 276);
display.printf("Carrier state %u | release %u\n", carrier.state, carrier.release);
if (propagationSeedAttempted) {
display.setCursor(190, 292);
display.setTextColor(propagationSeedSent ? COLOR_MINT : RGB565_RED);
display.print(propagationSeedSent ? F("SEED REQUEST SENT") : F("SEED REQUEST FAILED"));
}
display.setCursor(24, 316);
display.println(F("DISCOVERED UPDATE TARGETS"));
const size_t count = tubesS3CarrierTargetCount();
Expand All @@ -414,9 +434,12 @@ class WaveshareS3FieldOs : public Usermod {
TubesS3CarrierTarget target;
if (!tubesS3ReadCarrierTarget(index, target)) continue;
const int16_t y = 332 + index * 66;
const bool seedable = isSeedableDig2GoTarget(target);
drawDeviceCard(y, {target.nodeId, target.release, target.uplinkId,
(millis() - target.lastSeenMs) / 1000,
target.family == 1 ? "DIG2GO" : "C3"});
seedable ? "DIG2GO / TAP TO SEED P2P"
: target.family == TubeHardwareDig2Go
? "DIG2GO / TAP TO UPDATE" : "C3 / TAP TO UPDATE"});
}
#else
display.setTextColor(COLOR_MUTED);
Expand Down Expand Up @@ -558,8 +581,14 @@ class WaveshareS3FieldOs : public Usermod {
} else if (y >= 326) {
const size_t index = (y - 332) / 66;
TubesS3CarrierTarget target;
if (tubesS3ReadCarrierTarget(index, target))
tubesS3ArmCarrier(target.mac, target.family, target.variant, target.release);
if (tubesS3ReadCarrierTarget(index, target)) {
if (owner.isSeedableDig2GoTarget(target)) {
owner.propagationSeedAttempted = true;
owner.propagationSeedSent = tubesS3SeedDig2GoPropagation(target.nodeId);
} else {
tubesS3ArmCarrier(target.mac, target.family, target.variant, target.release);
}
}
}
#endif
return FieldViewId::Update;
Expand All @@ -571,6 +600,8 @@ class WaveshareS3FieldOs : public Usermod {
TubesS3CarrierStatus carrier;
tubesS3ReadCarrierStatus(carrier);
uint32_t value = owner.mixRevision(carrier.state, carrier.release);
value = owner.mixRevision(value, owner.propagationSeedAttempted);
value = owner.mixRevision(value, owner.propagationSeedSent);
const size_t count = tubesS3CarrierTargetCount();
value = owner.mixRevision(value, count);
for (size_t index = 0; index < count; index++) {
Expand Down
Loading