Fix the STM32 flash overflows and fleet fixture compile failure from https://github.com/mikecarper/MeshCore/actions/runs/40241742182. Fold already-unsupported RX control paths only for the integrated STM32WL wrapper. Preserve RXPS-off validation, persistence, pending application and rollback. External radios retain their supported controls. Use measured GCC 14 code sharing in the four small STM32 USB Companion recipes. Keep firmware features, contact/channel capacities, filesystem boundaries and flash/RAM admission checks unchanged. Refresh downstream host fixtures for saved radio intent, deferred service restarts, WebConfig ownership, RX recovery and color tint state. Retain or strengthen behavior checks rather than skipping failing coverage. Validation: canonical RAK3x72, Wio Mini, Wio Dev and Tiny Relay USB Companion packages and Mini Repeater build passed at release-version length. All post-failure host checks, compact arithmetic/float checks, build-profile and real PlatformIO recipe/path/RAM-policy checks passed locally.
184 lines
8.2 KiB
Python
184 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Check USB workaround instructions and execute the real MQTT sleep gates."""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def method(path, signature):
|
|
text = (ROOT / path).read_text(encoding="utf-8")
|
|
start = text.index(signature)
|
|
end = text.index("{", start) + 1
|
|
depth = 1
|
|
while depth:
|
|
depth += (text[end] == "{") - (text[end] == "}")
|
|
end += 1
|
|
return text[start:end]
|
|
|
|
|
|
HARNESS = r'''
|
|
#include <cstdint>
|
|
#include <stdexcept>
|
|
#include <iostream>
|
|
#include <helpers/PowerManagementUtils.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
uint32_t millis() { return 0; }
|
|
#define WITH_BRIDGE 1
|
|
struct AbstractBridge { virtual bool isRunning() const = 0; };
|
|
struct MQTTBridge : AbstractBridge {
|
|
bool _initialized = false;
|
|
@RUNNING@
|
|
};
|
|
struct Radio {
|
|
bool isWatchdogObserving() const { return false; }
|
|
bool isCalibratingNoiseFloor() const { return false; }
|
|
};
|
|
struct MyMesh {
|
|
MQTTBridge* bridge = nullptr;
|
|
const AbstractBridge* activeBridge() const { return bridge; }
|
|
struct { bool pending = false; } deferred_cli_command;
|
|
struct {
|
|
bool radio_settings_pending = false;
|
|
bool hasActiveUserGpioTimer() const { return false; }
|
|
bool hasPendingTxPower() const { return false; }
|
|
bool hasPendingRadioSettings() const { return radio_settings_pending; }
|
|
} _cli;
|
|
struct { int getNumClients() const { return 0; } } acl;
|
|
struct { bool battery_alert_enabled = false; } _prefs;
|
|
Radio radio_driver;
|
|
bool pending_self_advert = false;
|
|
bool saved_radio_apply_pending = false, temp_radio_applied = false;
|
|
unsigned long next_flood_advert = 0, next_local_advert = 0;
|
|
unsigned long dirty_contacts_expiry = 0, next_recent_repeater_sweep = 0;
|
|
unsigned long next_battery_alert_check = 0, next_push = 0;
|
|
unsigned long radio_apply_retry_at = 0, set_radio_at = 0, revert_radio_at = 0;
|
|
bool hasPendingOtaApply() const { return false; }
|
|
bool hasQueuedWorkDue() const { return false; }
|
|
bool hasRetryWorkDue() const { return false; }
|
|
bool hasScheduledRadioWorkDue() const { return false; }
|
|
bool isMillisTimerDue(unsigned long) const { return false; }
|
|
bool millisHasNowPassed(unsigned long) const { return false; }
|
|
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
|
|
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
|
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
|
|
bool dual_radio = false;
|
|
bool isDualRadioActive() const { return dual_radio; }
|
|
bool canIdleDualRadio() const { return !dual_radio; }
|
|
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
|
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
|
bool hasPendingWork() const;
|
|
mesh::powerdiag::Mask pendingWorkReason() const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t) const;
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t) const;
|
|
};
|
|
@METHODS@
|
|
static void require(bool ok, const char* why) {
|
|
if (!ok) throw std::runtime_error(why);
|
|
}
|
|
int main() {
|
|
try {
|
|
MyMesh node;
|
|
require(node.getPowerSaveSleepSeconds(30) == 30, "quiet node cannot sleep");
|
|
node._cli.radio_settings_pending = true;
|
|
require(node.hasPendingWork(), "pending radio settings lost their sleep blocker");
|
|
require(node.getPowerSaveSleepSeconds(30) == 0,
|
|
"device sleep can interrupt pending radio settings");
|
|
node._cli.radio_settings_pending = false;
|
|
MQTTBridge logger;
|
|
node.bridge = &logger;
|
|
require(node.getPowerSaveSleepSeconds(30) == 30, "stopped MQTT blocked sleep");
|
|
logger._initialized = true;
|
|
require(node.hasPendingWork(), "running MQTT lost its sleep blocker");
|
|
require(node.getPowerSaveSleepSeconds(30) == 0,
|
|
"WiFi-only logging can enter device sleep without a USB guard");
|
|
logger._initialized = false;
|
|
require(node.getPowerSaveSleepSeconds(30) == 30,
|
|
"stopping MQTT did not release its sleep blocker");
|
|
node.dual_radio = true;
|
|
require(node.hasPendingWork(), "dual-radio scanning lost its sleep blocker");
|
|
require(node.getPowerSaveSleepSeconds(30) == 0, "dual-radio scanning can enter device sleep");
|
|
node.dual_radio = false;
|
|
require(node.getPowerSaveSleepSeconds(30) == 30,
|
|
"single-radio mode did not release its sleep blocker");
|
|
require(node.getPowerSaveSleepSeconds(0) == 0, "zero sleep limit changed");
|
|
} catch (const std::exception& e) {
|
|
std::cerr << e.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class LoggingSleepContractTest(unittest.TestCase):
|
|
def test_usb_enable_examples_include_workaround_first(self):
|
|
for path in (
|
|
"docs/cli_commands.md", "docs/role_feature_switches.md",
|
|
"docs/full_companion_features.md", "docs/companion_radio_full.md",
|
|
"docs/firmware_picker.md", "docs/old-releases/1.17.1.5.md",
|
|
):
|
|
with self.subTest(path=path):
|
|
text = (ROOT / path).read_text(encoding="utf-8")
|
|
blocks = re.findall(r"```text\n(.*?)```", text, re.S)
|
|
self.assertTrue(any("powersaving off\nset usb.logging on\n" in block
|
|
for block in blocks), path)
|
|
self.assertNotRegex(text, r"powersaving off\nset usb.logging on reboot")
|
|
|
|
def test_wifi_exception_is_documented_with_running_state_check(self):
|
|
for path in ("docs/cli_commands.md", "docs/role_feature_switches.md",
|
|
"docs/old-releases/1.17.1.5.md"):
|
|
with self.subTest(path=path):
|
|
text = (ROOT / path).read_text(encoding="utf-8")
|
|
paragraphs = [
|
|
" ".join(block.split())
|
|
for block in re.split(r"\n\s*\n", text)
|
|
if "WiFi/MQTT-only logging" in block
|
|
]
|
|
self.assertTrue(paragraphs, f"{path}: missing WiFi-only sleep guidance")
|
|
for paragraph in paragraphs:
|
|
self.assertTrue(
|
|
re.search(r"WiFi/MQTT-only logging.*?does not need", paragraph),
|
|
f"{path}: missing running-MQTT sleep exception",
|
|
)
|
|
# Check the actual guidance, not an unrelated bridge-command
|
|
# reference elsewhere in the document. MQTT has its own
|
|
# shared running-state command across firmware roles.
|
|
self.assertTrue(
|
|
"`get mqtt.running`" in paragraph,
|
|
f"{path}: WiFi-only sleep guidance must check `get mqtt.running`",
|
|
)
|
|
|
|
def test_running_mqtt_prevents_sleep_in_repeater_and_room(self):
|
|
running = method("src/helpers/bridges/MQTTBridge.h",
|
|
"bool isRunning() const override")
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-sleep-") as temp:
|
|
for role in ("simple_repeater", "simple_room_server"):
|
|
with self.subTest(role=role):
|
|
source = f"examples/{role}/MyMesh.cpp"
|
|
methods = "\n".join(method(source, signature) for signature in (
|
|
"bool MyMesh::hasPendingWork() const",
|
|
"mesh::powerdiag::Mask MyMesh::pendingWorkReason() const",
|
|
"uint32_t MyMesh::getPowerSaveSleepMilliseconds(uint32_t max_ms) const",
|
|
"uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const",
|
|
))
|
|
cpp = Path(temp) / f"{role}.cpp"
|
|
binary = Path(temp) / role
|
|
cpp.write_text(HARNESS.replace("@RUNNING@", running)
|
|
.replace("@METHODS@", methods))
|
|
built = subprocess.run([
|
|
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
|
|
"-I", str(ROOT / "src"), str(cpp), "-o", str(binary),
|
|
], capture_output=True, text=True)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
result = subprocess.run([str(binary)], capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|