Add opt-in combined RAK4631 Ethernet test image

Keep Full sensors, UART2, adaptive OTA storage and the 8 KiB loop stack. Arbitrate shared SPI and OTA workspace ownership, move network waits into a worker, and use bounded queued TCP operations. Add reproducible test configs, RAM checks and regression coverage. Validated the Full build, UF2 layout and 29 sanitizer runtime scenarios.
This commit is contained in:
mikecarper committed 2026-10-08 13:53:31 -07:00
1 parent 910223a3b2
commit 76ddc06e56
31 files changed
+3581 -8

No files matched your search

+7
View File
@@ -444,6 +444,13 @@ jobs:
python3 -B test/test_nrf52_usb_power.py
python3 -B test/test_shared_mota_queue.py
- name: Verify opt-in RAK4631 combined Ethernet and OTA modes
run: |
python3 -B test/test_rak_combined_build_config.py -v
python3 -B test/test_rak_combined_ota.py -v
python3 -B test/test_rak_ethernet_transmit.py -v
python3 -B test/test_rak4631_ethernet_runtime.py -v
- name: Verify nRF52 infrastructure trims and unchanged Companion scope
run: |
python3 -B test/test_nrf52_flash_trim.py -v
+58
View File
@@ -45,6 +45,64 @@ Headless and OLED devices use their own smaller totals. The JSON lists each
component and checks the largest available region against the largest planned
single allocation.
The experimental RAK4631 combined Ethernet/UART2/adaptive-storage config adds
an explicit 2,560-byte allowance for the UART bridge and its TX semaphore.
Its Ethernet worker reserves 1,024 stack words (4,096 bytes) plus 256 bytes for
task metadata and allocator overhead. Its bounded TCP transmit queue allocates
512 bytes only while Ethernet is enabled; another 64 bytes cover allocator
overhead. The DHCP object, socket state, queue indices and SPI objects are
static and already reduce the linked heap capacity.
Ethernet and external OTA share an exclusive ownership contract. Enabling
Ethernet rejects a live or pinned transfer, releases idle external OTA buffers,
and blocks encoder, self-source, manual-staging and folder-source allocation
while Ethernet owns SPI. Disabling Ethernet parks the worker after socket/SPI
cleanup; the main loop deletes that parked task synchronously and frees its
TX queue before releasing ownership. The worker stack therefore cannot remain queued for idle-task
reclamation while OTA allocates again. The budget counts the larger of the
4,928-byte Ethernet worker/queue or 5,168-byte external OTA workspaces, with the UART
allowance alongside either mode. It keeps the 8 KiB loop stack, 63 flood rules,
and reserved 64 KiB OTA handoff arena.
This combined config uses a source-bounded 2 KiB SSD1306 allowance. Its named
128x64 geometry needs one 1,024-byte framebuffer, reused on later starts;
the driver itself is embedded in a linked global, measured at 156 bytes in
the ARM test ELF. Including that already-counted object again plus 32 bytes
for allocation overhead still leaves 836 bytes within the allowance.
A compile-time assertion
covers framebuffer, complete driver object and allocator overhead within that
bound. This corrects a padded allocation estimate; it does not free actual RAM.
Other images retain their existing display budgets. These are local
test configs, not a qualification of a published or physically tested image.
Generate matched Full/Reduced baseline and combined configs outside the
repository without starting PlatformIO:
```sh
python3 scripts/generate_rak4631_combined_test.py
```
Run its printed commands one at a time and preserve each ELF and memory report
before the next build reuses the environment directory.
The combined prototype starts with Ethernet off after every restart. Use the
USB CLI commands `eth on`, `eth.status`, and `eth off`; `get eth` reports the
requested mode. UART2 remains independently controlled by
`set bridge.uart 2` and `set rs232.enabled on|off`. Full retains every sensor
driver; absent sensors do not require a different image.
With Ethernet on, OTA uses eligible internal-flash deltas. External full-image
staging, self-serving, manual serving buffers and OTA folders are unavailable
until Ethernet is off. Active or staged OTA work prevents a mode change.
RAK15001 and Ethernet share chip-select 26 and cannot be used together; the
prototype refuses that combination. External storage also needs a matching
bootloader that permits internal OTA while Ethernet is enabled.
TCP sends and receives use bounded register operations; a stalled peer cannot
hold the mesh loop waiting for an acknowledgement. DHCP and listener management
run in the Ethernet worker. This is synthetic coverage and a build experiment,
not a physical Ethernet/PoE soak qualification or a replacement release image.
The [small-screen message layout](../test-results/v4_pixel5_font_trial.md) retains complete
160-byte messages. Its expanded preview records add 2,816 bytes to the
startup allowance and increase the contiguous history allocation budget.
+37
View File
@@ -73,6 +73,30 @@ SimpleMeshTables tables;
MyMesh the_mesh(board, radio_driver, *new ArduinoMillis(), fast_rng, rtc_clock, tables);
#include "../InfrastructureWireless.h"
#if defined(RAK4631_COMBINED_ETHERNET)
// Storage selection and OTA policy belong to the mesh loop. The Ethernet
// worker only handles its own controller and asks these hooks to hand the
// shared WisBlock pins over before/after it runs.
static bool prepareCombinedEthernet(void*, char* error, size_t capacity) {
auto* context = mesh::ota::ota_context_if_active();
if (!context) {
snprintf(error, capacity, "Error: OTA storage is not ready");
return false;
}
return context->prepareRakEthernet(error, capacity);
}
static bool canReleaseCombinedEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
return context && context->canReleaseRakEthernet();
}
static bool releaseCombinedEthernet(void*) {
auto* context = mesh::ota::ota_context_if_active();
return context && context->releaseRakEthernet();
}
#endif
void halt() {
while (1) ;
}
@@ -340,6 +364,13 @@ void setup() {
#endif
#ifdef ETHERNET_ENABLED
#if defined(RAK4631_COMBINED_ETHERNET)
EthernetCliHooks hooks;
hooks.prepare = prepareCombinedEthernet;
hooks.can_release = canReleaseCombinedEthernet;
hooks.released = releaseCombinedEthernet;
ethernet_configure(hooks); // First prototype defaults OFF on every restart.
#endif
ethernet_start_task();
#endif
@@ -481,7 +512,13 @@ static void __attribute__((noinline)) serviceCommandInterfaces() {
#ifdef ETHERNET_ENABLED
ethernet_loop_maintain();
#if defined(RAK4631_COMBINED_ETHERNET)
// The facade never accesses SPI while startup, renewal, or shutdown owns
// the controller. Keep pending output during a temporary renewal pause.
if (ethernet_take_session_reset() || !ethernet_get_enabled()) {
#else
if (ethernet_take_session_reset() || !ethernet_client.connected()) {
#endif
the_mesh.cancelLocalOutput(ethernet_client);
ethernet_command[0] = 0;
}
+42 -1
View File
@@ -62,11 +62,28 @@ def requirements(platform, defines, target):
else:
raise ValueError(f"add a runtime allocation budget for display {display!r}")
parts = {}
combined_rak_ethernet = "RAK4631_COMBINED_ETHERNET" in defines
if combined_rak_ethernet and (platform != "NRF52_PLATFORM"
or not {"RAK_4631", "ETHERNET_ENABLED", "OTA_RAK_AUTO_STORE"}.issubset(defines)):
raise ValueError("combined RAK Ethernet budget requires RAK4631 adaptive nRF52 storage")
if combined_rak_ethernet and display == "SSD1306Display":
# The fixed 128x64 driver allocates one 1024-byte framebuffer and
# reuses it after power cycling. Its linked source assertion binds
# framebuffer + complete driver object + allocator overhead to 2 KiB.
# The actual linked ARM driver is 156 bytes; it is already static,
# so including it here again retains a conservative allocation bound.
display_heap = 2048
if platform == "NRF52_PLATFORM":
parts["loop_and_callback_stacks"] = integer(defines, "MESH_NRF52_LOOP_STACK_WORDS", 2048) * 4 + 3072
parts["core_usb_filesystems_sensors"] = 12288
if "BLE_PIN_CODE" in defines:
parts["bluetooth_worker_stacks"] = 5920
if combined_rak_ethernet and "WITH_RS232_BRIDGE" in defines:
# The bridge owns a packet/ACK duplicate table as well as its RX
# frame. Its source assertion bounds the object to 2304 bytes;
# retain room for allocation metadata and the UART TX semaphore.
# UART RX/TX arrays are embedded in the already-linked globals.
parts["uart_bridge_and_driver"] = 2560
elif platform == "ESP32_PLATFORM":
parts["core_tasks_usb_filesystems"] = 24576
wifi = 49152 if "WIFI_SSID" in defines or "WIFI_OTA_SEEDER" in defines else 0
@@ -133,6 +150,29 @@ def requirements(platform, defines, target):
# Unlike ESP32's existing source scratch reserve these allocations
# were formerly covered only by the general transient margin.
parts["ota_self_source_scratch"] = 2 * 2048 + 2 * 16
if combined_rak_ethernet:
words = integer(defines, "RAK4631_ETHERNET_TASK_STACK_WORDS", 1024)
if words < 1024:
raise ValueError("combined RAK Ethernet task requires at least 1024 stack words")
tx_capacity = integer(defines, "ETHERNET_CLI_TX_BUFFER_BYTES", 512)
if not 512 <= tx_capacity <= 2048:
raise ValueError("combined RAK Ethernet TX queue requires 512..2048 bytes")
# Ethernet owns the shared bus only after rejecting live/pinned OTA
# sessions and freeing the external self-source buffers. While it owns
# that bus, OTA encoder/self-source entry points refuse allocation.
# On shutdown the main loop deletes the parked worker synchronously
# and frees its TX queue before releasing ownership, so its stack
# cannot await idle cleanup
# while external OTA workspaces are allocated again.
# UART can remain enabled in either mode, so its budget stays above.
# The library's function-static DHCP object and SPI/socket globals are
# already present in the linked image. The bounded TCP TX queue is
# allocated only while Ethernet is enabled; reserve its capacity plus
# 64 bytes for allocator overhead alongside the worker stack and TCB.
external_ota = sum(parts.pop(name, 0) for name in (
"ota_encoder_workspace", "ota_self_source_scratch"))
parts["ethernet_or_external_ota_workspaces"] = max(words * 4 + 256 + tx_capacity + 64,
external_ota)
if display and display != "NullDisplayDriver":
parts["display_pixels_and_driver"] = display_heap
screen_budget = integer(defines, "MESH_COMPANION_SCREEN_STARTUP_BYTES", 0)
@@ -164,7 +204,8 @@ def requirements(platform, defines, target):
if "expanded_message_previews" in parts:
largest = max(largest, 8192 + parts["expanded_message_previews"])
for name in ("client_table", "flood_filter_table", "room_flood_rule_table",
"ota_context", "screen_objects_and_history", "ota_encoder_workspace"):
"ota_context", "screen_objects_and_history", "ota_encoder_workspace",
"ethernet_or_external_ota_workspaces"):
largest = max(largest, parts.get(name, 0))
return {"required_heap_bytes": required, "required_contiguous_bytes": largest,
"components": parts, "display": display, "full_companion": full}
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Generate matched local RAK4631 baseline/combined test configs, without building.
The production environment and release artifacts are left untouched. The four
configs retain the unified target's OTA identity, 8 KiB loop stack, 63 flood rules
and exact board recipe. Full/Reduced sensor flags come from build.sh's normal
helpers. Run the printed PlatformIO commands individually; this checkout permits
only one PlatformIO process at a time. Preserve each build's outputs before the
next command reuses its environment directory.
"""
from __future__ import annotations
import argparse
import configparser
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import subprocess
ROOT = Path(__file__).resolve().parents[1]
TARGET = "RAK_4631_repeater_unified_lora_ota"
ETHERNET_LIBRARY = "https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip"
PROFILE_HELPERS = r'''
set -euo pipefail
source build.sh
target=RAK_4631_repeater_unified_lora_ota
PIO_ENV_PLATFORM_BY_NAME[$target]=NRF52_PLATFORM
PIO_ENV_QSPI_OTA_BY_NAME[$target]=1
PIO_ENV_OTA_BY_NAME[$target]=1
PIO_ENV_SD_OTA_BY_NAME[$target]=0
NRF52_OTA_SENSOR_PROFILE=$MESH_TRIAL_SENSOR_PROFILE
SKIP_DECLARED_REDUCTIONS=$([ "$NRF52_OTA_SENSOR_PROFILE" = full ] && printf 1 || printf 0)
DISABLE_DEBUG=1
PLATFORMIO_BUILD_FLAGS=''
PLATFORMIO_BUILD_UNFLAGS=''
disable_debug_flags "$target"
apply_nrf52_size_profile "$target"
apply_lora_ota_no_external_sensors_profile "$target"
apply_radio_overrides
apply_firmware_profile_overrides
export PLATFORMIO_BUILD_FLAGS PLATFORMIO_BUILD_UNFLAGS
python3 -c 'import json,os;print(json.dumps({name:os.environ.get(name, "") for name in ("PLATFORMIO_BUILD_FLAGS","PLATFORMIO_BUILD_UNFLAGS")}))'
'''
def profile_flags(sensor: str) -> dict[str, str]:
# No project-context initialization, build worker or PlatformIO invocation.
environment = {**os.environ, "MESH_TRIAL_SENSOR_PROFILE": sensor,
"FIRMWARE_PROFILE_OVERRIDE": "cascade",
"RADIO_FREQ_OVERRIDE": "910.525", "RADIO_BW_OVERRIDE": "62.5",
"RADIO_SF_OVERRIDE": "7", "RADIO_CR_OVERRIDE": "5",
"RADIO_SETTING_TITLE": "USA Cascadia"}
result = subprocess.run(["bash", "-c", PROFILE_HELPERS], cwd=ROOT,
env=environment, capture_output=True, text=True, check=True)
values = json.loads(result.stdout.splitlines()[-1])
if set(values) != {"PLATFORMIO_BUILD_FLAGS", "PLATFORMIO_BUILD_UNFLAGS"}:
raise ValueError("unexpected build-profile helper output")
return values
def generate(output: Path, firmware_version: str) -> dict:
output = output.expanduser().resolve()
if output == ROOT or ROOT in output.parents:
raise ValueError("put experimental configs outside the repository and release output")
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._+-]*", firmware_version):
raise ValueError("firmware version must contain only letters, digits, dots, underscores, plus or minus")
source = (ROOT / "platformio.ini").read_text()
extra_configs = " variants/*/platformio.ini\n platformio.local.ini"
if source.count(extra_configs) != 1:
raise ValueError("main extra_configs changed; refusing a disconnected trial config")
variant = configparser.ConfigParser(interpolation=None, inline_comment_prefixes=(";",))
variant.read(ROOT / "variants/rak4631/platformio.ini")
raw_flags = variant["env:" + TARGET]["build_flags"]
mota_id = "0x%08x" % int.from_bytes(hashlib.sha256(TARGET.encode()).digest()[:4], "little")
output.mkdir(parents=True, exist_ok=True)
profiles, configs = {}, []
for sensor in ("full", "reduced"):
flags = profile_flags(sensor)
profiles[sensor] = flags
for combined in (False, True):
name = ("combined" if combined else "baseline") + ("" if sensor == "full" else "-reduced")
common = (f"-DFIRMWARE_VERSION='\"{firmware_version}\"' "
f"-DOTA_VARIANT='\"{TARGET}\"' -DMOTA_TARGET_ID={mota_id}")
build_flags = raw_flags + "\n" + flags["PLATFORMIO_BUILD_FLAGS"] + " " + common
if combined:
build_flags += " -DETHERNET_ENABLED=1 -DRAK4631_COMBINED_ETHERNET=1"
overlay = "[env:" + TARGET + "]\nbuild_flags =\n"
overlay += "\n".join(" " + line.strip() for line in build_flags.splitlines() if line.strip()) + "\n"
overlay += "build_unflags = " + flags["PLATFORMIO_BUILD_UNFLAGS"].strip() + "\n"
if combined:
overlay += "lib_deps = ${rak4631.lib_deps}\n " + ETHERNET_LIBRARY + "\n"
overlay_path = output / ("overlay-" + name + ".ini")
overlay_path.write_text(overlay)
config = source.replace(extra_configs,
" " + str(ROOT / "variants/*/platformio.ini") + "\n"
" " + str(ROOT / "platformio.local.ini") + "\n"
" " + str(overlay_path))
config_path = output / ("platformio-" + name + ".ini")
config_path.write_text(config)
configs.append({"profile": name, "config": str(config_path),
"config_sha256": hashlib.sha256(config.encode()).hexdigest(),
"overlay_sha256": hashlib.sha256(overlay.encode()).hexdigest()})
source_commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
receipt = {"schema_version": 1, "target": TARGET, "mota_target_id": mota_id,
"firmware_version": firmware_version, "full_rule_capacity": 63,
"loop_stack_bytes": 8192, "source_commit": source_commit,
"profiles": profiles, "configs": configs,
"scope": "Local config generation only; builds and hardware acceptance remain required."}
(output / "profile-flags.json").write_text(json.dumps(receipt, indent=2) + "\n")
return receipt
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
default = Path.home() / ".cache" / ("meshcore-rak-combined-" + datetime.now(timezone.utc).strftime("%Y%m%d"))
parser.add_argument("--output", type=Path, default=default)
parser.add_argument("--firmware-version", default="v1.17.1.9-rak-combined-trial")
args = parser.parse_args()
receipt = generate(args.output, args.firmware_version)
for config in receipt["configs"]:
print(" ".join(shlex.quote(value) for value in (
"pio", "run", "-d", str(ROOT), "-c", config["config"], "-e", TARGET)))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+5
View File
@@ -10,6 +10,11 @@
// allocation even when a combined Full image starts with the UART disabled.
static_assert(sizeof(RS232Bridge) <= 4096, "Update the ESP32 UART heap budget");
#endif
#if defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET)
// The combined RAK RAM policy reserves this object plus allocator metadata
// and the UART driver's TX semaphore. Its RX/TX arrays are linked globals.
static_assert(sizeof(RS232Bridge) <= 2304, "Update the combined RAK UART heap budget");
#endif
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
int16_t tx_pin, mesh::PacketManager *mgr,
+187
View File
@@ -7,6 +7,9 @@
#include <RAK13800_W5100S.h>
#include <helpers/nrf52/EthernetMac.h>
#include <helpers/UsbLogging.h>
#if defined(RAK4631_COMBINED_ETHERNET)
#include <atomic>
#endif
// Ethernet startup runs on a FreeRTOS task, and client acceptance runs in the
// mesh loop. Never let either path wait on a CDC host which stopped reading.
@@ -38,15 +41,37 @@ static SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SP
#define ETHERNET_RETRY_INTERVAL_MS 30000
static EthernetServer ethernet_server(ETHERNET_TCP_PORT);
#if !defined(RAK4631_COMBINED_ETHERNET)
static EthernetClient ethernet_client;
#endif
#if defined(RAK4631_COMBINED_ETHERNET)
static std::atomic<bool> ethernet_running{false};
#else
static volatile bool ethernet_running = false;
#endif
#if defined(RAK4631_COMBINED_ETHERNET)
static std::atomic<bool> ethernet_session_reset{false};
static bool ethernet_line_discarding = false; // Main-thread framing state.
#else
static bool ethernet_session_reset = false;
#endif
static bool ethernet_take_session_reset() {
#if defined(RAK4631_COMBINED_ETHERNET)
const bool changed = ethernet_session_reset.exchange(false);
if (changed) ethernet_line_discarding = false;
return changed;
#else
const bool changed = ethernet_session_reset;
ethernet_session_reset = false;
return changed;
#endif
}
#if defined(RAK4631_COMBINED_ETHERNET)
static void ethernet_check_client();
#include "EthernetCliRuntime.h"
#else
// FreeRTOS task: handles hw init, DHCP, and retries in the background
static void ethernet_task(void* param) {
(void)param;
@@ -99,10 +124,48 @@ static void ethernet_task(void* param) {
static void ethernet_start_task() {
xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL);
}
#endif
// Format ethernet status into reply buffer. Returns true if command was handled.
static bool ethernet_handle_command(const char* command, char* reply) {
#if defined(RAK4631_COMBINED_ETHERNET)
if (strcmp(command, "eth on") == 0 || strcmp(command, "set eth on") == 0) {
(void)ethernet_request_start(reply, true);
return true;
}
if (strcmp(command, "eth off") == 0 || strcmp(command, "set eth off") == 0) {
(void)ethernet_request_stop(reply);
return true;
}
if (strcmp(command, "get eth") == 0) {
strcpy(reply, ethernet_enabled.load() ? "> on" : "> off");
return true;
}
if (strncmp(command, "eth ", 4) == 0 || strncmp(command, "set eth ", 8) == 0) {
strcpy(reply, "Error: Ethernet mode must be on or off");
return true;
}
#endif
if (strcmp(command, "eth.status") == 0) {
#if defined(RAK4631_COMBINED_ETHERNET)
const EthernetCliState state = ethernet_state.load();
if (state != EthernetCliState::Online) {
const char* status = state == EthernetCliState::Off ? "off" :
state == EthernetCliState::Starting ? "starting" :
state == EthernetCliState::Retrying ? "waiting for DHCP" :
state == EthernetCliState::Maintaining ? "renewing DHCP" :
state == EthernetCliState::Stopping ? "stopping" :
state == EthernetCliState::NoHardware ? "hardware not found" :
state == EthernetCliState::Faulted ? "controller stalled; turn Ethernet off/on" : "start failed";
snprintf(reply, 160, "ETH: %s", status);
return true;
}
const uint32_t ip = ethernet_cached_ip.load();
snprintf(reply, 160, "ETH: %u.%u.%u.%u:%d", (unsigned)(ip & 255),
(unsigned)((ip >> 8) & 255), (unsigned)((ip >> 16) & 255),
(unsigned)((ip >> 24) & 255), ETHERNET_TCP_PORT);
return true;
#endif
if (!ethernet_running) {
strcpy(reply, "ETH: not connected");
} else {
@@ -119,6 +182,36 @@ static bool ethernet_handle_command(const char* command, char* reply) {
// available() also returns existing connected sockets that have data, which
// would force us to disambiguate every inbound packet from a real new client.
static void ethernet_check_client() {
#if defined(RAK4631_COMBINED_ETHERNET)
// Combined accept/relisten can call vendor controller commands; run only
// in the background worker while the mesh-facing Stream is gated.
auto newClient = ethernet_server.accept();
if (newClient) {
ethernet_session_generation.fetch_add(1);
if (ethernet_raw_client && ethernet_raw_client.getSocketNumber() != newClient.getSocketNumber()) {
(void)mesh::nrf52::EthernetCliTransmit::close(ethernet_raw_client.getSocketNumber());
ethernet_require_command_check();
}
ethernet_queue_clear();
ethernet_transmit.reset();
ethernet_raw_client = newClient;
ethernet_client_attached.store(true);
ethernet_session_reset = true;
static const char banner[] = ETHERNET_CLI_BANNER "\r\n";
(void)ethernet_queue_write(reinterpret_cast<const uint8_t*>(banner), sizeof(banner) - 1);
}
if (ethernet_raw_client &&
!mesh::nrf52::EthernetCliTransmit::connected(ethernet_raw_client.getSocketNumber())) {
ethernet_session_generation.fetch_add(1);
(void)mesh::nrf52::EthernetCliTransmit::close(ethernet_raw_client.getSocketNumber());
ethernet_require_command_check();
ethernet_raw_client = EthernetClient();
ethernet_client_attached.store(false);
ethernet_queue_clear();
ethernet_transmit.reset();
ethernet_session_reset = true;
}
#else
auto newClient = ethernet_server.accept();
if (newClient) {
if (ethernet_client) ethernet_client.stop();
@@ -128,20 +221,102 @@ static void ethernet_check_client() {
ETHERNET_CLI_LOG("Client connected from %u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
ethernet_client.println(ETHERNET_CLI_BANNER);
}
#endif
}
// Call from loop() to maintain DHCP and check for new clients
static void ethernet_loop_maintain() {
#if defined(RAK4631_COMBINED_ETHERNET)
if (ethernet_state.load() == EthernetCliState::Stopping) {
if (ethernet_worker_active.load() && !ethernet_worker_stopped.load()) {
EthernetSpiAccess expected = EthernetSpiAccess::Idle;
if (ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Worker)) {
ethernet_running.store(false);
}
if (ethernet_spi_access.load() == EthernetSpiAccess::Worker) ethernet_notify_worker();
return;
}
if (ethernet_complete_release()) {
ethernet_state.store(EthernetCliState::Off);
}
return;
}
if (!ethernet_running.load() && ethernet_bus_reserved &&
(!ethernet_worker_active.load() || ethernet_worker_stopped.load())) {
(void)ethernet_complete_release();
return;
}
if (ethernet_running.load()) {
if (static_cast<uint32_t>(millis() - ethernet_last_maintain.load()) >= 1000) {
ethernet_request_maintenance();
}
}
#else
if (ethernet_running) {
ethernet_check_client();
Ethernet.maintain();
}
#endif
}
// Read a line from the Ethernet client into the command buffer.
// Returns true when a complete line is ready to process (command is null-terminated).
// The caller should process the command and then reset ethernet_command[0] = 0.
static bool ethernet_read_line(char* ethernet_command, size_t buf_size) {
#if defined(RAK4631_COMBINED_ETHERNET)
if (buf_size < 2) return false;
EthernetClientAccess access;
if (!access || !ethernet_client_attached.load()) return false;
const uint32_t generation = ethernet_session_generation.load();
if (ethernet_session_reset.load() ||
((ethernet_command[0] || ethernet_line_discarding) && ethernet_line_generation != generation)) {
ethernet_command[0] = 0;
ethernet_line_discarding = false;
return false;
}
ethernet_line_generation = generation;
const uint8_t socket = ethernet_raw_client.getSocketNumber();
if (!mesh::nrf52::EthernetCliTransmit::connected(socket)) return false;
// Reserve one complete synchronous CLI reply before dispatching a command;
// Print's component writes cannot otherwise retain a partially queued reply.
if (ethernet_queue_capacity() < 170) return false;
size_t length = strlen(ethernet_command);
// One gate covers this bounded turn, so a worker cannot replace the peer
// between bytes or between the terminal byte and reply-owner publication.
for (unsigned consumed = 0; consumed < 128 &&
mesh::nrf52::EthernetCliTransmit::available(socket); ++consumed) {
const int next = mesh::nrf52::EthernetCliTransmit::read(socket);
if (!mesh::nrf52::EthernetCliTransmit::commandReady(socket)) ethernet_require_command_check();
if (next < 0) break;
const char c = static_cast<char>(next);
if (c == '\r' || c == '\n') {
if (ethernet_line_discarding) {
ethernet_line_discarding = false;
ethernet_command[0] = 0;
static const char error[] = "Error: invalid or oversized command; discarded\r\n";
(void)ethernet_queue_write(reinterpret_cast<const uint8_t*>(error), sizeof(error) - 1);
ethernet_session_reset.store(true);
return false;
}
if (length == 0) continue;
ethernet_command[length] = 0;
ethernet_reply_generation = generation;
static const uint8_t newline[] = {'\r', '\n'};
(void)ethernet_queue_write(newline, sizeof(newline));
return true;
}
if (ethernet_line_discarding) continue;
if (c == '\0' || length == buf_size - 1) {
ethernet_line_discarding = true;
ethernet_command[0] = 0;
length = 0;
continue;
}
ethernet_command[length++] = c;
ethernet_command[length] = 0;
}
return false;
#else
if (!ethernet_running || !ethernet_client || !ethernet_client.connected()) return false;
int elen = strlen(ethernet_command);
@@ -162,12 +337,24 @@ static bool ethernet_read_line(char* ethernet_command, size_t buf_size) {
return true;
}
return false;
#endif
}
// Send a reply to the Ethernet client
static void ethernet_send_reply(const char* reply) {
if (reply[0]) {
#if defined(RAK4631_COMBINED_ETHERNET)
// One RAM-only enqueue keeps a synchronous command's complete reply even
// if the worker begins renewal between command dispatch and this call.
char frame[170];
const int length = snprintf(frame, sizeof(frame), " -> %s\r\n", reply);
if (length > 0 && static_cast<size_t>(length) < sizeof(frame)) {
(void)ethernet_queue_write(reinterpret_cast<const uint8_t*>(frame), static_cast<size_t>(length),
true, ethernet_reply_generation);
}
#else
ethernet_client.print(" -> "); ethernet_client.println(reply);
#endif
}
}
+544
View File
@@ -0,0 +1,544 @@
#pragma once
#include <cstdlib>
#include "EthernetCliTransmit.h"
// Included by EthernetCLI.h only for the opt-in combined RAK4631 prototype.
// Legacy Ethernet profiles retain their established implementation.
struct EthernetCliHooks {
bool (*prepare)(void*, char*, size_t) = nullptr;
bool (*can_release)(void*) = nullptr;
bool (*released)(void*) = nullptr;
bool (*save)(void*, bool) = nullptr;
void* context = nullptr;
};
enum class EthernetCliState : uint8_t {
Off, Starting, Retrying, Online, Maintaining, Stopping, NoHardware, StartFailed, Faulted
};
enum class EthernetSpiAccess : uint8_t { Idle, Client, Worker };
static EthernetCliHooks ethernet_hooks;
static std::atomic<bool> ethernet_enabled{false};
static std::atomic<bool> ethernet_worker_active{false};
static std::atomic<bool> ethernet_worker_stopped{false};
static std::atomic<EthernetCliState> ethernet_state{EthernetCliState::Off};
static std::atomic<EthernetSpiAccess> ethernet_spi_access{EthernetSpiAccess::Worker};
static std::atomic<bool> ethernet_maintain_requested{false};
static std::atomic<uint32_t> ethernet_cached_ip{0};
static std::atomic<uint32_t> ethernet_last_maintain{0};
// Only scheduler-critical sections access the handle. OTA context callbacks
// and bus reservation changes run exclusively on the main thread.
static TaskHandle_t ethernet_task_handle = nullptr;
static bool ethernet_bus_reserved = false;
static bool ethernet_spi_started = false;
static bool ethernet_hardware_found = false;
static EthernetClient ethernet_raw_client;
static std::atomic<bool> ethernet_client_attached{false};
static std::atomic<uint32_t> ethernet_session_generation{0};
static uint32_t ethernet_reply_generation = 0; // Main-thread command owner.
static uint32_t ethernet_line_generation = 0; // Main-thread partial input owner.
static std::atomic<bool> ethernet_command_check_requested{false};
static std::atomic<uint32_t> ethernet_command_check_started{0};
static bool ethernet_controller_faulted = false;
static mesh::nrf52::EthernetCliTransmit ethernet_transmit;
#ifndef ETHERNET_CLI_TX_BUFFER_BYTES
#define ETHERNET_CLI_TX_BUFFER_BYTES 512
#endif
static_assert(ETHERNET_CLI_TX_BUFFER_BYTES >= 512 && ETHERNET_CLI_TX_BUFFER_BYTES <= 2048,
"Ethernet queue capacity must match its qualified RAM budget");
// This queue exists only while Ethernet owns SPI. Free it and synchronously
// reclaim the worker before external OTA allocates its mutually exclusive RAM.
static uint8_t* ethernet_tx_queue = nullptr;
static size_t ethernet_tx_head = 0;
static size_t ethernet_tx_count = 0;
#ifndef RAK4631_ETHERNET_TASK_STACK_WORDS
#define RAK4631_ETHERNET_TASK_STACK_WORDS 1024
#endif
static_assert(RAK4631_ETHERNET_TASK_STACK_WORDS >= 1024,
"Ethernet worker requires its qualified stack budget");
static_assert(sizeof(StaticTask_t) + 32 <= 256,
"Ethernet task metadata exceeds its qualified RAM allowance");
static bool ethernet_is_running() { return ethernet_running.load(); }
static bool ethernet_get_enabled() { return ethernet_enabled.load(); }
static void ethernet_notify_worker() {
taskENTER_CRITICAL();
if (ethernet_task_handle != nullptr) xTaskNotifyGive(ethernet_task_handle);
taskEXIT_CRITICAL();
}
static void ethernet_queue_clear() {
taskENTER_CRITICAL();
ethernet_tx_head = 0;
ethernet_tx_count = 0;
taskEXIT_CRITICAL();
}
static size_t ethernet_queue_capacity() {
taskENTER_CRITICAL();
const size_t free = ethernet_tx_queue ? ETHERNET_CLI_TX_BUFFER_BYTES - ethernet_tx_count : 0;
taskEXIT_CRITICAL();
return free;
}
static size_t ethernet_queue_write(const uint8_t* data, size_t size,
bool bind_session = false, uint32_t generation = 0) {
taskENTER_CRITICAL();
if (!ethernet_tx_queue || !ethernet_client_attached.load() ||
(bind_session && generation != ethernet_session_generation.load())) {
taskEXIT_CRITICAL();
return 0;
}
const size_t free = ETHERNET_CLI_TX_BUFFER_BYTES - ethernet_tx_count;
if (size > free) size = free;
const size_t tail = (ethernet_tx_head + ethernet_tx_count) % ETHERNET_CLI_TX_BUFFER_BYTES;
const size_t first = size < ETHERNET_CLI_TX_BUFFER_BYTES - tail ? size : ETHERNET_CLI_TX_BUFFER_BYTES - tail;
memcpy(ethernet_tx_queue + tail, data, first);
memcpy(ethernet_tx_queue, data + first, size - first);
ethernet_tx_count += size;
taskEXIT_CRITICAL();
if (size) ethernet_notify_worker();
return size;
}
static void ethernet_require_command_check() {
ethernet_running.store(false);
ethernet_command_check_started.store(millis());
ethernet_command_check_requested.store(true);
ethernet_notify_worker();
}
class EthernetClientAccess {
bool acquired = false;
public:
EthernetClientAccess() {
if (!ethernet_running.load()) return;
EthernetSpiAccess expected = EthernetSpiAccess::Idle;
acquired = ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Client);
if (acquired && !ethernet_running.load()) {
ethernet_spi_access.store(EthernetSpiAccess::Idle);
acquired = false;
}
}
~EthernetClientAccess() {
if (!acquired) return;
ethernet_spi_access.store(EthernetSpiAccess::Idle);
}
explicit operator bool() const { return acquired; }
};
// MyMesh retains this Stream for asynchronous replies. It must never expose
// the raw client while DHCP initialization/renewal owns the SPI bus. Busy
// methods yield immediately so radio, USB and UART service can continue.
class EthernetCliClient : public Stream {
public:
int available() override {
EthernetClientAccess access;
if (!access || !ethernet_client_attached.load()) return 0;
const uint8_t socket = ethernet_raw_client.getSocketNumber();
const int available = mesh::nrf52::EthernetCliTransmit::available(socket);
if (!mesh::nrf52::EthernetCliTransmit::commandReady(socket)) ethernet_require_command_check();
return available;
}
int read() override {
EthernetClientAccess access;
if (!access || !ethernet_client_attached.load()) return -1;
const uint8_t socket = ethernet_raw_client.getSocketNumber();
const int value = mesh::nrf52::EthernetCliTransmit::read(socket);
if (!mesh::nrf52::EthernetCliTransmit::commandReady(socket)) ethernet_require_command_check();
return value;
}
int peek() override {
EthernetClientAccess access;
if (!access || !ethernet_client_attached.load()) return -1;
const uint8_t socket = ethernet_raw_client.getSocketNumber();
const int value = mesh::nrf52::EthernetCliTransmit::peek(socket);
if (!mesh::nrf52::EthernetCliTransmit::commandReady(socket)) ethernet_require_command_check();
return value;
}
int availableForWrite() override {
EthernetClientAccess access;
return access && ethernet_client_attached.load() ? static_cast<int>(ethernet_queue_capacity()) : 0;
}
size_t write(uint8_t value) override { return write(&value, 1); }
size_t write(const uint8_t* data, size_t size) override {
EthernetClientAccess access;
if (!access || ethernet_session_reset.load()) return 0;
return ethernet_queue_write(data, size);
}
void flush() override {} // Never wait for a remote TCP host.
bool connected() {
EthernetClientAccess access;
return access && ethernet_client_attached.load() &&
mesh::nrf52::EthernetCliTransmit::connected(ethernet_raw_client.getSocketNumber());
}
explicit operator bool() { return connected(); }
void stop() {
EthernetClientAccess access;
if (!access) return;
if (ethernet_raw_client) {
(void)mesh::nrf52::EthernetCliTransmit::close(ethernet_raw_client.getSocketNumber());
ethernet_require_command_check();
}
ethernet_raw_client = EthernetClient();
ethernet_client_attached.store(false);
ethernet_session_generation.fetch_add(1);
ethernet_queue_clear();
ethernet_transmit.reset();
ethernet_line_discarding = false;
ethernet_session_reset.store(true);
}
using Print::write;
};
static EthernetCliClient ethernet_client;
static bool ethernet_configure(const EthernetCliHooks& hooks, bool saved_enabled = false) {
if (ethernet_bus_reserved || ethernet_worker_active.load()) return false;
ethernet_hooks = hooks;
ethernet_enabled.store(saved_enabled);
ethernet_state.store(EthernetCliState::Off);
return true;
}
static void ethernet_stop_hardware() {
// The W5100S has four sockets; there is no EthernetServer::end() in the
// library. Stop listener, accepted-client and DHCP sockets before SPI.end.
// Never toggle the PHY reset or shared WisBlock supply during shutdown.
if (ethernet_spi_started && ethernet_hardware_found) {
for (uint8_t index = 0; index < 4; ++index) {
(void)mesh::nrf52::EthernetCliTransmit::close(index);
}
}
ethernet_raw_client = EthernetClient();
ethernet_client_attached.store(false);
ethernet_session_generation.fetch_add(1);
ethernet_running.store(false);
if (ethernet_spi_started) {
ETHERNET_SPI_PORT.end();
digitalWrite(PIN_ETHERNET_SS, HIGH);
ethernet_spi_started = false;
}
ethernet_hardware_found = false;
ethernet_transmit.reset();
ethernet_queue_clear();
ethernet_command_check_requested.store(false);
}
static bool ethernet_complete_release() {
// Main-thread only. A stopped worker parks with its handle intact. Deleting
// that other task reclaims its stack synchronously; self-deletion would
// defer reclamation to the idle task and overlap the external OTA workspace.
if (ethernet_worker_active.load()) {
if (!ethernet_worker_stopped.load()) return false;
taskENTER_CRITICAL();
TaskHandle_t stopped = ethernet_task_handle;
ethernet_task_handle = nullptr;
taskEXIT_CRITICAL();
if (stopped != nullptr) vTaskDelete(stopped);
ethernet_worker_active.store(false);
ethernet_worker_stopped.store(false);
}
ethernet_stop_hardware();
free(ethernet_tx_queue);
ethernet_tx_queue = nullptr;
ethernet_line_discarding = false;
if (ethernet_bus_reserved) {
if (!ethernet_hooks.released(ethernet_hooks.context)) return false;
ethernet_bus_reserved = false;
ethernet_session_reset = true;
}
return true;
}
static void ethernet_worker_take_access() {
while (ethernet_spi_access.load() != EthernetSpiAccess::Worker) {
EthernetSpiAccess expected = EthernetSpiAccess::Idle;
if (ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Worker)) break;
vTaskDelay(pdMS_TO_TICKS(1));
}
ethernet_running.store(false);
}
static void ethernet_finish_task(EthernetCliState state) {
// No hardware operation follows this publication. Keep the handle and park
// until the main thread deletes this task before handing SPI back to OTA.
taskENTER_CRITICAL();
ethernet_state.store(state);
ethernet_running.store(false);
ethernet_worker_stopped.store(true);
taskEXIT_CRITICAL();
for (;;) vTaskDelay(portMAX_DELAY);
}
static bool ethernet_publish_online() {
taskENTER_CRITICAL();
const bool enabled = ethernet_enabled.load();
if (enabled) {
ethernet_state.store(EthernetCliState::Online);
ethernet_spi_access.store(EthernetSpiAccess::Idle);
ethernet_running.store(true);
}
taskEXIT_CRITICAL();
return enabled;
}
static void ethernet_cache_ip() {
const IPAddress ip = Ethernet.localIP();
ethernet_cached_ip.store(static_cast<uint32_t>(ip[0]) |
(static_cast<uint32_t>(ip[1]) << 8) |
(static_cast<uint32_t>(ip[2]) << 16) |
(static_cast<uint32_t>(ip[3]) << 24));
}
static bool ethernet_has_tx_work() {
taskENTER_CRITICAL();
const bool queued = ethernet_tx_count != 0;
taskEXIT_CRITICAL();
return queued || ethernet_transmit.pending();
}
static bool ethernet_controller_commands_idle() {
SPIClass* spi = W5100.getSPI();
if (!spi) return false;
spi->beginTransaction(SPI_ETHERNET_SETTINGS);
bool idle = true;
for (uint8_t socket = 0; socket < 4; ++socket) {
if (W5100.readSnCR(socket) != 0) idle = false;
}
spi->endTransaction();
return idle;
}
static void ethernet_poll_command_check() {
if (!ethernet_command_check_requested.load()) return;
if (ethernet_controller_commands_idle()) {
ethernet_command_check_requested.store(false);
} else if (static_cast<uint32_t>(millis() - ethernet_command_check_started.load()) >= 250) {
ethernet_controller_faulted = true;
ethernet_state.store(EthernetCliState::Faulted);
ethernet_session_reset.store(true);
}
}
// Worker-only, while SPI access is Worker. No library socketSend/execCmdSn
// calls: stalled TCP acknowledgement must never hold the mesh loop.
static void ethernet_pump_tx() {
ethernet_transmit.poll(millis());
if (ethernet_transmit.failed()) {
if (ethernet_raw_client) {
if (!mesh::nrf52::EthernetCliTransmit::close(ethernet_raw_client.getSocketNumber())) {
ethernet_controller_faulted = true;
ethernet_state.store(EthernetCliState::Faulted);
} else {
ethernet_require_command_check();
}
}
ethernet_raw_client = EthernetClient();
ethernet_client_attached.store(false);
ethernet_session_generation.fetch_add(1);
ethernet_queue_clear();
ethernet_transmit.reset();
ethernet_session_reset.store(true);
return;
}
if (!ethernet_raw_client) { ethernet_queue_clear(); return; }
// Main-loop accesses are excluded by the SPI gate while this worker owns it.
taskENTER_CRITICAL();
const size_t head = ethernet_tx_head;
const size_t count = ethernet_tx_count;
taskEXIT_CRITICAL();
if (count == 0) return;
const size_t contiguous = count < ETHERNET_CLI_TX_BUFFER_BYTES - head
? count : ETHERNET_CLI_TX_BUFFER_BYTES - head;
const size_t written = ethernet_transmit.write(ethernet_raw_client.getSocketNumber(),
ethernet_tx_queue + head, contiguous, millis());
taskENTER_CRITICAL();
ethernet_tx_head = (ethernet_tx_head + written) % ETHERNET_CLI_TX_BUFFER_BYTES;
ethernet_tx_count -= written;
taskEXIT_CRITICAL();
}
static void ethernet_drain_before_stop() {
if (!ethernet_spi_started || !ethernet_hardware_found) return;
const uint32_t started = millis();
while (ethernet_has_tx_work() && static_cast<uint32_t>(millis() - started) < 250) {
ethernet_pump_tx();
if (!ethernet_has_tx_work()) break;
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10));
}
}
// Persistent background worker: both initial DHCP and renewal may wait for
// network responses. Main-loop Stream operations yield while it owns SPI.
// OFF interrupts retry waits, then lets an in-progress library call finish;
// never delete another task in the middle of its SPI transaction.
static void ethernet_task(void*) {
if (ethernet_enabled.load()) {
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
ETHERNET_SPI_PORT.begin();
ethernet_spi_started = true;
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
uint8_t mac[6];
generateEthernetMac(mac);
while (ethernet_enabled.load()) {
const bool connected = Ethernet.begin(mac, 10000, 2000) != 0;
ethernet_hardware_found = Ethernet.hardwareStatus() != EthernetNoHardware;
if (!ethernet_enabled.load()) break;
if (!ethernet_hardware_found) {
ethernet_stop_hardware();
ethernet_finish_task(EthernetCliState::NoHardware);
return;
}
if (connected) {
ethernet_server.begin();
ethernet_cache_ip();
ethernet_last_maintain.store(millis());
if (!ethernet_publish_online()) break;
for (;;) {
// An online OFF is notified by the next mesh-loop maintain call,
// allowing a command received over Ethernet to send its reply first.
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20));
if (!ethernet_enabled.load()) break;
if (ethernet_controller_faulted) continue;
const bool maintain = ethernet_maintain_requested.exchange(false);
ethernet_worker_take_access();
ethernet_poll_command_check();
if (ethernet_controller_faulted || ethernet_command_check_requested.load()) {
// Preserve the maintenance request while command checking yields.
if (maintain) ethernet_maintain_requested.store(true);
continue;
}
if (maintain) (void)Ethernet.maintain();
if (!ethernet_enabled.load()) break;
if (maintain) ethernet_last_maintain.store(millis());
// Do not enter vendor accept/relisten while SEND/RECV is pending.
if (!ethernet_transmit.pending()) ethernet_check_client();
ethernet_pump_tx();
if (ethernet_controller_faulted || ethernet_transmit.pending() ||
ethernet_command_check_requested.load()) continue;
ethernet_cache_ip();
if (!ethernet_publish_online()) break;
}
break;
}
ethernet_state.store(EthernetCliState::Retrying);
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS));
}
}
ethernet_worker_take_access();
ethernet_drain_before_stop();
ethernet_stop_hardware();
ethernet_finish_task(EthernetCliState::Stopping);
}
static bool ethernet_request_start(char* reply, bool persist) {
if (ethernet_state.load() == EthernetCliState::Stopping) {
strcpy(reply, "Error: Ethernet is still stopping");
return false;
}
if (ethernet_worker_stopped.load() && !ethernet_complete_release()) {
strcpy(reply, "Error: Ethernet bus release is still pending");
return false;
}
if (ethernet_worker_active.load() || ethernet_running.load()) {
strcpy(reply, "OK - Ethernet already on");
return true;
}
if (ethernet_bus_reserved && !ethernet_complete_release()) {
strcpy(reply, "Error: Ethernet bus release is still pending");
return false;
}
if (!ethernet_hooks.prepare || !ethernet_hooks.can_release || !ethernet_hooks.released) {
strcpy(reply, "Error: Ethernet bus safety hooks unavailable");
return false;
}
reply[0] = 0;
if (!ethernet_hooks.prepare(ethernet_hooks.context, reply, 160)) {
if (!reply[0]) strcpy(reply, "Error: Ethernet bus is busy");
return false;
}
ethernet_bus_reserved = true;
ethernet_tx_queue = static_cast<uint8_t*>(malloc(ETHERNET_CLI_TX_BUFFER_BYTES));
if (!ethernet_tx_queue) {
(void)ethernet_complete_release();
ethernet_state.store(EthernetCliState::StartFailed);
strcpy(reply, "Error: Ethernet queue allocation failed");
return false;
}
ethernet_queue_clear();
if (persist && ethernet_hooks.save && !ethernet_hooks.save(ethernet_hooks.context, true)) {
(void)ethernet_complete_release();
strcpy(reply, "Error: cannot save Ethernet mode");
return false;
}
ethernet_enabled.store(true);
ethernet_controller_faulted = false;
ethernet_command_check_requested.store(false);
ethernet_maintain_requested.store(false);
ethernet_spi_access.store(EthernetSpiAccess::Worker);
ethernet_state.store(EthernetCliState::Starting);
ethernet_worker_stopped.store(false);
ethernet_worker_active.store(true);
// Ensure a fast worker cannot clear the output handle before creation has
// finished publishing it. Handle notification/clearing use the same guard.
taskENTER_CRITICAL();
const BaseType_t created = xTaskCreate(ethernet_task, "eth_init",
RAK4631_ETHERNET_TASK_STACK_WORDS, nullptr, 1, &ethernet_task_handle);
taskEXIT_CRITICAL();
if (created != pdPASS) {
ethernet_task_handle = nullptr;
ethernet_enabled.store(false);
ethernet_worker_active.store(false);
(void)ethernet_complete_release();
ethernet_state.store(EthernetCliState::StartFailed);
strcpy(reply, "Error: Ethernet task allocation failed");
return false;
}
strcpy(reply, ethernet_hooks.save
? "OK - Ethernet starting; use eth.status"
: "OK - Ethernet starting until restart; use eth.status");
return true;
}
static bool ethernet_request_stop(char* reply) {
if (ethernet_bus_reserved && !ethernet_hooks.can_release(ethernet_hooks.context)) {
strcpy(reply, "Error: Ethernet mode is locked by OTA activity or staged data");
return false;
}
if (ethernet_hooks.save && !ethernet_hooks.save(ethernet_hooks.context, false)) {
strcpy(reply, "Error: cannot save Ethernet mode");
return false;
}
taskENTER_CRITICAL();
ethernet_enabled.store(false);
const bool active = ethernet_worker_active.load() || ethernet_bus_reserved;
ethernet_state.store(active ? EthernetCliState::Stopping : EthernetCliState::Off);
taskEXIT_CRITICAL();
// Startup/renewal already owns SPI, so wake cancellation immediately.
// Online OFF waits for the next main-loop maintain to preserve its reply.
if (ethernet_spi_access.load() == EthernetSpiAccess::Worker) ethernet_notify_worker();
strcpy(reply, active
? (ethernet_hooks.save ? "OK - Ethernet stopping" : "OK - Ethernet stopping until restart")
: (ethernet_hooks.save ? "OK - Ethernet off" : "OK - Ethernet off until restart"));
return true;
}
static void ethernet_start_task() {
if (!ethernet_enabled.load()) return;
char reply[160];
(void)ethernet_request_start(reply, false);
}
static void ethernet_request_maintenance() {
EthernetSpiAccess expected = EthernetSpiAccess::Idle;
if (!ethernet_spi_access.compare_exchange_strong(expected, EthernetSpiAccess::Worker)) return;
ethernet_running.store(false);
ethernet_state.store(EthernetCliState::Maintaining);
ethernet_last_maintain.store(millis());
ethernet_maintain_requested.store(true);
ethernet_notify_worker();
}
+219
View File
@@ -0,0 +1,219 @@
#pragma once
#include <RAK13800_W5100S.h>
#include <w5100.h>
#include <stddef.h>
#include <stdint.h>
namespace mesh {
namespace nrf52 {
#ifndef RAK4631_ETHERNET_SEND_TIMEOUT_MS
#define RAK4631_ETHERNET_SEND_TIMEOUT_MS 250u
#endif
static_assert(RAK4631_ETHERNET_SEND_TIMEOUT_MS > 0u &&
RAK4631_ETHERNET_SEND_TIMEOUT_MS < 0x80000000u,
"Ethernet send deadline must support wrap-safe elapsed time");
// The vendor socketSend()/execCmdSn() wait for ACK/command completion without
// a software deadline. Queue one SEND directly in the controller, then inspect
// its completion on later worker turns. Every method has a finite number of
// register transfers and never owns heap storage or waits for network traffic.
class EthernetCliTransmit {
static constexpr uint8_t NO_SOCKET = 0xFFu;
static constexpr uint8_t SOCKET_COUNT = 4u; // RAK13800's exact W5100S
static constexpr uint8_t FREE_SIZE_READS = 4u;
uint8_t _socket = NO_SOCKET;
bool _pending = false;
bool _failed = false;
bool _waiting_command = false;
uint32_t _started = 0;
class Transaction {
SPIClass* _spi;
public:
Transaction() : _spi(W5100.getSPI()) {
if (_spi) _spi->beginTransaction(SPI_ETHERNET_SETTINGS);
}
~Transaction() { if (_spi) _spi->endTransaction(); }
explicit operator bool() const { return _spi != nullptr; }
};
static bool usableStatus(uint8_t status) {
return status == SnSR::ESTABLISHED || status == SnSR::CLOSE_WAIT;
}
static bool socketValid(uint8_t socket) {
return socket < SOCKET_COUNT && W5100.getChip() == 51u;
}
static bool receiveSize(uint8_t socket, uint16_t& size) {
uint16_t previous = W5100.readSnRX_RSR(socket);
for (uint8_t count = 1u; count < FREE_SIZE_READS; ++count) {
const uint16_t current = W5100.readSnRX_RSR(socket);
if (current == previous) {
size = current;
return current <= W5100.SSIZE;
}
previous = current;
}
return false;
}
static int receiveByte(uint8_t socket, bool consume) {
if (!socketValid(socket)) return -1;
Transaction transaction;
if (!transaction || !usableStatus(W5100.readSnSR(socket)) ||
W5100.readSnCR(socket) != 0u) return -1;
uint16_t size = 0u;
if (!receiveSize(socket, size) || !size) return -1;
const uint16_t pointer = W5100.readSnRX_RD(socket);
uint8_t value = 0u;
if (W5100.read(static_cast<uint16_t>(W5100.RBASE(socket) + (pointer & W5100.SMASK)),
&value, 1u) != 1u) return -1;
if (consume) {
W5100.writeSnRX_RD(socket, static_cast<uint16_t>(pointer + 1u));
W5100.writeSnCR(socket, Sock_RECV);
}
return value;
}
bool expired(uint32_t now) const {
return static_cast<uint32_t>(now - _started) >= RAK4631_ETHERNET_SEND_TIMEOUT_MS;
}
void fail() {
_failed = true;
_pending = false;
_waiting_command = false;
}
public:
bool pending() const { return _pending || _waiting_command; }
bool failed() const { return _failed; }
void reset() {
_socket = NO_SOCKET;
_pending = _failed = _waiting_command = false;
_started = 0;
}
void poll(uint32_t now) {
if (_failed || (!_pending && !_waiting_command)) return;
Transaction transaction;
if (!transaction) { fail(); return; }
if (!usableStatus(W5100.readSnSR(_socket))) { fail(); return; }
const uint8_t interrupt = W5100.readSnIR(_socket);
if (interrupt & SnIR::TIMEOUT) {
W5100.writeSnIR(_socket, SnIR::TIMEOUT | SnIR::SEND_OK);
fail();
return;
}
const uint8_t command = W5100.readSnCR(_socket);
if (_pending && (interrupt & SnIR::SEND_OK) && command == 0u) {
W5100.writeSnIR(_socket, SnIR::SEND_OK);
_pending = false;
return;
}
if (_waiting_command && command == 0u) {
_waiting_command = false;
return;
}
if (expired(now)) fail();
}
size_t write(uint8_t socket, const uint8_t* data, size_t length, uint32_t now) {
if (!data || !length || _failed) return 0u;
if (socket >= SOCKET_COUNT || W5100.getChip() != 51u) { fail(); return 0u; }
if (_socket != NO_SOCKET && _socket != socket) { fail(); return 0u; }
_socket = socket;
poll(now);
if (_failed || _pending || _waiting_command) return 0u;
Transaction transaction;
if (!transaction) { fail(); return 0u; }
if (!usableStatus(W5100.readSnSR(socket))) { fail(); return 0u; }
if (W5100.readSnCR(socket) != 0u) {
_waiting_command = true;
_started = now;
return 0u;
}
if (!W5100.SSIZE || W5100.SMASK != W5100.SSIZE - 1u) { fail(); return 0u; }
// A 16-bit free-size register can change between byte reads. The vendor
// retries forever for a matching pair; retain backpressure after at most
// four reads and try again on a later worker turn instead.
uint16_t previous = W5100.readSnTX_FSR(socket);
uint16_t available = 0u;
bool stable = false;
for (uint8_t count = 1u; count < FREE_SIZE_READS; ++count) {
const uint16_t current = W5100.readSnTX_FSR(socket);
if (current == previous) { available = current; stable = true; break; }
previous = current;
}
if (!stable || !available) return 0u;
if (available > W5100.SSIZE) { fail(); return 0u; }
if (length > available) length = available;
const uint16_t count = static_cast<uint16_t>(length);
const uint16_t pointer = W5100.readSnTX_WR(socket);
const uint16_t offset = pointer & W5100.SMASK;
const uint16_t first = count <= W5100.SSIZE - offset
? count : static_cast<uint16_t>(W5100.SSIZE - offset);
if (W5100.write(static_cast<uint16_t>(W5100.SBASE(socket) + offset), data, first) != first) {
fail();
return 0u;
}
if (first < count && W5100.write(W5100.SBASE(socket), data + first,
static_cast<uint16_t>(count - first)) != count - first) {
fail();
return 0u;
}
W5100.writeSnTX_WR(socket, static_cast<uint16_t>(pointer + count));
W5100.writeSnIR(socket, SnIR::SEND_OK | SnIR::TIMEOUT); // revoke stale completion
W5100.writeSnCR(socket, Sock_SEND); // deliberately do not call execCmdSn()
_started = now;
_pending = true;
return count;
}
static bool close(uint8_t socket) {
if (socket >= SOCKET_COUNT || W5100.getChip() != 51u) return false;
Transaction transaction;
if (!transaction || W5100.readSnCR(socket) != 0u) return false;
W5100.writeSnCR(socket, Sock_CLOSE);
return true;
}
static bool commandReady(uint8_t socket) {
if (!socketValid(socket)) return false;
Transaction transaction;
return transaction && W5100.readSnCR(socket) == 0u;
}
static int available(uint8_t socket) {
if (!socketValid(socket)) return 0;
Transaction transaction;
if (!transaction || !usableStatus(W5100.readSnSR(socket)) ||
W5100.readSnCR(socket) != 0u) return 0;
uint16_t size = 0u;
return receiveSize(socket, size) ? size : 0;
}
static int peek(uint8_t socket) { return receiveByte(socket, false); }
static int read(uint8_t socket) { return receiveByte(socket, true); }
static bool connected(uint8_t socket) {
if (!socketValid(socket)) return false;
Transaction transaction;
if (!transaction) return false;
const uint8_t status = W5100.readSnSR(socket);
if (status == SnSR::ESTABLISHED) return true;
if (status != SnSR::CLOSE_WAIT) return false;
uint16_t size = 0u;
// An unstable size snapshot is backpressure, not proof of EOF. Retain
// the client until a stable zero proves its final bytes were consumed.
return !receiveSize(socket, size) || size != 0u;
}
};
} // namespace nrf52
} // namespace mesh
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#if defined(RAK4631_COMBINED_ETHERNET)
#include <atomic>
namespace mesh {
namespace ota {
// The combined RAK4631 image gives the WisBlock clock/data pins to one
// controller at a time. Reserve them before starting Ethernet's DHCP task;
// release them only after that task and every Ethernet socket have stopped.
// NOR discovery and QSPI staging must never touch these pins while reserved.
inline std::atomic<bool>& rak4631_ethernet_spi_owner() {
static std::atomic<bool> owner{false};
return owner;
}
inline bool rak4631_ethernet_owns_spi() {
return rak4631_ethernet_spi_owner().load(std::memory_order_acquire);
}
inline void rak4631_set_ethernet_spi_owner(bool ethernet) {
rak4631_ethernet_spi_owner().store(ethernet, std::memory_order_release);
}
} // namespace ota
} // namespace mesh
#endif
+114
View File
@@ -137,6 +137,9 @@ struct OtaContext {
// store. A failed allocation is reported by the CLI before any write.
uint8_t* serve_buf = nullptr;
bool ensureServeBuffer() {
#if defined(RAK4631_COMBINED_ETHERNET)
if (rak4631_ethernet_owns_spi()) return false;
#endif
if (!serve_buf) serve_buf = static_cast<uint8_t*>(malloc(OTA_SERVE_BUF_SIZE));
return serve_buf != nullptr;
}
@@ -470,6 +473,12 @@ struct OtaContext {
bool attach_folder_source(MotaSource* source, FolderLink link, const char* label,
char* msg, size_t cap) {
if (!msg || cap == 0) return false;
#if defined(RAK4631_COMBINED_ETHERNET)
if (rak4631_ethernet_owns_spi()) {
snprintf(msg, cap, "ERR turn Ethernet off before attaching an OTA folder");
return false;
}
#endif
if (!source || link == FOLDER_LINK_NONE) {
strncpy(msg, "ERR invalid folder source", cap);
msg[cap - 1] = 0;
@@ -564,6 +573,111 @@ struct OtaContext {
}
}
#if defined(RAK4631_COMBINED_ETHERNET)
// Only the main thread may change ownership. No allocation or SPI access in
// the DHCP worker is allowed until prepareRakEthernet() has finished.
bool canReleaseRakEthernet() const {
const bool own_image = manager.servingPrimaryManifest(serve_self_manifest);
return manager.fetchState() == OtaManager::IDLE &&
fetch_store.staged_size() == 0u && !apply_pending && !bootloader_apply_pending &&
!manager.pendingServeJobs() && !manager.pendingManifestJobs() &&
(!serving || own_image) && manager.servedCount() <= (own_image ? 1u : 0u) &&
serve_expected == 0u && !folder_active && !_folder_source && !folder_dest &&
!disconnected_folder_dest && !fetch_to_folder;
}
void refreshRakStorageCapabilities() {
const bool external = !rak4631_ethernet_owns_spi() && fetch_store.usesExternal();
manager.set_accept_full(external);
manager.set_accept_bootloader(
ota_bootloader_self_update_caps_valid(bootloaderUpdateCaps()));
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
manager.set_fetch_store(&fetch_store);
manager.set_archive_interest(false);
self_serve_supported = ota_self_serve_supported(external);
#if MESHCORE_OTA_DEVICE_DEFLATE
manager.set_transport_deflate_encoder(self_serve_supported ? ota_transport_deflate : nullptr);
#endif
}
void releaseRakIdleServingBuffers() {
// The idle preflight admits only our automatic own-image view, never a
// manual or folder source. Revoke it before freeing the caller-owned RAM.
manager.clear_primary();
serving = false;
free(serve_self_leaves); free(serve_self_proof);
serve_self_leaves = serve_self_proof = nullptr;
releaseServeBuffer();
serve_expected = 0u;
}
bool prepareRakEthernet(char* error, size_t cap) {
if (!error || !cap) return false;
error[0] = 0;
if (rak4631_ethernet_owns_spi()) {
snprintf(error, cap, "ERR Ethernet already owns WisBlock SPI");
return false;
}
if (!canReleaseRakEthernet()) {
snprintf(error, cap, "ERR OTA busy; cancel transfer, clear manual serve and detach folder first");
return false;
}
const uint8_t detected = OtaStoreQspiNrf52::autoDetect();
uint32_t rak_id = 0, w25_id = 0;
OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id);
(void)w25_id;
if (detected == 1u || detected == 3u) {
snprintf(error, cap, "ERR RAK15001 and Ethernet share chip-select 26; remove RAK15001 first");
return false;
}
if (rak_id != 0u && rak_id != 0xFFFFFFu) {
snprintf(error, cap, "ERR unknown SPI device on Ethernet chip-select 26 (%06lX)",
(unsigned long)rak_id);
return false;
}
const OtaBlCaps& caps = bootloaderAppCaps();
const bool qspi_bootloader = caps.present && (caps.storage_flags & OTA_BL_STORAGE_QSPI);
const OtaBootloaderIdentity& identity = bootloaderIdentity();
if (rak_storage_choice(0u, qspi_bootloader, identity.present && identity.crc_ok,
identity.device_name, false,
caps.optional_app_storage ==
(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25)) !=
RakStorageChoice::Internal) {
snprintf(error, cap, "ERR installed bootloader cannot use internal OTA with Ethernet");
return false;
}
if (!fetch_store.resetSelection()) {
snprintf(error, cap, "ERR staged OTA data must be cancelled before enabling Ethernet");
return false;
}
releaseRakIdleServingBuffers();
rak4631_set_ethernet_spi_owner(true);
refreshRakStorageCapabilities();
// The policy check above and the adaptive store must agree before the
// caller allocates a worker or lets the Ethernet library touch hardware.
if (!fetch_store.usesInternal()) {
rak4631_set_ethernet_spi_owner(false);
fetch_store.resetSelection();
refreshRakStorageCapabilities();
snprintf(error, cap, "ERR internal OTA selection failed; Ethernet remains off");
return false;
}
return true;
}
bool releaseRakEthernet() {
if (!rak4631_ethernet_owns_spi()) return true;
// OFF is cooperative. A transfer can start after its first preflight;
// keep the safe internal backend pinned and let the main loop retry after
// cancellation/completion. The Ethernet worker and SPI are already gone.
if (!canReleaseRakEthernet() || !fetch_store.resetSelection()) return false;
releaseRakIdleServingBuffers();
rak4631_set_ethernet_spi_owner(false);
refreshRakStorageCapabilities();
return true;
}
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
OtaStoreSdNrf52& sdStagingStore() {
#if defined(OTA_TOWER_AUTO_STORE)
+8
View File
@@ -1,4 +1,7 @@
#include "OtaDeflate.h"
#if defined(RAK4631_COMBINED_ETHERNET)
#include "../nrf52/Rak4631SharedSpi.h"
#endif
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST)
@@ -30,6 +33,11 @@ bool ota_transport_deflate(void* context, const uint8_t* src, uint16_t src_len,
uint8_t* dst, uint16_t dst_cap, uint16_t* dst_len) {
(void)context;
if (dst_len) *dst_len = 0;
#if defined(RAK4631_COMBINED_ETHERNET)
// The Ethernet worker and encoder workspace share the combined image's
// runtime RAM allowance. Refuse before tinydeflate can allocate its table.
if (rak4631_ethernet_owns_spi()) return false;
#endif
if (!src || src_len < 3 || src_len > MESHCORE_OTA_DEFLATE_BLOCK_MAX ||
!dst || !dst_cap || !dst_len) return false;
// Proof/output scratch is disjoint from the manager's raw input. Reject
+5
View File
@@ -198,6 +198,11 @@ static uint32_t parse_fw_version(const char* s) {
}
bool ota_serve_self(OtaContext& c, uint32_t fw_version) {
#if defined(RAK4631_COMBINED_ETHERNET)
// Also cover diagnostic `ota dev serve self`, which bypasses the public
// default-self-serve gate. Ethernet owns the alternative RAM workspace.
if (rak4631_ethernet_owns_spi()) return false;
#endif
// Derive our version from the build string when the caller didn't supply one, so the mOTA we advertise
// carries a real version (was hard-coded 0 -> peers saw "v0.0.0"). A dev build with no dotted number
// still reads 0 - the self-describing EndF identity (docs) is the durable fix for that.
+44
View File
@@ -8,6 +8,9 @@
#include "OtaStoreFlashNrf52.h"
#include "OtaStoreQspiNrf52.h"
#include "OtaRakStoragePolicy.h"
#if defined(RAK4631_COMBINED_ETHERNET)
#include "../nrf52/Rak4631SharedSpi.h"
#endif
#include <new>
namespace mesh {
@@ -63,7 +66,12 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
void select() const {
if (_mode != UNSELECTED) return;
#if defined(RAK4631_COMBINED_ETHERNET)
const bool ethernet = rak4631_ethernet_owns_spi();
const uint8_t detected = ethernet ? 0u : OtaStoreQspiNrf52::autoDetect();
#else
const uint8_t detected = OtaStoreQspiNrf52::autoDetect();
#endif
const OtaBlCaps caps = ota_bootloader_app_caps();
if (detected == 3u) {
_mode = UNSAFE;
@@ -92,6 +100,12 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
caps.optional_app_storage == (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25));
if (choice == RakStorageChoice::Internal) {
activateInternal();
#if defined(RAK4631_COMBINED_ETHERNET)
if (ethernet) {
_reason = "Ethernet active; internal application deltas only";
return;
}
#endif
_reason = detected == 0u ? "no external NOR" :
"external NOR fitted; internal bootloader";
return;
@@ -103,6 +117,12 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
return;
}
_mode = UNSAFE;
#if defined(RAK4631_COMBINED_ETHERNET)
if (ethernet) {
_reason = "Ethernet active; bootloader lacks internal OTA";
return;
}
#endif
if (detected == 0u) {
_reason = "QSPI bootloader but no matched external NOR";
} else if (!identity_valid) {
@@ -114,16 +134,40 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
OtaStore* active() {
select();
#if defined(RAK4631_COMBINED_ETHERNET)
if (_mode == QSPI_MODE && rak4631_ethernet_owns_spi()) return nullptr;
#endif
return _mode == QSPI_MODE ? static_cast<OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<OtaStore*>(&_storage.internal) : nullptr;
}
const OtaStore* active() const {
select();
#if defined(RAK4631_COMBINED_ETHERNET)
if (_mode == QSPI_MODE && rak4631_ethernet_owns_spi()) return nullptr;
#endif
return _mode == QSPI_MODE ? static_cast<const OtaStore*>(&_storage.external) :
_mode == INTERNAL_MODE ? static_cast<const OtaStore*>(&_storage.internal) : nullptr;
}
public:
#if defined(RAK4631_COMBINED_ETHERNET)
// Only the main-loop handoff may reset the selection, after OtaContext has
// proved that no receive/apply session is active. Never discard staged data
// as a side effect of changing a network setting.
bool resetSelection() {
if (_mode == QSPI_MODE) {
if (_storage.external.staged_size() != 0u) return false;
_storage.external.~OtaStoreQspiNrf52();
} else if (_mode == INTERNAL_MODE) {
if (_storage.internal.staged_size() != 0u) return false;
_storage.internal.~OtaStoreFlashNrf52();
}
_mode = UNSELECTED;
_reason = "not probed";
_bootloader_store = false;
return true;
}
#endif
~OtaStoreAdaptiveNrf52() override {
if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52();
else if (_mode == INTERNAL_MODE) _storage.internal.~OtaStoreFlashNrf52();
+28 -3
View File
@@ -13,6 +13,9 @@
#include <SPI.h>
#endif
#include <string.h>
#if defined(RAK4631_COMBINED_ETHERNET)
#include "../nrf52/Rak4631SharedSpi.h"
#endif
namespace mesh {
namespace ota {
@@ -324,6 +327,11 @@ static uint32_t probe_nor(uint8_t cs) {
#if defined(OTA_RAK_AUTO_STORE)
uint8_t OtaStoreQspiNrf52::autoDetect() {
#if defined(RAK4631_COMBINED_ETHERNET)
// Do not even bit-bang a JEDEC probe while the Ethernet task owns SPI.
// Leave the cached physical discovery unchanged for the next idle handoff.
if (rak4631_ethernet_owns_spi()) return 0u;
#endif
if (auto_detection != 0xFFu) return auto_detection;
const uint8_t w25_cs = arduino_to_physical(31);
@@ -479,6 +487,12 @@ bool OtaStoreQspiNrf52::waitMemoryReady(uint32_t timeout_ms) {
}
bool OtaStoreQspiNrf52::ensureFlash() {
#if defined(RAK4631_COMBINED_ETHERNET)
if (rak4631_ethernet_owns_spi()) {
fail("Ethernet owns WisBlock SPI; external OTA unavailable");
return false;
}
#endif
if (_qspi_ready) return true;
#if defined(OTA_QSPI_RAK3401_RADIO_BUS_HANDOFF)
@@ -613,7 +627,11 @@ bool OtaStoreQspiNrf52::ensureFlash() {
}
void OtaStoreQspiNrf52::releaseFlash() {
if (_qspi_awake && !_memory_operation_pending) {
bool send_power_down = _qspi_awake && !_memory_operation_pending;
#if defined(RAK4631_COMBINED_ETHERNET)
send_power_down = send_power_down && !rak4631_ethernet_owns_spi();
#endif
if (send_power_down) {
// The repeater can remain idle for hours after a capacity/status probe or
// a completed checkpoint. Put the NOR into deep power-down before
// releasing the nRF QSPI peripheral instead of leaving both active for
@@ -632,7 +650,14 @@ void OtaStoreQspiNrf52::releaseFlash() {
// disabling the peripheral. Trigger it after every successful ENABLE, even
// when wake or JEDEC identification failed before _qspi_ready was set.
if (_qspi_active) {
if (_memory_operation_pending) {
#if defined(RAK4631_COMBINED_ETHERNET)
// Also release the QSPI pin assignments after a successful operation so
// the Ethernet SPIM can safely acquire the same pads at an idle handoff.
const bool disconnect_pins = true;
#else
const bool disconnect_pins = _memory_operation_pending;
#endif
if (disconnect_pins) {
// QSPI owns pin direction while enabled. Preload and configure CS# high
// before releasing the peripheral so an unresolved NOR operation cannot
// see a floating or asserted chip select during the handoff.
@@ -645,7 +670,7 @@ void OtaStoreQspiNrf52::releaseFlash() {
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_DEACTIVATE);
nrf_qspi_disable(NRF_QSPI);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
if (_memory_operation_pending) {
if (disconnect_pins) {
#if defined(OTA_QSPI_RAK3401_RADIO_BUS_HANDOFF)
disconnect_qspi_pins();
#else
+8 -1
View File
@@ -6,10 +6,17 @@
#error "OTA_QSPI_STORE raw staging cannot share a QSPI chip with QSPIFLASH"
#endif
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED)
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED) && \
!defined(RAK4631_COMBINED_ETHERNET)
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with Ethernet"
#endif
#if defined(RAK4631_COMBINED_ETHERNET) && \
(!defined(NRF52_PLATFORM) || !defined(RAK_4631) || !defined(OTA_RAK_AUTO_STORE) || \
!defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || !defined(ETHERNET_ENABLED))
#error "Combined RAK4631 Ethernet requires its adaptive OTA and shared-SPI ownership contract"
#endif
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE)
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with SD staging"
#endif
+11
View File
@@ -1,5 +1,16 @@
#include "SSD1306Display.h"
#if defined(NRF52_PLATFORM) && defined(RAK4631_COMBINED_ETHERNET)
// Adafruit_SSD1306::begin allocates WIDTH * ((HEIGHT + 7) / 8) once and
// reuses that framebuffer on later begin calls. The driver itself is embedded
// in the linked global display, not allocated on the heap. Even including
// that object again plus allocator overhead fits the combined 2 KiB bound.
static_assert(SSD1306Display::FRAMEBUFFER_BYTES == 1024,
"Update the combined RAK OLED framebuffer budget");
static_assert(sizeof(SSD1306Display) + SSD1306Display::FRAMEBUFFER_BYTES + 32 <= 2048,
"Update the combined RAK OLED runtime budget");
#endif
bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
+7 -2
View File
@@ -16,6 +16,11 @@
#endif
class SSD1306Display : public DisplayDriver {
public:
static constexpr int16_t PANEL_WIDTH = 128;
static constexpr int16_t PANEL_HEIGHT = 64;
static constexpr size_t FRAMEBUFFER_BYTES = PANEL_WIDTH * ((PANEL_HEIGHT + 7) / 8);
private:
Adafruit_SSD1306 display;
bool _isOn;
bool _panel_ready = false;
@@ -32,8 +37,8 @@ class SSD1306Display : public DisplayDriver {
bool i2c_probe(TwoWire& wire, uint8_t addr);
void applyRotation();
public:
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
display(128, 64, &Wire, PIN_OLED_RESET),
SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(PANEL_WIDTH, PANEL_HEIGHT),
display(PANEL_WIDTH, PANEL_HEIGHT, &Wire, PIN_OLED_RESET),
_peripher_power(peripher_power)
{
_isOn = false;
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <atomic>
#include <cassert>
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
class Print {
public:
virtual ~Print() = default;
virtual size_t write(uint8_t) = 0;
virtual size_t write(const uint8_t* data, size_t size) {
size_t written = 0;
while (written < size && write(data[written])) ++written;
return written;
}
size_t write(const char* data) {
return write(reinterpret_cast<const uint8_t*>(data), std::strlen(data));
}
size_t print(const char* data) { return write(data); }
size_t println(const char* data = "") { return print(data) + print("\r\n"); }
size_t printf(const char* format, ...) {
char buffer[256];
va_list args;
va_start(args, format);
const int length = vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
assert(length >= 0 && length < static_cast<int>(sizeof(buffer)));
return write(reinterpret_cast<const uint8_t*>(buffer), static_cast<size_t>(length));
}
};
class Stream : public Print {
public:
virtual int available() = 0;
virtual int read() = 0;
virtual int peek() = 0;
virtual void flush() = 0;
virtual int availableForWrite() { return 0; }
};
extern std::atomic<uint32_t> mock_millis;
inline uint32_t millis() { return mock_millis.load(); }
inline uint32_t pdMS_TO_TICKS(uint32_t milliseconds) { return milliseconds; }
struct MockFicr { uint32_t DEVICEID[2] = {0x123456, 0}; };
extern MockFicr mock_ficr;
#define NRF_FICR (&mock_ficr)
#define NRF_SPIM1 1
#define WB_IO2 34
#define OUTPUT 1
#define HIGH 1
#define LOW 0
void pinMode(int pin, int mode);
void digitalWrite(int pin, int value);
struct MockTask;
using TaskHandle_t = MockTask*;
using BaseType_t = int;
struct StaticTask_t { uint8_t bytes[192]; };
constexpr int pdPASS = 1;
constexpr int pdTRUE = 1;
constexpr uint32_t portMAX_DELAY = UINT32_MAX;
extern std::recursive_mutex mock_scheduler_mutex;
#define taskENTER_CRITICAL() mock_scheduler_mutex.lock()
#define taskEXIT_CRITICAL() mock_scheduler_mutex.unlock()
BaseType_t xTaskCreate(void (*function)(void*), const char*, unsigned stack_words,
void* argument, int priority, TaskHandle_t* handle);
void xTaskNotifyGive(TaskHandle_t handle);
uint32_t ulTaskNotifyTake(int clear_on_exit, uint32_t ticks);
void vTaskDelay(uint32_t ticks);
void vTaskDelete(TaskHandle_t handle);
void mock_bus_touch();
void mock_worker_bus_touch();
void mock_spi_begin();
void mock_spi_end();
@@ -0,0 +1,82 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
class IPAddress {
uint32_t address;
public:
explicit IPAddress(uint32_t value = 0x2a01a8c0) : address(value) {}
uint8_t operator[](size_t index) const { return (address >> (8 * index)) & 255; }
};
extern std::atomic<bool> mock_socket_connected[4];
extern std::atomic<int> mock_stop_count[4];
extern std::atomic<bool> mock_accept_client;
extern std::string mock_ethernet_input;
extern std::string mock_ethernet_output;
extern std::atomic<int> mock_write_capacity;
class EthernetClient : public Stream {
int socket_index = -1;
public:
EthernetClient() = default;
explicit EthernetClient(uint8_t index) : socket_index(index) {}
explicit operator bool() const { return socket_index >= 0 && socket_index < 4; }
uint8_t getSocketNumber() const { return socket_index < 0 ? 255 : socket_index; }
bool connected() const {
mock_bus_touch();
return socket_index >= 0 && mock_socket_connected[socket_index].load();
}
void setConnectionTimeout(uint16_t milliseconds) { assert(milliseconds == 50); }
void stop() {
assert(false && "vendor blocking socket stop must not run");
}
int available() override { mock_bus_touch(); return mock_ethernet_input.size(); }
int read() override {
mock_bus_touch();
if (mock_ethernet_input.empty()) return -1;
const uint8_t next = mock_ethernet_input.front();
mock_ethernet_input.erase(0, 1);
return next;
}
int peek() override {
mock_bus_touch();
return mock_ethernet_input.empty() ? -1 : static_cast<uint8_t>(mock_ethernet_input.front());
}
int availableForWrite() override { assert(false && "vendor free-size spin must not run"); return 0; }
size_t write(uint8_t value) override { return write(&value, 1); }
size_t write(const uint8_t* data, size_t size) override {
(void)data; (void)size;
assert(false && "vendor blocking socketSend must not run");
return 0;
}
void flush() override { assert(false && "raw blocking flush must not be called"); }
using Print::write;
};
class EthernetServer {
public:
explicit EthernetServer(int port) { assert(port == 23); }
void begin() { mock_worker_bus_touch(); }
EthernetClient accept() {
mock_bus_touch();
if (!mock_accept_client.exchange(false)) return {};
mock_socket_connected[0].store(true);
return EthernetClient(uint8_t{0});
}
};
enum { EthernetNoHardware = 0, EthernetPresent = 1, LinkOFF = 0, LinkON = 1 };
class MockEthernet {
public:
std::atomic<int> begin_result{1};
std::atomic<int> hardware{EthernetPresent};
std::atomic<int> maintain_result{0};
std::atomic<uint32_t> ip{0x2a01a8c0};
std::atomic<int> begin_count{0};
std::atomic<int> maintain_count{0};
std::atomic<bool> begin_blocked{false};
std::atomic<bool> maintain_blocked{false};
void init(SPIClass&, int chip_select);
int begin(uint8_t* mac, int timeout, int response_timeout);
int hardwareStatus() const { mock_worker_bus_touch(); return hardware.load(); }
IPAddress localIP() const { mock_worker_bus_touch(); return IPAddress(ip.load()); }
int maintain();
};
extern MockEthernet Ethernet;
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <Arduino.h>
struct SPISettings {};
class SPIClass {
public:
SPIClass(int, int, int, int) {}
void begin() { mock_spi_begin(); }
void end() { mock_spi_end(); }
void beginTransaction(const SPISettings&) { mock_bus_touch(); }
void endTransaction() { mock_bus_touch(); }
};
@@ -0,0 +1,14 @@
#pragma once
#include <Arduino.h>
namespace mesh {
inline bool isUsbDebugLoggingEnabled() { return false; }
class NoUsbLog : public Stream {
public:
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t write(uint8_t) override { assert(false); return 0; }
};
inline Stream& usbLoggingPort() { static NoUsbLog port; return port; }
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include <array>
#define SPI_ETHERNET_SETTINGS SPISettings{}
enum SockCMD { Sock_CLOSE = 0x10, Sock_SEND = 0x20, Sock_RECV = 0x40 };
struct SnSR { enum { CLOSED = 0, ESTABLISHED = 0x17, CLOSE_WAIT = 0x1c }; };
struct SnIR { enum { SEND_OK = 0x10, TIMEOUT = 8 }; };
extern SPIClass* mock_w5100_spi;
extern std::atomic<bool> mock_ack_send;
extern std::atomic<bool> mock_stuck_command;
extern std::atomic<bool> mock_hold_terminal_recv;
extern std::atomic<bool> mock_replace_during_parser;
void mock_replace_while_parser_active();
extern std::mutex mock_output_mutex;
class MockW5100 {
public:
static constexpr uint16_t SSIZE = 2048;
static constexpr uint16_t SMASK = SSIZE - 1;
std::array<std::array<uint8_t, SSIZE>, 4> memory{};
uint16_t tx_wr[4] = {}, committed[4] = {};
uint16_t rx_rd[4] = {};
uint8_t interrupt[4] = {};
mutable uint8_t command[4] = {};
uint8_t getChip() const { return 51; }
SPIClass* getSPI() const { return mock_w5100_spi; }
uint16_t SBASE(uint8_t socket) const { return 0x4000 + socket * SSIZE; }
uint16_t RBASE(uint8_t socket) const { return 0x6000 + socket * SSIZE; }
uint8_t readSnSR(uint8_t socket) const {
mock_bus_touch();
return mock_socket_connected[socket].load() ? SnSR::ESTABLISHED : SnSR::CLOSED;
}
uint8_t readSnIR(uint8_t socket) const { mock_bus_touch(); return interrupt[socket]; }
uint8_t readSnCR(uint8_t socket) const {
mock_bus_touch();
if (command[socket] == Sock_RECV && !mock_stuck_command.load() && !mock_hold_terminal_recv.load()) {
command[socket] = 0;
}
return command[socket];
}
uint16_t readSnTX_FSR(uint8_t) const { mock_bus_touch(); return mock_write_capacity.load(); }
uint16_t readSnTX_WR(uint8_t socket) const { mock_bus_touch(); return tx_wr[socket]; }
uint16_t readSnRX_RSR(uint8_t) const { mock_bus_touch(); return mock_ethernet_input.size(); }
uint16_t readSnRX_RD(uint8_t socket) const { mock_bus_touch(); return rx_rd[socket]; }
void writeSnRX_RD(uint8_t socket, uint16_t value) { mock_bus_touch(); rx_rd[socket] = value; }
void writeSnTX_WR(uint8_t socket, uint16_t value) { mock_bus_touch(); tx_wr[socket] = value; }
void writeSnIR(uint8_t socket, uint8_t bits) { mock_bus_touch(); interrupt[socket] &= ~bits; }
uint16_t write(uint16_t address, const uint8_t* data, uint16_t count) {
mock_bus_touch();
const uint16_t relative = address - 0x4000;
const uint8_t socket = relative / SSIZE;
const uint16_t offset = relative % SSIZE;
assert(socket < 4 && offset + count <= SSIZE);
memcpy(memory[socket].data() + offset, data, count);
return count;
}
uint16_t read(uint16_t address, uint8_t* data, uint16_t count) {
mock_bus_touch();
assert(address >= 0x6000 && count == 1 && !mock_ethernet_input.empty());
data[0] = static_cast<uint8_t>(mock_ethernet_input.front());
return 1;
}
void writeSnCR(uint8_t socket, uint8_t value) {
mock_bus_touch();
assert(command[socket] == 0 && "commands must never overwrite a stalled controller command");
if (value == Sock_RECV) {
assert(!mock_ethernet_input.empty());
if (mock_replace_during_parser.exchange(false)) mock_replace_while_parser_active();
const char consumed = mock_ethernet_input.front();
mock_ethernet_input.erase(0, 1);
command[socket] = mock_stuck_command.load() || (consumed == '\r' && mock_hold_terminal_recv.load()) ? Sock_RECV : 0;
return;
}
if (value == Sock_CLOSE) {
++mock_stop_count[socket];
mock_socket_connected[socket].store(false);
return;
}
assert(value == Sock_SEND);
{
std::lock_guard<std::mutex> lock(mock_output_mutex);
for (uint16_t pointer = committed[socket]; pointer != tx_wr[socket]; ++pointer) {
mock_ethernet_output.push_back(memory[socket][pointer & SMASK]);
}
}
committed[socket] = tx_wr[socket];
command[socket] = mock_stuck_command.load() ? Sock_SEND : 0;
interrupt[socket] = mock_ack_send.load() ? SnIR::SEND_OK : 0;
}
};
extern MockW5100 W5100;
+737
View File
@@ -0,0 +1,737 @@
// Runs the actual opt-in production Ethernet helper on a fake RTOS and SPI
// device. Threads deliberately hold DHCP calls while the mesh loop continues.
#include <Arduino.h>
#include <RAK13800_W5100S.h>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <memory>
#include <thread>
#include <vector>
std::atomic<uint32_t> mock_millis{0};
MockFicr mock_ficr;
std::recursive_mutex mock_scheduler_mutex;
std::atomic<bool> mock_socket_connected[4];
std::atomic<int> mock_stop_count[4];
std::atomic<bool> mock_accept_client{false};
std::string mock_ethernet_input;
std::string mock_ethernet_output;
std::atomic<int> mock_write_capacity{2048};
MockEthernet Ethernet;
SPIClass* mock_w5100_spi = nullptr;
std::atomic<bool> mock_ack_send{true};
std::atomic<bool> mock_stuck_command{false};
std::atomic<bool> mock_hold_terminal_recv{false};
std::atomic<bool> mock_replace_during_parser{false};
static std::atomic<int> worker_access_waits{0};
std::mutex mock_output_mutex;
static const std::thread::id main_thread = std::this_thread::get_id();
static std::atomic<bool> bus_owner{false};
static std::atomic<bool> spi_active{false};
static std::atomic<int> hardware_operations{0};
static std::atomic<int> main_hardware_operations{0};
static std::atomic<int> live_task_allocations{0};
static std::atomic<int> task_deletions{0};
static bool fail_task_creation = false;
static std::atomic<bool> defer_task_start{false};
static std::mutex library_mutex;
static std::condition_variable library_changed;
struct TaskDeleted {};
struct MockTask {
std::thread thread;
std::mutex mutex;
std::condition_variable changed;
unsigned notifications = 0;
bool deleted = false;
};
static thread_local MockTask* current_task = nullptr;
static std::vector<std::unique_ptr<MockTask>> tasks;
#include <helpers/nrf52/EthernetCLI.h>
MockW5100 W5100;
static void wait_until(const std::function<bool()>& condition) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (!condition()) {
assert(std::chrono::steady_clock::now() < deadline && "worker did not reach expected state");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void mock_bus_touch() {
assert(bus_owner.load() && spi_active.load());
assert(ethernet_spi_access.load() != EthernetSpiAccess::Idle);
++hardware_operations;
if (std::this_thread::get_id() == main_thread) ++main_hardware_operations;
}
void mock_worker_bus_touch() {
assert(current_task != nullptr && std::this_thread::get_id() != main_thread);
assert(ethernet_spi_access.load() == EthernetSpiAccess::Worker);
mock_bus_touch();
}
void mock_spi_begin() {
assert(current_task != nullptr && bus_owner.load());
assert(!spi_active.exchange(true));
++hardware_operations;
}
void mock_spi_end() {
assert(bus_owner.load());
assert(spi_active.exchange(false));
++hardware_operations;
}
void pinMode(int pin, int mode) { assert(pin == 21 && mode == OUTPUT); }
void digitalWrite(int pin, int value) {
assert((pin == 21 || pin == 26) && value == HIGH);
}
BaseType_t xTaskCreate(void (*function)(void*), const char* name, unsigned words,
void* argument, int priority, TaskHandle_t* handle) {
assert(std::this_thread::get_id() == main_thread);
assert(std::string(name) == "eth_init" && words == 1024 && priority == 1);
if (fail_task_creation) return 0;
auto created = std::make_unique<MockTask>();
MockTask* raw = created.get();
*handle = raw;
++live_task_allocations;
raw->thread = std::thread([raw, function, argument] {
current_task = raw;
try {
{ std::lock_guard<std::recursive_mutex> lock(mock_scheduler_mutex); }
{
std::unique_lock<std::mutex> lock(library_mutex);
library_changed.wait(lock, [] { return !defer_task_start.load(); });
}
function(argument);
assert(false && "worker must park for main-thread deletion");
} catch (const TaskDeleted&) {}
});
tasks.push_back(std::move(created));
return pdPASS;
}
void xTaskNotifyGive(TaskHandle_t handle) {
assert(handle != nullptr);
std::lock_guard<std::mutex> lock(handle->mutex);
assert(!handle->deleted && "stale task notification");
++handle->notifications;
handle->changed.notify_all();
}
uint32_t ulTaskNotifyTake(int clear_on_exit, uint32_t ticks) {
assert(current_task != nullptr && clear_on_exit == pdTRUE);
std::unique_lock<std::mutex> lock(current_task->mutex);
const auto ready = [] { return current_task->notifications || current_task->deleted; };
if (ticks == portMAX_DELAY) current_task->changed.wait(lock, ready);
else if (!current_task->changed.wait_for(lock, std::chrono::milliseconds(ticks), ready)) {
mock_millis.fetch_add(ticks);
}
if (current_task->deleted) throw TaskDeleted{};
const uint32_t notifications = current_task->notifications;
current_task->notifications = 0;
return notifications;
}
void vTaskDelay(uint32_t ticks) {
assert(current_task != nullptr);
if (ticks == portMAX_DELAY) {
std::unique_lock<std::mutex> lock(current_task->mutex);
current_task->changed.wait(lock, [] { return current_task->deleted; });
throw TaskDeleted{};
}
if (ticks == 1) ++worker_access_waits;
std::this_thread::sleep_for(std::chrono::milliseconds(ticks));
}
void vTaskDelete(TaskHandle_t handle) {
assert(std::this_thread::get_id() == main_thread && handle != nullptr);
assert(ethernet_worker_stopped.load() && !spi_active.load());
assert(bus_owner.load() && "owner must remain held until stack reclaimed");
{
std::lock_guard<std::mutex> lock(handle->mutex);
assert(!handle->deleted);
handle->deleted = true;
handle->changed.notify_all();
}
handle->thread.join();
--live_task_allocations;
++task_deletions;
}
int MockEthernet::begin(uint8_t* mac, int timeout, int response_timeout) {
mock_worker_bus_touch();
assert(mac[0] == 2 && mac[3] == 0x12 && mac[4] == 0x34 && mac[5] == 0x56);
assert(timeout == 10000 && response_timeout == 2000);
++begin_count;
std::unique_lock<std::mutex> lock(library_mutex);
library_changed.wait(lock, [this] { return !begin_blocked.load(); });
return begin_result.load();
}
void MockEthernet::init(SPIClass& spi, int chip_select) {
assert(chip_select == 26);
mock_worker_bus_touch();
mock_w5100_spi = &spi;
W5100 = MockW5100{};
}
int MockEthernet::maintain() {
mock_worker_bus_touch();
++maintain_count;
std::unique_lock<std::mutex> lock(library_mutex);
library_changed.wait(lock, [this] { return !maintain_blocked.load(); });
return maintain_result.load();
}
struct Context {
bool prepare_allowed = true;
bool off_allowed = true;
bool release_allowed = true;
bool save_allowed = true;
unsigned prepares = 0, releases = 0, saves = 0;
};
static bool prepare(void* opaque, char* error, size_t capacity) {
assert(std::this_thread::get_id() == main_thread);
auto& context = *static_cast<Context*>(opaque);
++context.prepares;
assert(!bus_owner.load() && !spi_active.load() && !live_task_allocations.load());
if (!context.prepare_allowed) {
snprintf(error, capacity, "Error: flash conflict or active OTA");
return false;
}
bus_owner.store(true);
return true;
}
static bool can_release(void* opaque) {
assert(std::this_thread::get_id() == main_thread);
return static_cast<Context*>(opaque)->off_allowed;
}
static bool release(void* opaque) {
assert(std::this_thread::get_id() == main_thread);
auto& context = *static_cast<Context*>(opaque);
++context.releases;
assert(bus_owner.load() && !spi_active.load());
assert(live_task_allocations.load() == 0 && "OTA workspace cannot overlap deferred task memory");
if (!context.release_allowed) return false;
bus_owner.store(false);
return true;
}
static bool save(void* opaque, bool) {
assert(std::this_thread::get_id() == main_thread);
auto& context = *static_cast<Context*>(opaque);
++context.saves;
return context.save_allowed;
}
static void configure(Context& context, bool persisted = false, bool saved_on = false) {
EthernetCliHooks hooks;
hooks.prepare = prepare;
hooks.can_release = can_release;
hooks.released = release;
hooks.save = persisted ? save : nullptr;
hooks.context = &context;
assert(ethernet_configure(hooks, saved_on));
}
static std::string command(const char* text) {
char reply[160] = {};
// Commands used directly by this fixture stand in for a complete input line.
ethernet_reply_generation = ethernet_session_generation.load();
assert(ethernet_handle_command(text, reply));
return reply;
}
static void start_online() {
assert(command("eth on").find("starting until restart") != std::string::npos);
wait_until([] { return ethernet_is_running(); });
assert(ethernet_state.load() == EthernetCliState::Online);
assert(ethernet_worker_active.load() && live_task_allocations.load() == 1);
}
static void assert_busy_facade() {
const int before = main_hardware_operations.load();
assert(!ethernet_client.connected() && !static_cast<bool>(ethernet_client));
assert(ethernet_client.available() == 0 && ethernet_client.availableForWrite() == 0);
assert(ethernet_client.read() == -1 && ethernet_client.peek() == -1);
assert(ethernet_client.write(uint8_t{1}) == 0);
const uint8_t bytes[] = {1, 2, 3};
Stream* retained_mesh_output = &ethernet_client;
assert(retained_mesh_output->write(bytes, sizeof(bytes)) == 0);
retained_mesh_output->flush();
ethernet_client.stop();
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)));
ethernet_send_reply("paused");
assert(main_hardware_operations.load() == before);
}
static void finish_off() {
assert(command("eth off").find("until restart") != std::string::npos);
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (ethernet_state.load() != EthernetCliState::Off) {
ethernet_loop_maintain();
assert(std::chrono::steady_clock::now() < deadline);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
assert(!bus_owner.load() && !spi_active.load() && live_task_allocations.load() == 0);
assert(!ethernet_get_enabled() && !ethernet_is_running());
assert(ethernet_task_handle == nullptr);
}
static void unblock(std::atomic<bool>& flag) { flag.store(false); library_changed.notify_all(); }
static std::string output() {
std::lock_guard<std::mutex> lock(mock_output_mutex);
return mock_ethernet_output;
}
void mock_replace_while_parser_active() {
const uint32_t before = ethernet_session_generation.load();
const int previous_waits = worker_access_waits.load();
mock_accept_client.store(true);
wait_until([&] { return worker_access_waits.load() > previous_waits; });
// The background worker attempted to acquire SPI during this record. It
// must remain excluded until both input and reply-owner capture finish.
assert(ethernet_spi_access.load() == EthernetSpiAccess::Client);
assert(ethernet_session_generation.load() == before);
}
static void accept_connection() {
mock_accept_client.store(true);
ethernet_loop_maintain();
wait_until([] { return output().find("MeshCore CLI\r\n") != std::string::npos && ethernet_is_running(); });
}
int main(int argc, char** argv) {
assert(argc == 2);
const int scenario = std::stoi(argv[1]);
Context context;
configure(context);
switch (scenario) {
case 0: { // Disabled boot, strict parser and mandatory bus hooks.
ethernet_start_task();
assert(command("get eth") == "> off");
assert(command("eth.status") == "ETH: off");
assert(command("eth true").find("must be on or off") != std::string::npos);
assert(command("set eth on extra").find("must be on or off") != std::string::npos);
char reply[160];
assert(!ethernet_handle_command("eth.status extra", reply));
assert_busy_facade();
EthernetCliHooks missing;
assert(ethernet_configure(missing));
assert(command("eth on").find("hooks unavailable") != std::string::npos);
assert(hardware_operations.load() == 0 && context.prepares == 0);
break;
}
case 1: { // OTA or known flash conflict refuses before any SPI operation.
context.prepare_allowed = false;
assert(command("set eth on").find("flash conflict") != std::string::npos);
assert(!ethernet_get_enabled() && context.prepares == 1 && !bus_owner.load());
assert(hardware_operations.load() == 0);
break;
}
case 2: { // Allocation failure releases ownership without a deferred task.
fail_task_creation = true;
assert(command("eth on").find("allocation failed") != std::string::npos);
assert(context.releases == 1 && !bus_owner.load() && !ethernet_worker_active.load());
assert(command("eth.status") == "ETH: start failed");
assert(hardware_operations.load() == 0);
break;
}
case 3: { // OFF before worker gets scheduled never initializes hardware.
defer_task_start.store(true);
assert(command("eth on").find("starting") != std::string::npos);
assert(command("eth off").find("stopping") != std::string::npos);
assert(bus_owner.load() && context.releases == 0);
unblock(defer_task_start);
wait_until([] { return ethernet_worker_stopped.load(); });
assert(hardware_operations.load() == 0 && task_deletions.load() == 0);
ethernet_loop_maintain();
assert(task_deletions.load() == 1 && context.releases == 1 && !bus_owner.load());
break;
}
case 4: { // OFF during blocking DHCP is cooperative and cannot release SPI early.
Ethernet.begin_blocked.store(true);
assert(command("eth on").find("starting") != std::string::npos);
wait_until([] { return Ethernet.begin_count.load() == 1; });
assert_busy_facade();
assert(command("eth off").find("stopping") != std::string::npos);
ethernet_loop_maintain();
assert(context.releases == 0 && bus_owner.load() && task_deletions.load() == 0);
unblock(Ethernet.begin_blocked);
wait_until([] { return ethernet_worker_stopped.load(); });
ethernet_loop_maintain();
assert(task_deletions.load() == 1 && context.releases == 1 && !bus_owner.load());
for (const auto& count : mock_stop_count) assert(count.load() == 1);
break;
}
case 5: { // OFF wakes the 30-second retry wait rather than waiting its timeout.
Ethernet.begin_result.store(0);
assert(command("eth on").find("starting") != std::string::npos);
wait_until([] { return ethernet_state.load() == EthernetCliState::Retrying; });
assert(command("eth.status") == "ETH: waiting for DHCP");
finish_off();
assert(Ethernet.begin_count.load() == 1 && task_deletions.load() == 1);
break;
}
case 6: { // Absent hardware releases its parked task in the main loop.
Ethernet.begin_result.store(0);
Ethernet.hardware.store(EthernetNoHardware);
assert(command("eth on").find("starting") != std::string::npos);
wait_until([] { return ethernet_worker_stopped.load(); });
assert(command("eth.status") == "ETH: hardware not found");
assert(context.releases == 0 && live_task_allocations.load() == 1);
ethernet_loop_maintain();
assert(!bus_owner.load() && context.releases == 1 && live_task_allocations.load() == 0);
for (const auto& count : mock_stop_count) assert(count.load() == 0);
break;
}
case 7: { // Online framing, bounded writes and real disconnect reset the retained stream.
start_online();
assert(command("get eth") == "> on");
assert(command("eth on").find("already on") != std::string::npos);
const int before_status = hardware_operations.load();
assert(command("eth.status") == "ETH: 192.168.1.42:23");
assert(hardware_operations.load() == before_status);
accept_connection();
assert(ethernet_take_session_reset() && !ethernet_take_session_reset());
assert(ethernet_client.connected());
mock_ethernet_input = "eth.status\r\n";
char line[32] = {};
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "eth.status");
wait_until([] { return ethernet_is_running(); });
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>("abcde"), 5) == 5);
wait_until([] { return output().find("abcde") != std::string::npos && ethernet_is_running(); });
mock_socket_connected[0].store(false);
wait_until([] { return ethernet_session_reset.load() && !ethernet_client_attached.load(); });
wait_until([] { return ethernet_is_running(); });
assert(ethernet_take_session_reset() && !ethernet_client.connected());
finish_off();
break;
}
case 8: { // Renewal owns SPI; every retained Stream call yields without raw IO.
start_online();
Ethernet.maintain_blocked.store(true);
mock_millis.store(1000);
const auto before = std::chrono::steady_clock::now();
ethernet_loop_maintain();
assert(std::chrono::steady_clock::now() - before < std::chrono::milliseconds(100));
wait_until([] { return Ethernet.maintain_count.load() == 1; });
assert(command("eth.status") == "ETH: renewing DHCP");
assert_busy_facade();
for (int index = 0; index < 20; ++index) ethernet_loop_maintain();
assert(context.releases == 0 && !ethernet_take_session_reset());
unblock(Ethernet.maintain_blocked);
wait_until([] { return ethernet_is_running(); });
assert(command("eth.status") == "ETH: 192.168.1.42:23");
finish_off();
break;
}
case 9: { // Failed lease renewal does not block mesh service or restart the worker.
start_online();
Ethernet.maintain_result.store(1);
Ethernet.ip.store(0x3301a8c0);
mock_millis.store(1000);
ethernet_loop_maintain();
wait_until([] { return Ethernet.maintain_count.load() == 1 && ethernet_is_running(); });
assert(command("eth.status") == "ETH: 192.168.1.51:23");
assert(Ethernet.begin_count.load() == 1 && live_task_allocations.load() == 1);
finish_off();
break;
}
case 10: { // Active or staged OTA refuses OFF without affecting the active transport.
start_online();
context.off_allowed = false;
assert(command("eth off").find("locked by OTA") != std::string::npos);
assert(ethernet_get_enabled() && ethernet_is_running() && bus_owner.load());
assert(context.releases == 0 && task_deletions.load() == 0);
context.off_allowed = true;
finish_off();
break;
}
case 11: { // Ethernet-origin OFF can acknowledge before the next loop gates the bus.
start_online();
accept_connection();
const std::string reply = command("eth off");
assert(reply.find("stopping until restart") != std::string::npos);
ethernet_send_reply(reply.c_str());
assert(context.releases == 0 && task_deletions.load() == 0);
ethernet_loop_maintain();
wait_until([] { return ethernet_worker_stopped.load(); });
assert(output().find(reply) != std::string::npos);
assert_busy_facade();
assert(live_task_allocations.load() == 1 && bus_owner.load());
ethernet_loop_maintain();
assert(task_deletions.load() == 1 && context.releases == 1 && !bus_owner.load());
for (const auto& count : mock_stop_count) assert(count.load() >= 1);
assert(ethernet_take_session_reset());
break;
}
case 12: { // OTA race after preflight retains owner while release retries on the main loop.
start_online();
context.release_allowed = false;
assert(command("eth off").find("stopping") != std::string::npos);
ethernet_loop_maintain();
wait_until([] { return ethernet_worker_stopped.load(); });
ethernet_loop_maintain();
assert(task_deletions.load() == 1 && live_task_allocations.load() == 0);
assert(bus_owner.load() && !spi_active.load() && context.releases == 1);
assert(command("eth on").find("still stopping") != std::string::npos);
ethernet_loop_maintain();
assert(context.releases == 2 && bus_owner.load());
context.release_allowed = true;
ethernet_loop_maintain();
assert(context.releases == 3 && !bus_owner.load());
assert(ethernet_state.load() == EthernetCliState::Off);
break;
}
case 13: { // Optional save failures are transactional; prototype has no save hook.
configure(context, true);
context.save_allowed = false;
assert(command("eth on").find("cannot save") != std::string::npos);
assert(!bus_owner.load() && hardware_operations.load() == 0 && context.saves == 1);
context.save_allowed = true;
assert(command("eth on") == "OK - Ethernet starting; use eth.status");
wait_until([] { return ethernet_is_running(); });
context.save_allowed = false;
assert(command("eth off").find("cannot save") != std::string::npos);
assert(ethernet_get_enabled() && ethernet_is_running());
context.save_allowed = true;
assert(command("eth off") == "OK - Ethernet stopping");
ethernet_loop_maintain();
wait_until([] { return ethernet_worker_stopped.load(); });
ethernet_loop_maintain();
assert(!bus_owner.load() && live_task_allocations.load() == 0);
break;
}
case 14: { // Repeated hot switches reclaim each worker and cannot notify a stale handle.
for (int cycle = 0; cycle < 4; ++cycle) {
start_online();
assert(!ethernet_configure(EthernetCliHooks{}));
finish_off();
ethernet_notify_worker();
}
assert(task_deletions.load() == 4 && context.prepares == 4 && context.releases == 4);
assert(Ethernet.begin_count.load() == 4);
break;
}
case 15: { // A future saved-ON configuration starts without writing its settings again.
configure(context, true, true);
ethernet_start_task();
wait_until([] { return ethernet_is_running(); });
assert(context.saves == 0 && context.prepares == 1);
assert(command("eth off") == "OK - Ethernet stopping");
ethernet_loop_maintain();
wait_until([] { return ethernet_worker_stopped.load(); });
ethernet_loop_maintain();
assert(context.saves == 1 && !bus_owner.load());
break;
}
case 16: { // OFF during a slow renewal leaves bus reserved until that library call returns.
start_online();
Ethernet.maintain_blocked.store(true);
mock_millis.store(1000);
ethernet_loop_maintain();
wait_until([] { return Ethernet.maintain_count.load() == 1; });
assert(command("eth off").find("stopping") != std::string::npos);
for (int index = 0; index < 10; ++index) ethernet_loop_maintain();
assert_busy_facade();
assert(context.releases == 0 && task_deletions.load() == 0 && bus_owner.load());
unblock(Ethernet.maintain_blocked);
wait_until([] { return ethernet_worker_stopped.load(); });
ethernet_loop_maintain();
assert(context.releases == 1 && task_deletions.load() == 1 && !bus_owner.load());
break;
}
case 17: { // Bounded ring queue applies backpressure and preserves wrap-around order.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_write_capacity.store(0);
const std::string first(400, 'a');
const std::string second(400, 'b');
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>(first.data()), first.size()) == first.size());
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>(second.data()), second.size()) == 112);
assert(ethernet_client.availableForWrite() == 0);
assert(ethernet_client.write(uint8_t{'x'}) == 0);
mock_ethernet_input = "eth off\r";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && mock_ethernet_input == "eth off\r");
mock_write_capacity.store(2048);
wait_until([&] { return output().find(first + second.substr(0, 112)) != std::string::npos && ethernet_is_running(); });
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>(second.data() + 112), 288) == 288);
wait_until([&] { return output().find(first + second) != std::string::npos && ethernet_is_running(); });
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "eth off");
finish_off();
break;
}
case 18: { // Oversized records cannot execute their truncated command prefix.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ethernet_input = "eth off" + std::string(80, 'x') + "\rget eth\r";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && line[0] == 0);
assert(ethernet_get_enabled() && ethernet_take_session_reset());
wait_until([] { return ethernet_is_running(); });
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "get eth");
wait_until([] { return output().find("invalid or oversized command; discarded") != std::string::npos && ethernet_is_running(); });
line[0] = 0;
mock_ethernet_input = std::string(31, 'm') + "\r";
assert(ethernet_read_line(line, sizeof(line)) && std::strlen(line) == 31);
finish_off();
break;
}
case 19: { // Embedded NUL discards the entire record, including a valid-looking prefix.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ethernet_input = std::string("eth off\0junk", 12) + "\rget eth\r";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && line[0] == 0);
assert(ethernet_get_enabled() && ethernet_take_session_reset());
wait_until([] { return ethernet_is_running(); });
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "get eth");
finish_off();
break;
}
case 20: { // Missing TCP ACK retains Worker ownership and cannot block retained streams.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ack_send.store(false);
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>("payload"), 7) == 7);
wait_until([] { return !ethernet_is_running(); });
assert_busy_facade();
for (int iteration = 0; iteration < 10; ++iteration) ethernet_loop_maintain();
wait_until([] { return ethernet_is_running() && !ethernet_client_attached.load(); });
assert(ethernet_take_session_reset() && output().find("payload") != std::string::npos);
assert(ethernet_worker_active.load() && bus_owner.load());
finish_off();
break;
}
case 21: { // A stuck SEND register faults safely rather than exposing it to vendor RX/accept.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ack_send.store(false);
mock_stuck_command.store(true);
assert(ethernet_client.write(uint8_t{'z'}) == 1);
wait_until([] { return ethernet_state.load() == EthernetCliState::Faulted; });
assert(command("eth.status").find("controller stalled") != std::string::npos);
assert_busy_facade();
const int before = hardware_operations.load();
mock_accept_client.store(true);
for (int iteration = 0; iteration < 20; ++iteration) ethernet_loop_maintain();
assert(hardware_operations.load() == before && bus_owner.load());
finish_off();
assert(task_deletions.load() == 1);
mock_stuck_command.store(false);
mock_ack_send.store(true);
start_online();
finish_off();
break;
}
case 22: { // OFF transmits its queued acknowledgement, then bounds waiting for missing ACK.
start_online();
accept_connection();
mock_ack_send.store(false);
const std::string reply = command("eth off");
ethernet_send_reply(reply.c_str());
const auto started = std::chrono::steady_clock::now();
ethernet_loop_maintain();
wait_until([] { return ethernet_worker_stopped.load(); });
assert(std::chrono::steady_clock::now() - started < std::chrono::seconds(1));
assert(output().find(" -> " + reply + "\r\n") != std::string::npos);
ethernet_loop_maintain();
assert(!bus_owner.load() && task_deletions.load() == 1);
break;
}
case 23: { // Discard is bounded across calls and never exposes a fragmented long record.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ethernet_input = std::string(220, 'x') + "\nget eth\n";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && line[0] == 0);
assert(mock_ethernet_input.size() == 101 && ethernet_line_discarding);
assert(!ethernet_read_line(line, sizeof(line)) && line[0] == 0);
assert(!ethernet_line_discarding);
(void)ethernet_take_session_reset();
wait_until([] { return ethernet_is_running(); });
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "get eth");
finish_off();
break;
}
case 24: { // A newly stuck RECV command yields instead of entering vendor execCmdSn.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_stuck_command.store(true);
mock_ethernet_input = "get eth\r";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && std::string(line) == "g");
assert_busy_facade();
wait_until([] { return ethernet_state.load() == EthernetCliState::Faulted; });
assert(ethernet_get_enabled() && bus_owner.load());
finish_off();
break;
}
case 25: { // A delayed terminal RECV command does not lose its synchronous reply.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_hold_terminal_recv.store(true);
mock_ethernet_input = "get eth\r";
char line[32] = {};
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "get eth");
assert(!ethernet_is_running());
ethernet_send_reply("> on");
assert(ethernet_tx_count >= std::strlen(" -> > on\r\n"));
mock_hold_terminal_recv.store(false);
wait_until([] { return output().find(" -> > on\r\n") != std::string::npos && ethernet_is_running(); });
finish_off();
break;
}
case 26: { // Replacement cancels retained async output and rejects the previous peer's reply.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ethernet_input = "get eth\r";
char line[32] = {};
assert(ethernet_read_line(line, sizeof(line)));
const uint32_t previous = ethernet_session_generation.load();
mock_accept_client.store(true);
wait_until([&] { return ethernet_session_generation.load() != previous && ethernet_is_running(); });
ethernet_send_reply("previous-peer-result");
assert(ethernet_client.write(reinterpret_cast<const uint8_t*>("old-async"), 9) == 0);
(void)ethernet_take_session_reset();
finish_off();
assert(output().find("previous-peer-result") == std::string::npos);
assert(output().find("old-async") == std::string::npos);
break;
}
case 27: { // A partial line from a replaced client cannot concatenate with the next peer.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
mock_ethernet_input = "eth ";
char line[32] = {};
assert(!ethernet_read_line(line, sizeof(line)) && std::string(line) == "eth ");
const uint32_t before = ethernet_session_generation.load();
mock_accept_client.store(true);
wait_until([&] { return ethernet_session_generation.load() != before && ethernet_is_running(); });
mock_ethernet_input = "off\r";
assert(!ethernet_read_line(line, sizeof(line)) && line[0] == 0);
assert(mock_ethernet_input == "off\r");
(void)ethernet_take_session_reset();
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "off");
finish_off();
break;
}
case 28: { // Concurrent replacement cannot steal the record before its owner is captured.
start_online();
accept_connection();
(void)ethernet_take_session_reset();
const uint32_t before = ethernet_session_generation.load();
mock_ethernet_input = "get eth\r";
mock_replace_during_parser.store(true);
char line[32] = {};
assert(ethernet_read_line(line, sizeof(line)) && std::string(line) == "get eth");
assert(ethernet_reply_generation == before && worker_access_waits.load() > 0);
wait_until([&] { return ethernet_session_generation.load() != before && ethernet_is_running(); });
ethernet_send_reply("old-record-result");
finish_off();
assert(output().find("old-record-result") == std::string::npos);
break;
}
default: assert(false);
}
assert(live_task_allocations.load() == 0 && !spi_active.load() && !bus_owner.load());
assert(ethernet_tx_queue == nullptr && ethernet_tx_count == 0);
for (const auto& task : tasks) assert(!task->thread.joinable());
printf("PASS: RAK4631 Ethernet runtime scenario %d\n", scenario);
}
+3
View File
@@ -21,6 +21,9 @@ int main(int argc, char** argv) {
TestSSD1306Display d;
assert(d.begin());
auto& c=*Adafruit_SSD1306::last;
assert(d.width()==128 && d.height()==64);
assert(c.width()==128 && c.height()==64);
static_assert(SSD1306Display::FRAMEBUFFER_BYTES==1024);
const char* message="GIANT KILLER: Out on the Mercerwood mesh today, just the V4 and a small battery Checking the smaller font so the rest of this message is visible. 0123456789 END";
assert(std::string(message).size()==160);
d.startFrame();
+111 -1
View File
@@ -188,6 +188,103 @@ class FirmwareRamTest(unittest.TestCase):
}, "v3_repeater")
self.assertEqual(alternate, combined)
def test_combined_rak_budgets_uart_beside_exclusive_ethernet_and_ota(self):
defines = {
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
"ENABLE_OTA": 1, "MESHCORE_OTA_DEVICE_DEFLATE": 1,
"WITH_RS232_BRIDGE": "Serial2", "DISPLAY_CLASS": "SSD1306Display",
}
legacy = ram.requirements("NRF52_PLATFORM", defines,
"RAK_4631_repeater_unified_lora_ota")
combined = ram.requirements("NRF52_PLATFORM", {
**defines, "RAK4631_COMBINED_ETHERNET": 1,
}, "RAK_4631_repeater_unified_lora_ota")
components = combined["components"]
self.assertEqual(components["uart_bridge_and_driver"], 2560)
self.assertEqual(components["ethernet_or_external_ota_workspaces"], 5168)
self.assertNotIn("ota_encoder_workspace", components)
self.assertNotIn("ota_self_source_scratch", components)
self.assertEqual(components["display_pixels_and_driver"], 2048)
self.assertEqual(combined["required_heap_bytes"] - legacy["required_heap_bytes"], 2560 - 2048)
self.assertEqual(components["loop_and_callback_stacks"], 8192 + 3072)
self.assertEqual(components["flood_filter_table"], 63 * 200 + 16)
# An alternate port selects the same bridge instead of allocating two.
alternate = ram.requirements("NRF52_PLATFORM", {
**defines, "RAK4631_COMBINED_ETHERNET": 1,
"WITH_RS232_BRIDGE_ALT": "Serial1",
}, "RAK_4631_repeater_unified_lora_ota")
self.assertEqual(alternate, combined)
def test_combined_rak_worker_budget_tracks_stack_and_encoder_overrides(self):
defines = {
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
"RAK4631_COMBINED_ETHERNET": 1, "ENABLE_OTA": 1,
"MESHCORE_OTA_DEVICE_DEFLATE": 1,
}
for words, expected in ((1024, 5168), (1536, 6976), (2048, 9024)):
with self.subTest(words=words):
policy = ram.requirements("NRF52_PLATFORM", {
**defines, "RAK4631_ETHERNET_TASK_STACK_WORDS": words,
}, "RAK_4631_sensor")
self.assertEqual(policy["components"]["ethernet_or_external_ota_workspaces"], expected)
self.assertGreaterEqual(policy["required_contiguous_bytes"], expected)
no_encoder = ram.requirements("NRF52_PLATFORM", {
**defines, "MESHCORE_OTA_DEVICE_DEFLATE": 0,
}, "RAK_4631_sensor")
self.assertEqual(no_encoder["components"]["ethernet_or_external_ota_workspaces"], 4928)
for bits in (7, 8, 9, 10):
with self.subTest(bits=bits):
policy = ram.requirements("NRF52_PLATFORM", {
**defines, "MESHCORE_OTA_DEFLATE_HASH_BITS": bits,
}, "RAK_4631_sensor")
self.assertEqual(policy["components"]["ethernet_or_external_ota_workspaces"],
max(4928, (1 << bits) * 2 + 16 + 4096 + 32))
for capacity, expected in ((512, 5168), (1024, 5440), (2048, 6464)):
with self.subTest(tx_capacity=capacity):
policy = ram.requirements("NRF52_PLATFORM", {
**defines, "ETHERNET_CLI_TX_BUFFER_BYTES": capacity,
}, "RAK_4631_sensor")
self.assertEqual(policy["components"]["ethernet_or_external_ota_workspaces"], expected)
def test_combined_rak_oled_bound_does_not_change_other_display_budgets(self):
defines = {
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
"RAK4631_COMBINED_ETHERNET": 1,
}
for driver, expected in (("SSD1306Display", 2048), ("SH1106Display", 4096),
("SH1107Display", 4096), ("ST7735Display", 25602)):
with self.subTest(driver=driver):
combined = ram.requirements("NRF52_PLATFORM", {
**defines, "DISPLAY_CLASS": driver,
}, "RAK_4631_repeater")
self.assertEqual(combined["components"]["display_pixels_and_driver"], expected)
ordinary = ram.requirements("NRF52_PLATFORM", {
"DISPLAY_CLASS": "SSD1306Display",
}, "RAK_4631_repeater")
self.assertEqual(ordinary["components"]["display_pixels_and_driver"], 4096)
def test_combined_rak_budget_rejects_unmatched_hardware_or_short_task_stack(self):
defines = {
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
"RAK4631_COMBINED_ETHERNET": 1,
}
for missing in ("RAK_4631", "ETHERNET_ENABLED", "OTA_RAK_AUTO_STORE"):
invalid = {name: value for name, value in defines.items() if name != missing}
with self.subTest(missing=missing), self.assertRaisesRegex(ValueError, "adaptive nRF52 storage"):
ram.requirements("NRF52_PLATFORM", invalid, "RAK_4631_repeater")
with self.assertRaisesRegex(ValueError, "adaptive nRF52 storage"):
ram.requirements("ESP32_PLATFORM", defines, "esp_repeater")
for words in (0, 512, 1023):
with self.subTest(words=words), self.assertRaisesRegex(ValueError, "1024 stack words"):
ram.requirements("NRF52_PLATFORM", {
**defines, "RAK4631_ETHERNET_TASK_STACK_WORDS": words,
}, "RAK_4631_repeater")
for capacity in (0, 256, 511, 2049):
with self.subTest(tx_capacity=capacity), self.assertRaisesRegex(ValueError, "512..2048 bytes"):
ram.requirements("NRF52_PLATFORM", {
**defines, "ETHERNET_CLI_TX_BUFFER_BYTES": capacity,
}, "RAK_4631_repeater")
def test_published_image_tables_match_elf_and_use_its_own_reservations(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "reference.elf"
@@ -225,7 +322,20 @@ class FirmwareRamTest(unittest.TestCase):
def test_lazy_manual_stage_allocation_failure_and_clear(self):
source = (ROOT / "src/helpers/ota/OtaContext.h").read_text()
begin = source.index("#if defined(ESP32_PLATFORM) || (defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY))")
end = source.index("#endif", begin) + len("#endif")
# The actual buffer methods contain their own platform/owner guards.
# Compile the complete outer branch rather than its first nested #if.
end, depth = begin, 0
for line in source[begin:].splitlines(keepends=True):
directive = line.lstrip()
if directive.startswith("#if"):
depth += 1
elif directive.startswith("#endif"):
depth -= 1
end += len(line)
if depth == 0:
break
else:
self.fail("unterminated manual-stage buffer platform branch")
branch = source[begin:end]
code = r'''
#include <cstdlib>
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Exercise combined Ethernet on/off, SPI ownership and retained Stream safety."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_serial_wifi_sessions import SANITIZER_FLAGS
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/rak4631_ethernet_runtime"
SCENARIOS = (
"disabled boot and exact command parsing",
"flash conflict or active OTA refuses initialization",
"task allocation failure releases SPI",
"OFF before worker scheduling avoids hardware IO",
"OFF during slow initial DHCP waits for cooperative cleanup",
"OFF wakes DHCP retry promptly",
"missing Ethernet hardware reaps worker before returning SPI",
"online framing, short writes and real remote disconnect",
"slow DHCP renewal gates retained Stream without blocking mesh",
"renewal failure returns to service without restarting worker",
"active or staged OTA refuses OFF",
"remote OFF reply precedes socket cleanup and synchronous task reclamation",
"OTA race retains SPI owner while main-thread release retries",
"optional setting-save failure is transactional",
"repeated hot switches reclaim workers without stale notifications",
"saved-ON boot does not save twice",
"OFF during slow renewal never releases SPI early",
"bounded ring queue retains order and prevents command dispatch under backpressure",
"oversized line discards the whole command prefix",
"embedded NUL discards the whole record",
"missing TCP acknowledgement holds the SPI gate without blocking mesh",
"stuck SEND command faults without vendor RX or accept",
"remote OFF acknowledgement is queued and missing ACK shutdown is bounded",
"fragmented oversized record discard is bounded between loop turns",
"newly stuck RECV command yields without a vendor command spin",
"delayed terminal RECV retains its synchronous response",
"new peer cannot receive previous peer synchronous or retained async output",
"partial record from a replaced peer cannot concatenate with new input",
"concurrent worker replacement cannot steal a record's response owner",
)
class Rak4631EthernetRuntimeTests(unittest.TestCase):
def test_production_runtime_with_background_dhcp_and_spi_gate(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-rak4631-ethernet-runtime-") as directory:
binary = Path(directory) / "ethernet-runtime"
compiled = subprocess.run(
[compiler, "-std=c++17", "-pthread", "-Wall", "-Wextra", "-Werror",
*SANITIZER_FLAGS, "-DETHERNET_ENABLED=1", "-DRAK4631_COMBINED_ETHERNET=1",
f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'src'}",
str(FIXTURE / "test.cpp"), "-o", str(binary)],
capture_output=True, text=True, timeout=60,
)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
for scenario, description in enumerate(SCENARIOS):
with self.subTest(scenario=description):
checked = subprocess.run([str(binary), str(scenario)],
capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(f"PASS: RAK4631 Ethernet runtime scenario {scenario}", checked.stdout)
if __name__ == "__main__":
unittest.main()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Keep experimental RAK configs matched and separate from release recipes."""
import configparser
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts/generate_rak4631_combined_test.py"
SPEC = importlib.util.spec_from_file_location("rak_combined_config", SCRIPT)
generator = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(generator)
def ini(path):
result = configparser.ConfigParser(interpolation=None)
result.read(path)
return result
class RakCombinedBuildConfigTest(unittest.TestCase):
def test_matching_profiles_preserve_identity_and_never_run_platformio(self):
original = (ROOT / "platformio.ini").read_bytes()
variant = (ROOT / "variants/rak4631/platformio.ini").read_bytes()
with tempfile.TemporaryDirectory(prefix="meshcore-rak-config-") as temp:
directory = Path(temp)
marker = directory / "unexpected-platformio"
fake_pio = directory / "pio"
fake_pio.write_text("#!/bin/sh\ntouch " + shlex.quote(str(marker)) + "\nexit 91\n")
fake_pio.chmod(0o755)
output = directory / "generated configs"
result = subprocess.run(["python3", str(SCRIPT), "--output", str(output)],
cwd=ROOT, text=True, capture_output=True,
env={**os.environ, "PATH": str(directory) + os.pathsep + os.environ["PATH"]})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(marker.exists(), "config generation invoked PlatformIO")
receipt = json.loads((output / "profile-flags.json").read_text())
self.assertEqual(receipt["loop_stack_bytes"], 8192)
self.assertEqual(receipt["full_rule_capacity"], 63)
self.assertEqual(receipt["mota_target_id"], "0x05f5ffae")
self.assertEqual(len(result.stdout.splitlines()), 4)
for item in receipt["configs"]:
name = item["profile"]
config_path = Path(item["config"])
config = ini(config_path)
overlay_path = output / ("overlay-" + name + ".ini")
overlay = ini(overlay_path)["env:" + receipt["target"]]
flags = overlay["build_flags"]
self.assertIn("-DMOTA_TARGET_ID=0x05f5ffae", flags)
self.assertIn("-D MAX_NEIGHBOURS=50", flags)
self.assertNotIn("FLOOD_PACKET_FILTER_SLOTS", flags)
self.assertIn("-Ofast", overlay["build_unflags"])
self.assertIn("-Os", flags)
self.assertIn("-DLORA_FREQ=910.525", flags)
self.assertIn(str(overlay_path), config["platformio"]["extra_configs"])
self.assertEqual(hashlib.sha256(config_path.read_bytes()).hexdigest(), item["config_sha256"])
combined = name.startswith("combined")
self.assertEqual("-DRAK4631_COMBINED_ETHERNET=1" in flags, combined)
self.assertEqual("-DETHERNET_ENABLED=1" in flags, combined)
self.assertEqual("lib_deps" in overlay, combined)
if combined:
self.assertIn(generator.ETHERNET_LIBRARY, overlay["lib_deps"])
reduced = name.endswith("reduced")
self.assertEqual("-UENV_INCLUDE_BME680_BSEC" in flags, reduced)
if reduced:
self.assertIn("-DENV_INCLUDE_INA219=1", flags)
self.assertIn("${rak4631.build_flags}", flags)
self.assertNotIn("-UENV_INCLUDE_GPS", flags)
self.assertNotIn("ENV_INCLUDE_GPS", overlay["build_unflags"])
# GPS comes from the inherited full sensor base; the
# reduction never removes or overrides that driver.
self.assertIn("-D ENV_INCLUDE_GPS=1", ini(ROOT / "platformio.ini")["sensor_base"]["build_flags"])
self.assertEqual((ROOT / "platformio.ini").read_bytes(), original)
self.assertEqual((ROOT / "variants/rak4631/platformio.ini").read_bytes(), variant)
def test_rejects_release_output_and_invalid_version_before_writing(self):
with self.assertRaisesRegex(ValueError, "outside the repository"):
generator.generate(ROOT / "out" / "unexpected-trial", "v1.17.1.9-test")
with tempfile.TemporaryDirectory() as temp:
output = Path(temp) / "configs"
for version in ("", "v1\n-DOTHER=1", 'v1"bad', "v1;bad", "v1$(bad)"):
with self.subTest(version=version), self.assertRaisesRegex(ValueError, "firmware version"):
generator.generate(output, version)
self.assertFalse(output.exists())
if __name__ == "__main__":
unittest.main()
+508
View File
@@ -0,0 +1,508 @@
"""Exercise combined RAK4631 SPI ownership and the actual adaptive store."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def run_cpp(source, extra_sources=()):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
sources = []
for index, text in enumerate((source, *extra_sources)):
cpp = path / f"test{index}.cpp"
cpp.write_text(text)
sources.append(str(cpp))
exe = path / "test"
subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-DRAK4631_COMBINED_ETHERNET", "-DOTA_RAK_AUTO_STORE",
"-DOTA_INTERNAL_BOOTLOADER_UPDATE", "-DMESHCORE_OTA_DEVICE_DEFLATE=1",
"-I", str(ROOT),
*sources, "-o", str(exe),
], check=True)
subprocess.run([str(exe)], check=True)
class CombinedRakOtaTest(unittest.TestCase):
def test_self_serve_and_manual_buffer_cannot_allocate_during_ethernet(self):
context = (ROOT / "src/helpers/ota/OtaContext.h").read_text()
start = context.index(" bool ensureServeBuffer() {")
end = context.index(" void releaseServeBuffer()", start)
buffer_method = context[start:end]
self_source = (ROOT / "src/helpers/ota/OtaSelf.cpp").read_text()
start = self_source.index("bool ota_serve_self(OtaContext& c, uint32_t fw_version) {")
end = self_source.index(" SelfFwInfo fi;", start)
self_prologue = self_source[start:end]
source = r'''
#include <cassert>
#include <cstdlib>
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
using namespace mesh::ota;
namespace mesh { namespace ota {
static unsigned allocations = 0, self_body_calls = 0;
constexpr unsigned OTA_SERVE_BUF_SIZE = 4096;
void* tracked_malloc(size_t bytes) {
assert(!rak4631_ethernet_owns_spi()); ++allocations;
return std::malloc(bytes);
}
struct OtaContext {
uint8_t* serve_buf = nullptr;
#define malloc tracked_malloc
''' + buffer_method + r'''
#undef malloc
};
''' + self_prologue + r'''
(void)c; (void)fw_version;
++self_body_calls;
return true;
}
} }
int main() {
OtaContext context;
rak4631_set_ethernet_spi_owner(true);
assert(!context.ensureServeBuffer() && !ota_serve_self(context, 0));
assert(!context.serve_buf && allocations == 0 && self_body_calls == 0);
rak4631_set_ethernet_spi_owner(false);
assert(context.ensureServeBuffer() && ota_serve_self(context, 0));
assert(context.serve_buf && allocations == 1 && self_body_calls == 1);
free(context.serve_buf);
}
'''
run_cpp(source)
def test_context_handoff_frees_idle_self_serve_and_preserves_busy_ota(self):
header = (ROOT / "src/helpers/ota/OtaContext.h").read_text()
start = header.index(" // Only the main thread may change ownership.")
end = header.index("#endif\n\n#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)", start)
body = header[start:end]
source = r'''
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "src/helpers/ota/OtaBlInfo.h"
#include "src/helpers/ota/OtaRakStoragePolicy.h"
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
using namespace mesh::ota;
namespace mesh { namespace ota {
static unsigned frees = 0, probes = 0;
static uint8_t detection = 2;
static uint32_t rak_id = 0;
static OtaBlCaps app_caps, boot_caps;
struct OtaBootloaderIdentity {
bool present = true, crc_ok = true;
const char* device_name = "4631_DFU";
};
static OtaBootloaderIdentity identity;
void ota_transport_deflate() {}
bool ota_self_serve_supported(bool external) { return external; }
constexpr unsigned CODEC_DETOOLS_INPLACE = 2;
class OtaStoreQspiNrf52 {
public:
static uint8_t autoDetect() { assert(!rak4631_ethernet_owns_spi()); ++probes; return detection; }
static void autoProbeIds(uint32_t& rak, uint32_t& w25) { rak = rak_id; w25 = 0xef4015; }
};
struct OtaManager {
enum FetchState { IDLE, WANT_MANIFEST, WANT_LEAVES, VERIFYING_STAGED, FETCHING,
COMPLETE, FAILED, PAUSED };
unsigned state = IDLE, serve_jobs = 0, manifest_jobs = 0, served = 0;
bool own = false, accept_full = true, accept_boot = false, archive = true;
void (*encoder)() = ota_transport_deflate;
bool servingPrimaryManifest(const uint8_t*) const { return own; }
unsigned fetchState() const { return state; }
unsigned pendingServeJobs() const { return serve_jobs; }
unsigned pendingManifestJobs() const { return manifest_jobs; }
unsigned servedCount() const { return served; }
void set_accept_full(bool value) { accept_full = value; }
void set_accept_bootloader(bool value) { accept_boot = value; }
void set_apply_codec(unsigned) {}
void set_fetch_store(void*) {}
void set_archive_interest(bool value) { archive = value; }
void set_transport_deflate_encoder(void (*value)()) { encoder = value; }
void clear_primary() { own = false; served = 0; }
};
struct Store {
unsigned size = 0, resets = 0;
bool allow_reset = true, allow_internal = true;
unsigned staged_size() const { return size; }
bool resetSelection() { if (size || !allow_reset) return false; ++resets; return true; }
bool usesExternal() const { return !rak4631_ethernet_owns_spi() && detection == 2; }
bool usesInternal() const { return allow_internal && rak4631_ethernet_owns_spi(); }
};
void tracked_free(void* pointer) {
if (pointer) { assert(!rak4631_ethernet_owns_spi()); ++frees; }
std::free(pointer);
}
struct Context {
OtaManager manager;
Store fetch_store;
uint8_t serve_self_manifest[1] = {};
bool apply_pending = false, bootloader_apply_pending = false, serving = false;
bool folder_active = false, fetch_to_folder = false, self_serve_supported = true;
unsigned serve_expected = 0;
void* _folder_source = nullptr;
void* folder_dest = nullptr;
void* disconnected_folder_dest = nullptr;
uint8_t *serve_self_leaves = nullptr, *serve_self_proof = nullptr, *serve_buf = nullptr;
OtaBlCaps& bootloaderAppCaps() { return app_caps; }
OtaBlCaps& bootloaderUpdateCaps() { return boot_caps; }
OtaBootloaderIdentity& bootloaderIdentity() { return identity; }
void releaseServeBuffer() { tracked_free(serve_buf); serve_buf = nullptr; }
#define free tracked_free
''' + body + r'''
#undef free
};
} }
int main() {
app_caps.present = boot_caps.present = true;
app_caps.apply_abi = boot_caps.apply_abi = 3;
app_caps.codec_mask = boot_caps.codec_mask = 5;
app_caps.storage_flags = 0x1e; app_caps.optional_app_storage = 0x14;
boot_caps.storage_flags = 0x0a;
Context c;
char error[160];
for (unsigned state = 1; state <= 7; ++state) {
c.manager.state = state;
assert(!c.prepareRakEthernet(error, sizeof(error)));
assert(!c.canReleaseRakEthernet() && !rak4631_ethernet_owns_spi());
assert(probes == 0 && c.fetch_store.resets == 0);
}
c.manager.state = OtaManager::IDLE;
c.fetch_store.size = 100;
assert(!c.prepareRakEthernet(error, sizeof(error)) && probes == 0);
c.fetch_store.size = 0;
for (bool* flag : {&c.apply_pending, &c.bootloader_apply_pending, &c.serving,
&c.folder_active, &c.fetch_to_folder}) {
*flag = true;
assert(!c.prepareRakEthernet(error, sizeof(error)) && probes == 0);
*flag = false;
}
for (unsigned* count : {&c.manager.serve_jobs, &c.manager.manifest_jobs,
&c.manager.served, &c.serve_expected}) {
*count = 1;
assert(!c.prepareRakEthernet(error, sizeof(error)) && probes == 0);
*count = 0;
}
for (void** pointer : {&c._folder_source, &c.folder_dest, &c.disconnected_folder_dest}) {
*pointer = &c;
assert(!c.prepareRakEthernet(error, sizeof(error)) && probes == 0);
*pointer = nullptr;
}
detection = 1; rak_id = 0xc84015;
assert(!c.prepareRakEthernet(error, sizeof(error)));
assert(strstr(error, "RAK15001") && !rak4631_ethernet_owns_spi());
detection = 3;
assert(!c.prepareRakEthernet(error, sizeof(error)));
detection = 2; rak_id = 0x123456;
assert(!c.prepareRakEthernet(error, sizeof(error)));
assert(strstr(error, "unknown SPI"));
rak_id = 0;
app_caps.optional_app_storage = 0;
identity.device_name = "4631_W25Q16_DFU";
assert(!c.prepareRakEthernet(error, sizeof(error)));
assert(strstr(error, "bootloader cannot") && c.fetch_store.resets == 0);
app_caps.optional_app_storage = 0x14; identity.device_name = "4631_DFU";
c.manager.own = true; c.manager.served = 1; c.serving = true;
c.serve_self_leaves = static_cast<uint8_t*>(malloc(64));
c.serve_self_proof = static_cast<uint8_t*>(malloc(64));
c.serve_buf = static_cast<uint8_t*>(malloc(64));
assert(c.prepareRakEthernet(error, sizeof(error)));
assert(rak4631_ethernet_owns_spi() && c.fetch_store.resets == 1);
assert(frees == 3 && !c.serve_self_leaves && !c.serve_self_proof && !c.serve_buf);
assert(!c.serving && c.manager.served == 0 && !c.manager.own);
assert(!c.manager.accept_full && c.manager.accept_boot && !c.manager.archive);
assert(!c.self_serve_supported && !c.manager.encoder);
c.manager.state = OtaManager::FETCHING;
assert(!c.releaseRakEthernet() && rak4631_ethernet_owns_spi());
assert(c.fetch_store.resets == 1 && !c.self_serve_supported);
c.manager.state = OtaManager::IDLE; c.fetch_store.size = 100;
assert(!c.releaseRakEthernet() && rak4631_ethernet_owns_spi());
c.fetch_store.size = 0;
assert(c.releaseRakEthernet() && !rak4631_ethernet_owns_spi());
assert(c.fetch_store.resets == 2 && c.manager.accept_full);
assert(c.self_serve_supported && c.manager.encoder == ota_transport_deflate);
}
'''
run_cpp("#include <initializer_list>\n" + source)
def test_real_encoder_cannot_allocate_while_ethernet_owns_spi(self):
source = r'''
#include <cassert>
#include <cstdlib>
#include <cstring>
#include "src/helpers/ota/OtaDeflate.h"
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
using namespace mesh::ota;
static unsigned allocations = 0;
extern "C" void* __real_calloc(size_t, size_t);
extern "C" void* __wrap_calloc(size_t n, size_t size) {
assert(!rak4631_ethernet_owns_spi()); ++allocations;
return __real_calloc(n, size);
}
int main() {
uint8_t input[256], encoded[256], output[256];
memset(input, 'A', sizeof(input));
uint16_t length = 123;
rak4631_set_ethernet_spi_owner(true);
assert(!ota_transport_deflate(nullptr, input, sizeof(input), encoded, sizeof(encoded), &length));
assert(length == 0 && allocations == 0);
rak4631_set_ethernet_spi_owner(false);
assert(ota_transport_deflate(nullptr, input, sizeof(input), encoded, sizeof(encoded), &length));
assert(allocations == 1 && length < sizeof(input));
rak4631_set_ethernet_spi_owner(true);
uint16_t decoded = 0;
assert(ota_transport_inflate(nullptr, encoded, length, output, sizeof(output), &decoded));
assert(decoded == sizeof(input) && memcmp(input, output, sizeof(input)) == 0);
assert(allocations == 1); // ordinary OTA reception stays available
}
'''
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
objects = []
for relative in ("OtaTinf.c",):
obj = path / (Path(relative).stem + ".o")
subprocess.run([
"cc", "-std=c99", "-DENABLE_OTA", "-DMESHCORE_OTA_DEVICE_DEFLATE=1",
"-c", str(ROOT / "src/helpers/ota" / relative), "-o", str(obj),
], check=True)
objects.append(str(obj))
cpp = path / "test.cpp"
cpp.write_text(source)
exe = path / "test"
subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-DRAK4631_COMBINED_ETHERNET", "-DENABLE_OTA",
"-DMESHCORE_OTA_DEVICE_DEFLATE=1", "-I", str(ROOT),
str(cpp), str(ROOT / "src/helpers/ota/OtaDeflate.cpp"),
*objects, "-Wl,--wrap=calloc", "-o", str(exe),
], check=True)
subprocess.run([str(exe)], check=True)
def test_only_explicit_combined_build_can_relax_static_bus_exclusion(self):
base = ["NRF52_PLATFORM", "OTA_QSPI_STORE", "OTA_QSPI_SHARED_WISBLOCK_SPI"]
cases = [
(base, True),
(base + ["ETHERNET_ENABLED"], False),
(base + ["ETHERNET_ENABLED", "RAK4631_COMBINED_ETHERNET"], False),
(base + ["ETHERNET_ENABLED", "RAK4631_COMBINED_ETHERNET",
"OTA_RAK_AUTO_STORE", "RAK_4631"], True),
]
for defines, accepted in cases:
with self.subTest(defines=defines):
result = subprocess.run([
"c++", "-std=c++17", "-E", "-x", "c++", "-",
"-I", str(ROOT), *("-D" + value for value in defines),
], input='#include "src/helpers/ota/OtaStoreQspiNrf52.h"\n',
capture_output=True, text=True)
self.assertEqual(result.returncode == 0, accepted, result.stderr)
def test_spi_owner_is_shared_between_translation_units(self):
run_cpp(r'''
#include <cassert>
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
void reserve_elsewhere(bool);
int main() {
assert(!mesh::ota::rak4631_ethernet_owns_spi());
reserve_elsewhere(true);
assert(mesh::ota::rak4631_ethernet_owns_spi());
reserve_elsewhere(false);
assert(!mesh::ota::rak4631_ethernet_owns_spi());
}
''', (r'''
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
void reserve_elsewhere(bool owner) {
mesh::ota::rak4631_set_ethernet_spi_owner(owner);
}
''',))
def test_actual_adaptive_store_respects_owner_and_bootloader(self):
header = (ROOT / "src/helpers/ota/OtaStoreAdaptiveNrf52.h").read_text()
body = header[header.index("class OtaStoreAdaptiveNrf52"):
header.index("} // namespace ota")]
source = r'''
#include <cassert>
#include <cstring>
#include <new>
#include "src/helpers/ota/OtaStore.h"
#include "src/helpers/ota/OtaBlInfo.h"
#include "src/helpers/ota/OtaRakStoragePolicy.h"
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
using namespace mesh::ota;
namespace mesh { namespace ota {
static unsigned detection = 2, probes = 0, internal_live = 0, external_live = 0;
static uint8_t storage_flags = 0x1e, optional_storage = 0x14;
static bool valid_identity = true;
static const char* device_name = "4631_DFU";
struct OtaBootloaderIdentity { bool crc_ok = true; const char* device_name; };
bool ota_installed_bootloader_identity(OtaBootloaderIdentity& identity) {
identity.device_name = device_name; return valid_identity;
}
struct MotaManifest { bool is_bootloader() const { return false; } };
bool mota_parse_manifest(const uint8_t*, size_t, MotaManifest&) { return false; }
OtaBlCaps simulatedAppCaps() {
OtaBlCaps c; c.present = true; c.apply_abi = 3; c.codec_mask = 5;
c.storage_flags = storage_flags; c.optional_app_storage = optional_storage;
return c;
}
OtaBlCaps simulatedBootCaps() {
OtaBlCaps c = simulatedAppCaps(); c.storage_flags = 0x0a; return c;
}
class Store : public OtaStore {
public:
uint32_t size = 0;
bool begin(uint32_t n) override { size = n; return true; }
bool write(uint32_t, const uint8_t*, uint32_t) override { return true; }
bool read(uint32_t, uint8_t*, uint32_t) const override { return true; }
uint32_t capacity() const override { return 65536; }
uint32_t staged_size() const override { return size; }
void clear() override { size = 0; }
};
class OtaStoreFlashNrf52 : public Store {
public:
OtaStoreFlashNrf52() { ++internal_live; assert(external_live == 0); }
~OtaStoreFlashNrf52() { --internal_live; }
};
class OtaStoreQspiNrf52 : public Store {
public:
OtaStoreQspiNrf52() { ++external_live; assert(internal_live == 0); }
~OtaStoreQspiNrf52() { --external_live; }
static uint8_t autoDetect() { ++probes; return detection; }
static bool headerW25Detected() { return false; }
uint32_t jedec_id() const { return 0xef4015; }
uint8_t status1() const { return 0; }
const char* last_stage() const { return "idle"; }
const char* last_error() const { return ""; }
};
#define ota_bootloader_app_caps simulatedAppCaps
#define ota_bootloader_update_caps simulatedBootCaps
''' + body + r'''
} }
int main() {
// Ethernet may not discover NOR, and selects the internal path only when
// the installed bootloader supports that actual source.
rak4631_set_ethernet_spi_owner(true);
{
OtaStoreAdaptiveNrf52 store;
assert(store.usesInternal() && !store.usesExternal());
assert(probes == 0);
assert(strstr(store.selectionReason(), "Ethernet active"));
assert(store.plan_layout(true, 40960, 365, 40960, true));
assert(store.usesInternal());
assert(store.begin(41330));
assert(!store.resetSelection()); // privileged staging is pinned too
}
assert(internal_live == 0 && external_live == 0);
storage_flags = 0x06; optional_storage = 0;
device_name = "4631_W25Q16_DFU";
{
OtaStoreAdaptiveNrf52 store;
assert(!store.usesInternal() && !store.usesExternal());
assert(store.capacity() == 0 && !store.begin(100));
assert(strstr(store.selectionReason(), "bootloader lacks internal OTA"));
assert(probes == 0);
}
storage_flags = 0x1e; optional_storage = 0x14;
device_name = "4631_DFU"; valid_identity = false;
{
OtaStoreAdaptiveNrf52 store;
assert(!store.usesInternal()); // an unverified optional marker is insufficient
assert(!store.begin(100));
assert(probes == 0);
}
valid_identity = true;
rak4631_set_ethernet_spi_owner(false);
{
OtaStoreAdaptiveNrf52 store;
assert(store.usesExternal() && probes == 1);
assert(store.begin(100));
assert(!store.resetSelection()); // neither receive nor ready data is dropped
rak4631_set_ethernet_spi_owner(true); // defensive gate against a bad caller
uint8_t byte = 0;
assert(!store.begin(200) && !store.write(0, &byte, 1));
assert(!store.read(0, &byte, 1) && store.capacity() == 0);
assert(!store.resetSelection()); // existing data remains pinned
rak4631_set_ethernet_spi_owner(false);
store.clear();
assert(store.resetSelection());
assert(internal_live == 0 && external_live == 0);
rak4631_set_ethernet_spi_owner(true);
assert(store.usesInternal() && probes == 1);
assert(store.resetSelection());
rak4631_set_ethernet_spi_owner(false);
assert(store.usesExternal() && probes == 2);
}
assert(internal_live == 0 && external_live == 0);
}
'''
run_cpp(source)
def test_actual_discovery_does_no_gpio_io_while_ethernet_owns_spi(self):
cpp = (ROOT / "src/helpers/ota/OtaStoreQspiNrf52.cpp").read_text()
body = cpp[cpp.index("uint8_t OtaStoreQspiNrf52::autoDetect()"):
cpp.index("OtaStoreQspiNrf52::OtaStoreQspiNrf52()")]
source = r'''
#include <cassert>
#include <cstdint>
#include "src/helpers/nrf52/Rak4631SharedSpi.h"
using namespace mesh::ota;
namespace mesh { namespace ota {
static uint8_t auto_cs_pin = 0xff, auto_detection = 0xff;
static uint32_t auto_jedec_id = 0, auto_rak_probe_id = 0, auto_w25_probe_id = 0;
static bool auto_header_w25 = false;
static constexpr uint8_t HEADER_SCK_PIN = 16, HEADER_IO0_PIN = 17, HEADER_IO1_PIN = 15;
static unsigned gpio_calls = 0, nor_calls = 0;
static uint32_t rak_id = 0, w25_id = 0xef4015, header_id = 0;
uint8_t arduino_to_physical(uint32_t pin) { return pin; }
void nrf_gpio_pin_set(uint32_t) { ++gpio_calls; }
void nrf_gpio_cfg_output(uint32_t) { ++gpio_calls; }
void nrf_gpio_cfg_default(uint32_t) { ++gpio_calls; }
uint32_t probe_nor(uint8_t cs) {
++nor_calls;
return cs == 26 ? rak_id : auto_header_w25 ? header_id : w25_id;
}
class OtaStoreQspiNrf52 {
public:
static uint8_t autoDetect();
static bool headerW25Detected();
static void autoProbeIds(uint32_t&, uint32_t&);
};
#if defined(OTA_RAK_AUTO_STORE)
''' + body + r'''
} }
int main() {
uint32_t rak, w25;
rak4631_set_ethernet_spi_owner(true);
assert(OtaStoreQspiNrf52::autoDetect() == 0);
OtaStoreQspiNrf52::autoProbeIds(rak, w25);
assert(!OtaStoreQspiNrf52::headerW25Detected());
assert(gpio_calls == 0 && nor_calls == 0 && auto_detection == 0xff);
rak4631_set_ethernet_spi_owner(false);
assert(OtaStoreQspiNrf52::autoDetect() == 2);
assert(nor_calls == 2 && auto_cs_pin == 31 && auto_jedec_id == 0xef4015);
const unsigned gpio_after_discovery = gpio_calls;
rak4631_set_ethernet_spi_owner(true);
assert(OtaStoreQspiNrf52::autoDetect() == 0);
OtaStoreQspiNrf52::autoProbeIds(rak, w25);
assert(rak == 0 && w25 == 0xef4015); // diagnostics retain physical discovery
assert(gpio_calls == gpio_after_discovery && nor_calls == 2);
rak4631_set_ethernet_spi_owner(false);
assert(OtaStoreQspiNrf52::autoDetect() == 2 && nor_calls == 2);
auto_detection = 0xff; w25_id = 0; header_id = 0xef4015;
assert(OtaStoreQspiNrf52::autoDetect() == 2);
assert(OtaStoreQspiNrf52::headerW25Detected());
const unsigned after_header_probe = nor_calls;
rak4631_set_ethernet_spi_owner(true);
assert(OtaStoreQspiNrf52::headerW25Detected()); // GPS still reserves header UART1
assert(nor_calls == after_header_probe);
}
'''
run_cpp(source)
if __name__ == "__main__":
unittest.main()
+286
View File
@@ -0,0 +1,286 @@
"""Run the bounded TCP sender against a register-level W5100S simulator."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
DRIVER = (ROOT / ".pio/libdeps/RAK_4631_repeater_unified_lora_ota"
/ "RAK13800-W5100S/src/w5100.h")
ARDUINO = r'''
#pragma once
#include <cstdint>
constexpr unsigned OUTPUT = 1, LOW = 0, HIGH = 1;
inline void pinMode(unsigned, unsigned) {}
inline void digitalWrite(unsigned, unsigned) {}
'''
SPI = r'''
#pragma once
#include <cassert>
constexpr unsigned MSBFIRST = 1, SPI_MODE0 = 0;
struct SPISettings { SPISettings(unsigned, unsigned, unsigned) {} };
struct SPIClass {
bool active = false;
unsigned begins = 0, ends = 0;
void beginTransaction(SPISettings) { assert(!active); active = true; ++begins; }
void endTransaction() { assert(active); active = false; ++ends; }
};
'''
RAK_HEADER = r'''
#pragma once
#define _RAK13800_W5100S_H_
#define MAX_SOCK_NUM 8
#include <Arduino.h>
#include <SPI.h>
'''
# Keep the production register interface available in native CI without
# downloading board libraries. A second test uses the actual installed vendor
# header when available, and the firmware build always checks that interface.
PORTABLE_DRIVER = r'''
#pragma once
#include <Arduino.h>
#include <SPI.h>
#define SPI_ETHERNET_SETTINGS SPISettings(14000000, MSBFIRST, SPI_MODE0)
struct SnSR { static constexpr uint8_t CLOSED=0, ESTABLISHED=0x17, CLOSE_WAIT=0x1c; };
struct SnIR { static constexpr uint8_t SEND_OK=0x10, TIMEOUT=0x08; };
enum SockCMD { Sock_CLOSE=0x10, Sock_SEND=0x20, Sock_RECV=0x40 };
class W5100Class {
static uint8_t chip, CH_BASE_MSB, ss_pin;
static SPIClass* spi;
public:
static constexpr uint16_t SSIZE=2048, SMASK=2047;
static uint8_t init();
static uint8_t getChip() { return chip; }
static SPIClass* getSPI() { return spi; }
static void setSPI(SPIClass& value) { spi=&value; }
static uint16_t SBASE(uint8_t socket) { return 0x4000 + socket*SSIZE; }
static uint16_t RBASE(uint8_t socket) { return 0x6000 + socket*SSIZE; }
static uint16_t read(uint16_t, uint8_t*, uint16_t);
static uint16_t write(uint16_t, const uint8_t*, uint16_t);
static uint8_t read(uint16_t address) { uint8_t value; read(address,&value,1); return value; }
static uint8_t write(uint16_t address,uint8_t value) { return write(address,&value,1); }
static uint16_t reg(uint8_t socket,uint16_t offset) { return (CH_BASE_MSB<<8)+socket*256+offset; }
static uint8_t readSnSR(uint8_t socket) { return read(reg(socket,3)); }
static uint8_t readSnIR(uint8_t socket) { return read(reg(socket,2)); }
static uint8_t readSnCR(uint8_t socket) { return read(reg(socket,1)); }
static void writeSnIR(uint8_t socket,uint8_t value) { write(reg(socket,2),value); }
static void writeSnCR(uint8_t socket,uint8_t value) { write(reg(socket,1),value); }
static uint16_t readSnTX_WR(uint8_t socket) {
uint8_t bytes[2]; read(reg(socket,0x24),bytes,2); return (bytes[0]<<8)|bytes[1];
}
static uint16_t readSnTX_FSR(uint8_t socket) {
uint8_t bytes[2]; read(reg(socket,0x20),bytes,2); return (bytes[0]<<8)|bytes[1];
}
static uint16_t readSnRX_RSR(uint8_t socket) {
uint8_t bytes[2]; read(reg(socket,0x26),bytes,2); return (bytes[0]<<8)|bytes[1];
}
static uint16_t readSnRX_RD(uint8_t socket) {
uint8_t bytes[2]; read(reg(socket,0x28),bytes,2); return (bytes[0]<<8)|bytes[1];
}
static void writeSnRX_RD(uint8_t socket,uint16_t value) {
uint8_t bytes[2]={uint8_t(value>>8),uint8_t(value)}; write(reg(socket,0x28),bytes,2);
}
static void writeSnTX_WR(uint8_t socket,uint16_t value) {
uint8_t bytes[2]={uint8_t(value>>8),uint8_t(value)}; write(reg(socket,0x24),bytes,2);
}
static void execCmdSn(uint8_t, SockCMD); // deliberately undefined: a blocking call fails linking
};
extern W5100Class W5100;
'''
RUNNER = r'''
#include <cassert>
#include <cstring>
#include <vector>
#include "src/helpers/nrf52/EthernetCliTransmit.h"
using mesh::nrf52::EthernetCliTransmit;
static_assert(Sock_SEND == 0x20 && Sock_CLOSE == 0x10 && Sock_RECV == 0x40);
static_assert(SnIR::SEND_OK == 0x10 && SnIR::TIMEOUT == 0x08);
static_assert(SnSR::ESTABLISHED == 0x17 && SnSR::CLOSE_WAIT == 0x1c);
static SPIClass spi;
uint8_t W5100Class::chip = 51, W5100Class::CH_BASE_MSB = 4, W5100Class::ss_pin = 26;
SPIClass* W5100Class::spi = &::spi;
W5100Class W5100;
static uint8_t memory[65536] = {};
static uint8_t next_chip = 51;
static unsigned reads = 0, writes = 0, free_reads = 0, data_writes = 0, commands = 0;
static bool short_write = false;
static std::vector<uint16_t> changing_free;
static std::vector<uint16_t> changing_receive;
uint8_t W5100Class::init() { chip = next_chip; spi = &::spi; return chip; }
uint16_t W5100Class::read(uint16_t address, uint8_t* buffer, uint16_t length) {
assert(::spi.active); ++reads;
if (address >= 0x400 && address < 0x800 && (address&255) == 0x20 && length == 2) {
++free_reads;
if (!changing_free.empty()) {
const uint16_t value = changing_free.front(); changing_free.erase(changing_free.begin());
buffer[0] = value>>8; buffer[1] = value; return length;
}
}
if (address >= 0x400 && address < 0x800 && (address&255) == 0x26 && length == 2 &&
!changing_receive.empty()) {
const uint16_t value=changing_receive.front(); changing_receive.erase(changing_receive.begin());
buffer[0]=value>>8; buffer[1]=value; return length;
}
memcpy(buffer,memory+address,length);
return length;
}
uint16_t W5100Class::write(uint16_t address,const uint8_t* buffer,uint16_t length) {
assert(::spi.active); ++writes;
if (address >= 0x4000) {
++data_writes;
if (short_write) return 0;
}
if (address >= 0x400 && address < 0x800 && length == 1 && (address&255) == 2) {
memory[address] &= ~buffer[0];
} else {
if (address >= 0x400 && address < 0x800 && length == 1 && (address&255) == 1) ++commands;
memcpy(memory+address,buffer,length);
}
return length;
}
static uint16_t reg(unsigned offset) { return 0x400+offset; }
static void put16(unsigned offset,uint16_t value) {
memory[reg(offset)] = value>>8; memory[reg(offset)+1] = value;
}
static uint16_t get16(unsigned offset) { return (memory[reg(offset)]<<8)|memory[reg(offset)+1]; }
static void reset() {
memset(memory,0,sizeof(memory));
memory[reg(3)] = SnSR::ESTABLISHED;
put16(0x20,2048);
reads = writes = free_reads = data_writes = commands = 0;
changing_free.clear(); changing_receive.clear(); short_write = false; next_chip = 51;
W5100.init();
assert(!::spi.active && ::spi.begins == ::spi.ends);
}
int main() {
uint8_t payload[512];
for (unsigned i=0;i<sizeof(payload);++i) payload[i]=i;
reset();
EthernetCliTransmit sender;
memory[reg(2)] = SnIR::SEND_OK; // stale ACK must be revoked
assert(sender.write(0,payload,4,10) == 4 && sender.pending() && !sender.failed());
assert(memcmp(memory+0x4000,payload,4)==0 && get16(0x24)==4);
assert(memory[reg(2)]==0 && memory[reg(1)]==Sock_SEND && commands==1);
assert(sender.write(0,payload,4,11)==0 && commands==1);
const unsigned before=reads;
sender.poll(12); // command/ACK remain stuck: one finite poll
assert(sender.pending() && reads-before<=3);
memory[reg(1)]=0; memory[reg(2)]=SnIR::SEND_OK;
sender.poll(20);
assert(!sender.pending() && !sender.failed() && memory[reg(2)]==0);
assert(sender.write(0,payload,4,21)==4 && get16(0x24)==8);
reset(); sender.reset();
put16(0x24,0xfffe); // circular SRAM and 16-bit counter both wrap
assert(sender.write(0,payload,4,1)==4);
assert(memory[0x47fe]==0 && memory[0x47ff]==1 && memory[0x4000]==2 && memory[0x4001]==3);
assert(get16(0x24)==2 && data_writes==2);
reset(); sender.reset(); put16(0x20,32);
assert(sender.write(0,payload,sizeof(payload),1)==32 && get16(0x24)==32);
reset(); sender.reset(); changing_free={100,101,100,101};
assert(sender.write(0,payload,4,1)==0 && free_reads==4 && !sender.failed());
assert(!sender.pending() && !data_writes && !commands);
assert(sender.write(0,payload,4,2)==4); // instability yields and retries
reset(); sender.reset(); put16(0x20,0);
assert(sender.write(0,payload,4,1)==0 && !sender.failed());
assert(sender.write(0,payload,4,10000)==0 && !sender.failed()); // backpressure is not an ACK timeout
reset(); sender.reset(); short_write=true;
assert(sender.write(0,payload,4,1)==0 && sender.failed());
assert(get16(0x24)==0 && commands==0);
reset(); sender.reset();
assert(sender.write(0,payload,4,0xfffffff0u)==4);
memory[reg(1)]=0; // ACK never arrives; register polling stays finite
for (unsigned elapsed=1;elapsed<250;++elapsed) {
const unsigned first=reads;
sender.poll(0xfffffff0u+elapsed);
assert(sender.pending() && !sender.failed() && reads-first<=3);
}
sender.poll(0xfffffff0u+250);
assert(sender.failed() && !sender.pending());
assert(EthernetCliTransmit::close(0));
assert(memory[reg(1)]==Sock_CLOSE);
reset(); sender.reset(); memory[reg(1)]=Sock_SEND;
assert(sender.write(0,payload,4,10)==0 && sender.pending() && !sender.failed());
sender.poll(259); assert(!sender.failed());
sender.poll(260); assert(sender.failed());
const unsigned close_reads=reads;
assert(!EthernetCliTransmit::close(0) && reads-close_reads==1); // stuck CR never spins
reset(); sender.reset(); assert(sender.write(0,payload,4,1)==4);
memory[reg(1)]=0; memory[reg(2)]=SnIR::SEND_OK|SnIR::TIMEOUT;
sender.poll(2); assert(sender.failed() && memory[reg(2)]==0);
reset(); sender.reset(); assert(sender.write(0,payload,4,1)==4);
memory[reg(3)]=SnSR::CLOSED;
sender.poll(2); assert(sender.failed());
reset(); sender.reset(); put16(0x20,0xffff);
assert(sender.write(0,payload,4,1)==0 && sender.failed() && commands==0);
reset(); sender.reset();
assert(sender.write(4,payload,4,1)==0 && sender.failed() && reads==0);
assert(!EthernetCliTransmit::close(4));
reset(); sender.reset(); next_chip=55; W5100.init();
assert(sender.write(0,payload,4,1)==0 && sender.failed() && reads==0);
reset(); sender.reset(); assert(sender.write(0,payload,4,1)==4);
const unsigned no_io_reads=reads, no_io_writes=writes;
sender.reset(); assert(!sender.pending() && !sender.failed());
assert(reads==no_io_reads && writes==no_io_writes);
reset(); sender.reset();
assert(sender.write(0,nullptr,4,1)==0 && sender.write(0,payload,0,1)==0 && !sender.failed());
reset();
assert(EthernetCliTransmit::commandReady(0) && EthernetCliTransmit::connected(0));
put16(0x26,2); put16(0x28,0xffff);
memory[0x67ff]='X'; memory[0x6000]='Y';
assert(EthernetCliTransmit::available(0)==2);
assert(EthernetCliTransmit::peek(0)=='X' && get16(0x28)==0xffff && commands==0);
assert(EthernetCliTransmit::read(0)=='X');
assert(get16(0x28)==0 && memory[reg(1)]==Sock_RECV && commands==1);
assert(!EthernetCliTransmit::commandReady(0)); // newly stuck RECV never spins
const unsigned first_rx_read=reads;
assert(EthernetCliTransmit::available(0)==0 && EthernetCliTransmit::read(0)==-1);
assert(reads-first_rx_read<=4 && commands==1);
memory[reg(1)]=0; put16(0x26,1);
assert(EthernetCliTransmit::read(0)=='Y' && get16(0x28)==1);
reset(); changing_receive={1,2,1,2};
const unsigned before_rx_reads=reads;
assert(EthernetCliTransmit::available(0)==0 && reads-before_rx_reads==6);
assert(commands==0); // unstable register values yield after four reads
memory[reg(3)]=SnSR::CLOSE_WAIT; changing_receive={1,2,1,2};
assert(EthernetCliTransmit::connected(0)); // uncertainty must not discard final bytes
put16(0x26,1); assert(EthernetCliTransmit::connected(0));
put16(0x26,0); assert(!EthernetCliTransmit::connected(0));
memory[reg(3)]=SnSR::CLOSED;
assert(EthernetCliTransmit::available(0)==0 && EthernetCliTransmit::read(0)==-1);
assert(!EthernetCliTransmit::connected(0));
assert(EthernetCliTransmit::read(4)==-1 && EthernetCliTransmit::available(4)==0);
assert(!EthernetCliTransmit::commandReady(4) && !EthernetCliTransmit::connected(4));
assert(!::spi.active && ::spi.begins==::spi.ends);
}
'''
class EthernetTransmitTest(unittest.TestCase):
def compile_and_run(self, driver):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
for name, content in (("Arduino.h", ARDUINO), ("SPI.h", SPI),
("RAK13800_W5100S.h", RAK_HEADER), ("w5100.h", driver)):
(path / name).write_text(content)
(path / "test.cpp").write_text(RUNNER)
exe = path / "test"
subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror", "-pedantic",
"-I", str(path), "-I", str(ROOT), str(path / "test.cpp"), "-o", str(exe),
], check=True)
subprocess.run([str(exe)], check=True, timeout=5)
def test_nonblocking_send_ring_wrap_deadlines_and_faults(self):
self.compile_and_run(PORTABLE_DRIVER)
@unittest.skipUnless(DRIVER.is_file(), "RAK13800 board library is not installed in this native checkout")
def test_actual_vendor_register_header_compatibility(self):
self.compile_and_run(DRIVER.read_text())
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -57,6 +57,7 @@ class SSD1306SmallMessageFontTest(unittest.TestCase):
result = subprocess.run([
"c++", "-std=c++17", "-g",
"-fsanitize=address,undefined", "-DARDUINO=10819",
"-DNRF52_PLATFORM=1", "-DRAK4631_COMBINED_ETHERNET=1",
f"-DUI_SMALL_MESSAGE_FONT={enabled}",
"-I", str(FIXTURE / "mocks"),
"-I", str(ROOT / "src"), "-I", str(library),