feat: add Bluetooth-controlled LoRa OTA
Add the framed BLE mOTA transport and companion control path, complete the full-profile build and runtime fixes, and document and test the OTA workflow.
This commit is contained in:
1 parent
91870e4bb0
commit
e5fd77e53f
79 files changed
+3178
-304
No files matched your search
@@ -51,6 +51,7 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_companion_status_response` | `src/helpers/CompanionStatusResponse.h` | request-tag correlation and minimum status-response length, including rejection of the three-entry ACL payload that previously masqueraded as status |
|
||||
| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp`, `src/helpers/MultiSerialInterface.h` | terminal/seeder control-sequence recognition and passthrough ownership; queued/atomic USB output under backpressure and short writes; partial-frame busy state; requester-affine replies, locked contact streams, and Bluetooth-only pairing routing |
|
||||
| `test_ble_tx_stall_watchdog` | `src/helpers/BleTxStallWatchdog.h` | exact BLE fragment progress; blocked-reply timeout; rollover-safe elapsed time; disconnect recovery retry and completion |
|
||||
| `test_ble_mota_control` | `src/helpers/BleMotaStream.h`, `CompanionMotaControl.h` | encrypted mOTA channel ring buffering, overflow fail-closed behavior, request gating, and strict rejection of injected or USB-ownership control commands |
|
||||
| `test_atomic_file_writer` | `src/helpers/AtomicFileWriter.h` | verified temporary-file commit; short-write, readback, validation, and rename failures; preservation of the live file and stale-temp cleanup |
|
||||
| `test_cad_timing` | `src/helpers/radiolib/CadTiming.h`, `LR2021SideDetectorConfig.h`, `RadioAirtime.h` | Cascade and slow-profile CAD deadlines; invalid airtime handling; bounded LR2021 side-detector parsing and LDRO recomputation |
|
||||
| `test_companion_node_prefs` | `examples/companion_radio/NodePrefs.h` | independent device power saving, RXPS, Wi-Fi, and FEM preferences; one-time migration of the regressed power-saving default |
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helpers/BleMotaStream.h"
|
||||
#include "helpers/CompanionMotaControl.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
bool allowed(const char* text) {
|
||||
return mesh::companion::isBleOtaControlCommandAllowed(
|
||||
reinterpret_cast<const uint8_t*>(text), std::strlen(text));
|
||||
}
|
||||
|
||||
TEST(BleMotaControl, AllowsOnlyBoundedOtaSessionCommands) {
|
||||
EXPECT_TRUE(allowed("tempradio"));
|
||||
EXPECT_TRUE(allowed("tempradio 915,250,5,5,10"));
|
||||
EXPECT_TRUE(allowed("normalradio"));
|
||||
EXPECT_TRUE(allowed("ota"));
|
||||
EXPECT_TRUE(allowed("ota status"));
|
||||
EXPECT_TRUE(allowed("ota neighbors"));
|
||||
|
||||
EXPECT_FALSE(allowed("tempradio "));
|
||||
EXPECT_FALSE(allowed("normalradio now"));
|
||||
EXPECT_FALSE(allowed("otafolder status"));
|
||||
EXPECT_FALSE(allowed("ota folder on"));
|
||||
EXPECT_FALSE(allowed("ota folder off"));
|
||||
EXPECT_FALSE(allowed("reboot"));
|
||||
}
|
||||
|
||||
TEST(BleMotaControl, RejectsCommandInjectionBytes) {
|
||||
const uint8_t embedded_nul[] = {'o', 't', 'a', ' ', 's', 't', 0, 'a'};
|
||||
const uint8_t newline[] = {'o', 't', 'a', ' ', 's', 't', '\n', 'x'};
|
||||
const uint8_t carriage_return[] = {'n', 'o', 'r', 'm', 'a', 'l', 'r',
|
||||
'a', 'd', 'i', 'o', '\r'};
|
||||
const uint8_t shell_chain[] = {'o', 't', 'a', ' ', 's', 't', 'a', 't',
|
||||
'u', 's', ';', 'r', 'e', 'b', 'o', 'o', 't'};
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
embedded_nul, sizeof(embedded_nul)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
newline, sizeof(newline)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
carriage_return, sizeof(carriage_return)));
|
||||
EXPECT_FALSE(mesh::companion::isBleOtaControlCommandAllowed(
|
||||
shell_chain, sizeof(shell_chain)));
|
||||
}
|
||||
|
||||
struct SendCapture {
|
||||
std::vector<uint8_t> bytes;
|
||||
};
|
||||
|
||||
size_t captureSend(void* context, const uint8_t* data, size_t length) {
|
||||
auto* capture = static_cast<SendCapture*>(context);
|
||||
capture->bytes.assign(data, data + length);
|
||||
return length;
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, IsInactiveAndEmptyByDefault) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
const uint8_t byte = 7;
|
||||
EXPECT_FALSE(stream.isActive());
|
||||
EXPECT_FALSE(stream.pushRx(&byte, 1));
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
EXPECT_EQ(stream.write(&byte, 1), 0u);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, CarriesFragmentedResponseAcrossRingWrap) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
|
||||
std::vector<uint8_t> first(200);
|
||||
for (size_t i = 0; i < first.size(); ++i) first[i] = i;
|
||||
ASSERT_TRUE(stream.pushRx(first.data(), first.size()));
|
||||
for (size_t i = 0; i < 190; ++i) {
|
||||
ASSERT_EQ(stream.read(), first[i]);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> second(100);
|
||||
for (size_t i = 0; i < second.size(); ++i) second[i] = 200 + i;
|
||||
ASSERT_TRUE(stream.pushRx(second.data(), second.size()));
|
||||
EXPECT_EQ(stream.available(), 110);
|
||||
for (size_t i = 190; i < first.size(); ++i) {
|
||||
ASSERT_EQ(stream.read(), first[i]);
|
||||
}
|
||||
for (uint8_t value : second) ASSERT_EQ(stream.read(), value);
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, RejectsOverflowWithoutQueuingPartialFragment) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
std::vector<uint8_t> first(240, 0x11);
|
||||
std::vector<uint8_t> overflow(20, 0x22);
|
||||
ASSERT_TRUE(stream.pushRx(first.data(), first.size()));
|
||||
EXPECT_FALSE(stream.pushRx(overflow.data(), overflow.size()));
|
||||
EXPECT_TRUE(stream.overflowed());
|
||||
EXPECT_EQ(stream.available(), 240);
|
||||
while (stream.available()) EXPECT_EQ(stream.read(), 0x11);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, SendsRequestsOnlyWhileActiveAndClearsOnStop) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
SendCapture capture;
|
||||
stream.setSender(captureSend, &capture);
|
||||
stream.setActive(true);
|
||||
const uint8_t request[] = {'M', 'S', 1, 1};
|
||||
EXPECT_EQ(stream.write(request, sizeof(request)), sizeof(request));
|
||||
EXPECT_EQ(capture.bytes,
|
||||
std::vector<uint8_t>(request, request + sizeof(request)));
|
||||
|
||||
const uint8_t response[] = {'m', 's', 1, 0, 1, 1};
|
||||
ASSERT_TRUE(stream.pushRx(response, sizeof(response)));
|
||||
stream.setActive(false);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
EXPECT_EQ(stream.write(request, sizeof(request)), 0u);
|
||||
}
|
||||
|
||||
TEST(BleMotaStream, DisconnectAndRestartDiscardEveryPartialOldResponse) {
|
||||
mesh::ota::BleMotaStream stream;
|
||||
stream.setActive(true);
|
||||
|
||||
const uint8_t partial_old_response[] = {'m', 's', 3, 0, 0xAA, 0xBB};
|
||||
ASSERT_TRUE(stream.pushRx(partial_old_response,
|
||||
sizeof(partial_old_response)));
|
||||
ASSERT_EQ(stream.available(), (int)sizeof(partial_old_response));
|
||||
|
||||
// A BLE loss can happen between response fragments. Starting the next
|
||||
// source session must never splice those stale bytes into its first frame.
|
||||
stream.setActive(false);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
stream.setActive(true);
|
||||
EXPECT_EQ(stream.available(), 0);
|
||||
|
||||
const uint8_t fresh_response[] = {'m', 's', 1, 0, 1, 1};
|
||||
ASSERT_TRUE(stream.pushRx(fresh_response, sizeof(fresh_response)));
|
||||
for (uint8_t expected : fresh_response) {
|
||||
EXPECT_EQ(stream.read(), expected);
|
||||
}
|
||||
EXPECT_EQ(stream.read(), -1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -9,6 +9,9 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "$OPTION3_BUILD_WORKERS" -eq 1 ] \
|
||||
|| fail "logging matrix permits concurrent PlatformIO target builds"
|
||||
|
||||
# Full targets are synthesized from an ordinary transport environment. Check
|
||||
# the resolved PlatformIO configuration so board-specific display, GPS, input,
|
||||
# and BLE constraints cannot silently disappear through that inheritance.
|
||||
@@ -53,6 +56,45 @@ require(m8_usb, "lib_deps", "GxEPD2 @ 1.6.9")
|
||||
reject(m8_usb, "lib_deps", "GxEPD2 @ 1.6.2")
|
||||
|
||||
require("ThinkNode_M5_companion_radio_wifi", "build_flags", "UI_RECENT_LIST_SIZE=9")
|
||||
require("ThinkNode_M5_companion_radio_full", "build_flags", "SERIAL_TX=43")
|
||||
require("ThinkNode_M2_companion_radio_full", "build_flags", "SERIAL_RX=44")
|
||||
require("Xiao_S3_WIO_companion_radio_full", "build_flags", "SERIAL_TX=D6")
|
||||
require("ThinkNode_M7_companion_radio_full", "build_flags", "ETHERNET_USE_CH390")
|
||||
require("ThinkNode_M7_companion_radio_full", "build_src_filter", "helpers/ethernet/ch390")
|
||||
require("ThinkNode_M7_companion_radio_full", "lib_deps", "ESP32-CH390")
|
||||
require("RAK_4631_companion_radio_full", "build_flags", "ETHERNET_USE_RAK13800")
|
||||
require("RAK_4631_companion_radio_full", "build_src_filter", "helpers/ethernet/RAK13800")
|
||||
require("RAK_4631_companion_radio_full", "lib_deps", "RAK13800-W5100S")
|
||||
require("Heltec_E290_companion_usb_ble", "build_flags", "ENABLE_USB_INTERFACE")
|
||||
require("Heltec_E290_companion_usb_ble", "build_flags", "BLE_PIN_CODE=123456")
|
||||
require("Heltec_T190_companion_radio_usb_ble_", "build_flags", "ENABLE_USB_INTERFACE")
|
||||
require("Heltec_T190_companion_radio_usb_ble_", "build_flags", "BLE_PIN_CODE=123456")
|
||||
|
||||
for env_name in (
|
||||
"Heltec_t114_without_display_repeater",
|
||||
"Heltec_t114_repeater",
|
||||
"Heltec_t114_repeater_lora_ota_no_external_sensors",
|
||||
"RAK_3112_repeater",
|
||||
"RAK_11310_repeater",
|
||||
"ProMicro_repeater",
|
||||
"waveshare_rp2040_lora_repeater",
|
||||
"solarxiao_30S_repeater",
|
||||
"solarxiao_33S_repeater",
|
||||
"Heltec_v3_repeater",
|
||||
"Heltec_WSL3_repeater",
|
||||
"Heltec_t096_repeater",
|
||||
"Heltec_t096_repeater_lora_ota_no_external_sensors",
|
||||
"RAK_4631_repeater",
|
||||
"RAK_4631_repeater_lora_ota_no_external_sensors",
|
||||
"LilyGo_TLora_V2_1_1_6_repeater",
|
||||
):
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE=")
|
||||
require(env_name, "build_flags", "RS232_BRIDGE_MERGED=1")
|
||||
require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
|
||||
|
||||
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_ALT=Serial1")
|
||||
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_UART=2")
|
||||
reject("wio-e5_repeater", "build_flags", "WITH_RS232_BRIDGE=")
|
||||
|
||||
for env_name in (
|
||||
"t1000e_companion_radio_usb",
|
||||
@@ -133,6 +175,63 @@ configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "$AUTO_PUBLISH_REDUCED_SECOND_PASS" = 0 ] \
|
||||
|| fail "non-repeater nRF52 target incorrectly scheduled two artifacts"
|
||||
|
||||
# Ordinary USB-loggable roles compile their historical logging profile into
|
||||
# the canonical artifact. OTA receivers, KISS, and BLE keep their distinct
|
||||
# stream/partition contracts. A standard ESP32 artifact still embeds logging
|
||||
# when the same target also has an expanded FULL profile.
|
||||
PIO_ENV_PLATFORM_BY_NAME[wio-e5-mini_repeater]=STM32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_kiss_modem]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[nrf_companion_radio_ble]=NRF52_PLATFORM
|
||||
uses_merged_standard_usb_logging nrf_sensor \
|
||||
|| fail "ordinary nRF52 sensor omitted merged USB logging"
|
||||
uses_merged_standard_usb_logging wio-e5-mini_repeater \
|
||||
|| fail "size-constrained STM32 target omitted merged packet logging"
|
||||
if uses_merged_standard_usb_logging nrf_repeater_lora_ota_no_external_sensors; then
|
||||
fail "LoRa OTA receiver incorrectly merged USB logging"
|
||||
fi
|
||||
if uses_merged_standard_usb_logging nrf_kiss_modem; then
|
||||
fail "KISS target incorrectly merged plaintext USB logging"
|
||||
fi
|
||||
if uses_merged_standard_usb_logging nrf_companion_radio_ble; then
|
||||
fail "BLE Companion incorrectly merged USB logging"
|
||||
fi
|
||||
uses_merged_standard_usb_logging esp_repeater \
|
||||
|| fail "standard ESP32 target omitted logging because FULL also exists"
|
||||
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
MESHDEBUG_OVERRIDE=""
|
||||
PACKET_LOGGING_OVERRIDE=""
|
||||
DISABLE_DEBUG=0
|
||||
apply_merged_standard_usb_logging_profile nrf_sensor
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_PACKET_LOGGING=1* ]] \
|
||||
|| fail "merged profile omitted packet logging"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_DEBUG=1* ]] \
|
||||
|| fail "merged profile omitted mesh diagnostics"
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
apply_merged_standard_usb_logging_profile wio-e5-mini_repeater
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *-DMESH_PACKET_LOGGING=1* ]] \
|
||||
|| fail "constrained merged profile omitted packet logging"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" != *-DMESH_DEBUG=1* ]] \
|
||||
|| fail "constrained merged profile enabled oversized mesh diagnostics"
|
||||
if declare -f run_logging_matrix_build_targets \
|
||||
| grep -q 'FIRMWARE_FILENAME_INFIX="logging"'; then
|
||||
fail "logging matrix still emits a separate standard logging artifact"
|
||||
fi
|
||||
if declare -f get_firmware_filename | grep -q 'filename_infix="logging"'; then
|
||||
fail "explicit packet logging still renames the ordinary artifact"
|
||||
fi
|
||||
FIRMWARE_FILENAME_INFIX=""
|
||||
ESP32_FULL_BUILD=0
|
||||
PACKET_LOGGING_OVERRIDE="on"
|
||||
MQTT_BRIDGE_OVERRIDE="off"
|
||||
[ "$(get_firmware_filename nrf_sensor vtest)" = "nrf_sensor-vtest" ] \
|
||||
|| fail "packet logging still creates a separately named artifact"
|
||||
PACKET_LOGGING_OVERRIDE=""
|
||||
MQTT_BRIDGE_OVERRIDE=""
|
||||
if declare -f run_logging_matrix_build_targets | grep -q 'Profile 2'; then
|
||||
fail "logging matrix still labels a separate Profile 2"
|
||||
fi
|
||||
|
||||
calls=()
|
||||
build_firmware() {
|
||||
calls+=("$1:$BUILD_PROFILE_EFFECTIVE:$SKIP_DECLARED_REDUCTIONS:$FIRMWARE_OUTPUT_ENV_NAME")
|
||||
@@ -220,6 +319,7 @@ SUPPORTED_PIO_ENVS=(
|
||||
Station_G2_companion_radio_ble
|
||||
Station_G2_companion_radio_wifi
|
||||
Station_G2_companion_radio_full
|
||||
Station_G2_terminal_chat
|
||||
)
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
PIO_ENV_PLATFORM_BY_NAME[$env_name]=ESP32_PLATFORM
|
||||
@@ -233,6 +333,87 @@ mapfile -t companion_release_targets < <(
|
||||
[ "${companion_release_targets[0]}" = Station_G2_companion_radio_full ] \
|
||||
|| fail "Station G2 release selected a non-Full Companion artifact"
|
||||
|
||||
# Full Companion's text terminal also replaces a separate Terminal Chat image
|
||||
# in bulk builds. Cover direct ESP32/nRF52 names and the canonical Heltec names
|
||||
# whose hardware/revision suffixes differ from their Terminal Chat targets.
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_3401_terminal_chat]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_3401_companion_radio_full]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_2_v4_3_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_tft_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_v4_tft_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_tracker_v2_terminal_chat]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[heltec_tracker_v2_companion_radio_full_femon]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[PicoW_terminal_chat]=RP2040_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[PicoW_companion_radio_usb]=RP2040_PLATFORM
|
||||
PIO_ENV_BOARD_BY_NAME[PicoW_terminal_chat]=rpipicow
|
||||
PIO_ENV_BOARD_BY_NAME[PicoW_companion_radio_usb]=rpipicow
|
||||
|
||||
[ "$(get_terminal_chat_full_companion_replacement RAK_3401_terminal_chat)" \
|
||||
= RAK_3401_companion_radio_full ] \
|
||||
|| fail "nRF52 Terminal Chat did not map to Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_v4_terminal_chat)" \
|
||||
= heltec_v4_2_v4_3_companion_radio_full_femon ] \
|
||||
|| fail "Heltec V4 Terminal Chat did not map to its canonical Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_v4_tft_terminal_chat)" \
|
||||
= heltec_v4_tft_companion_radio_full_femon ] \
|
||||
|| fail "Heltec V4 TFT Terminal Chat did not map to Full Companion"
|
||||
[ "$(get_terminal_chat_full_companion_replacement heltec_tracker_v2_terminal_chat)" \
|
||||
= heltec_tracker_v2_companion_radio_full_femon ] \
|
||||
|| fail "Heltec Tracker V2 Terminal Chat did not map to Full Companion"
|
||||
is_redundant_bulk_build_target Station_G2_terminal_chat \
|
||||
|| fail "ESP32 Terminal Chat remained in bulk builds beside Full Companion"
|
||||
is_redundant_bulk_build_target RAK_3401_terminal_chat \
|
||||
|| fail "nRF52 Terminal Chat remained in bulk builds beside Full Companion"
|
||||
[ "$(get_terminal_chat_companion_replacement PicoW_terminal_chat)" \
|
||||
= PicoW_companion_radio_usb ] \
|
||||
|| fail "RP2040 Terminal Chat did not map to USB Companion"
|
||||
is_redundant_bulk_build_target PicoW_terminal_chat \
|
||||
|| fail "Terminal Chat remained beside its matching USB Companion"
|
||||
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_4631_companion_radio_ethernet]=NRF52_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[RAK_4631_companion_radio_full]=NRF52_PLATFORM
|
||||
[ "$(get_nrf52_full_companion_replacement \
|
||||
RAK_4631_companion_radio_ethernet)" = RAK_4631_companion_radio_full ] \
|
||||
|| fail "RAK4631 Ethernet Companion did not map to Full Companion"
|
||||
PIO_ENV_PLATFORM_BY_NAME[ThinkNode_M2_companion_radio_serial]=ESP32_PLATFORM
|
||||
PIO_ENV_PLATFORM_BY_NAME[ThinkNode_M2_companion_radio_full]=ESP32_PLATFORM
|
||||
[ "$(get_esp32_full_companion_replacement \
|
||||
ThinkNode_M2_companion_radio_serial)" = ThinkNode_M2_companion_radio_full ] \
|
||||
|| fail "serial Companion did not map to Full Companion"
|
||||
|
||||
[ "$(get_combined_usb_ble_companion_replacement \
|
||||
Heltec_E290_companion_usb)" = Heltec_E290_companion_usb_ble ] \
|
||||
|| fail "E290 USB Companion did not map to USB+BLE Companion"
|
||||
[ "$(get_combined_usb_ble_companion_replacement \
|
||||
Heltec_T190_companion_radio_ble_)" \
|
||||
= Heltec_T190_companion_radio_usb_ble_ ] \
|
||||
|| fail "T190 BLE Companion did not map to USB+BLE Companion"
|
||||
|
||||
if is_redundant_bulk_build_target RAK_4631_repeater_ethernet \
|
||||
|| is_redundant_bulk_build_target RAK_4631_room_server_ethernet; then
|
||||
fail "RAK4631 repeater/room Ethernet artifact was incorrectly merged"
|
||||
fi
|
||||
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
RAK_4631_repeater_bridge_rs232_serial1)" = RAK_4631_repeater ] \
|
||||
|| fail "RAK4631 Serial1 bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors)" \
|
||||
= RAK_4631_repeater_lora_ota_no_external_sensors ] \
|
||||
|| fail "RAK4631 Serial2 OTA bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
Heltec_t114_repeater_bridge_rs232)" = Heltec_t114_repeater ] \
|
||||
|| fail "T114 RS232 bridge did not map to merged repeater"
|
||||
[ "$(get_merged_rs232_repeater_replacement \
|
||||
Heltec_t096_repeater_bridge_rs232)" = Heltec_t096_repeater ] \
|
||||
|| fail "T096 RS232 bridge did not map to merged repeater"
|
||||
is_redundant_bulk_build_target Heltec_v3_repeater_bridge_rs232 \
|
||||
|| fail "merged RS232 bridge remained in canonical bulk builds"
|
||||
if is_redundant_bulk_build_target wio-e5-repeater_bridge_rs232; then
|
||||
fail "capacity-constrained Wio-E5 RS232 bridge was incorrectly merged"
|
||||
fi
|
||||
|
||||
# A WiFi base's final -UENABLE_OTA must not win over the Full Companion's
|
||||
# source-only OTA overlay. That mismatch compiles out both the TCP terminal and
|
||||
# seeder while still leaving a superficially valid binary.
|
||||
|
||||
@@ -414,6 +414,70 @@ TEST(CLICommandUtils, RejectsInvalidOrOverflowingIntegers) {
|
||||
EXPECT_EQ(123, value);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, ParsesRadioTuplesWithoutFloatScanf) {
|
||||
float frequency = 0.0f;
|
||||
float bandwidth = 0.0f;
|
||||
uint8_t spreading_factor = 0;
|
||||
uint8_t coding_rate = 0;
|
||||
uint32_t timeout_minutes = 0;
|
||||
|
||||
EXPECT_TRUE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FLOAT_EQ(910.525f, frequency);
|
||||
EXPECT_FLOAT_EQ(62.5f, bandwidth);
|
||||
EXPECT_EQ(7, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
|
||||
EXPECT_TRUE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
" 909.950 , 250 , 5 , 5 , 120 ", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FLOAT_EQ(909.950f, frequency);
|
||||
EXPECT_FLOAT_EQ(250.0f, bandwidth);
|
||||
EXPECT_EQ(5, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
EXPECT_EQ(120u, timeout_minutes);
|
||||
}
|
||||
|
||||
TEST(CLICommandUtils, RejectsMalformedRadioTuples) {
|
||||
float frequency = 123.0f;
|
||||
float bandwidth = 456.0f;
|
||||
uint8_t spreading_factor = 7;
|
||||
uint8_t coding_rate = 5;
|
||||
uint32_t timeout_minutes = 60;
|
||||
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
nullptr, frequency, bandwidth, spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,7,5,1", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525x,62.5,7,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
EXPECT_FALSE(mesh::cli::parseRadioTupleStrict(
|
||||
"910.525,62.5,256,5", frequency, bandwidth,
|
||||
spreading_factor, coding_rate));
|
||||
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,1,99", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,1x", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
EXPECT_FALSE(mesh::cli::parseTemporaryRadioTupleStrict(
|
||||
"909.950,250,5,5,2147483648", frequency, bandwidth,
|
||||
spreading_factor, coding_rate, timeout_minutes));
|
||||
|
||||
EXPECT_FLOAT_EQ(123.0f, frequency);
|
||||
EXPECT_FLOAT_EQ(456.0f, bandwidth);
|
||||
EXPECT_EQ(7, spreading_factor);
|
||||
EXPECT_EQ(5, coding_rate);
|
||||
EXPECT_EQ(60u, timeout_minutes);
|
||||
}
|
||||
|
||||
TEST(RadioPowerLimits, LeavesUnspecifiedBackendRangeToDriver) {
|
||||
EXPECT_EQ(INT8_MIN, mesh::minLoRaTxPowerForFrequency(915.0f));
|
||||
EXPECT_EQ(INT8_MAX, mesh::maxLoRaTxPowerForFrequency(915.0f));
|
||||
|
||||
@@ -217,6 +217,7 @@ TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
saved.radio_fem_rxgain = 0;
|
||||
saved.radio_fem_txgain = 1;
|
||||
saved.usb_logging_enabled = 0;
|
||||
saved.bridge_uart = 2;
|
||||
|
||||
MockPrintStream output;
|
||||
ASSERT_TRUE(saved.saveSerial(output));
|
||||
@@ -225,17 +226,20 @@ TEST(NodePrefs, FemGainSettingsRoundTrip) {
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("usb_log:0"));
|
||||
EXPECT_NE(std::string::npos, serialised.find("uart:2"));
|
||||
|
||||
MockInputStream input(serialised.c_str());
|
||||
NodePrefs loaded;
|
||||
loaded.radio_fem_rxgain = 1;
|
||||
loaded.radio_fem_txgain = 0;
|
||||
loaded.usb_logging_enabled = 1;
|
||||
loaded.bridge_uart = 1;
|
||||
|
||||
ASSERT_TRUE(loaded.loadSerial(input));
|
||||
EXPECT_EQ(0, loaded.radio_fem_rxgain);
|
||||
EXPECT_EQ(1, loaded.radio_fem_txgain);
|
||||
EXPECT_EQ(0, loaded.usb_logging_enabled);
|
||||
EXPECT_EQ(2, loaded.bridge_uart);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -158,6 +158,31 @@ assert(catalog.rows.some(function (item) {
|
||||
return item.target === "RAK_4631_companion_radio_usb-logging";
|
||||
}));
|
||||
|
||||
const rakSensor = profile("RAK_4631_sensor");
|
||||
assert.strictEqual(rakSensor.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(rakSensor.loggingModes, ["none", "usb"]);
|
||||
|
||||
const standardBesideFull = picker.applyMergedStandardUsbLoggingCapabilities([
|
||||
Object.assign(picker.parseTargetProfile("Example_repeater"), {
|
||||
hardware: "Example",
|
||||
role: "repeater",
|
||||
variant: "default",
|
||||
feature: "standard",
|
||||
logging: "none",
|
||||
ota: "none",
|
||||
}),
|
||||
Object.assign(picker.parseTargetProfile("Example_repeater-full-logging-ota"), {
|
||||
hardware: "Example",
|
||||
role: "repeater",
|
||||
variant: "default",
|
||||
feature: "full",
|
||||
logging: "usb",
|
||||
ota: "lora-receiver",
|
||||
}),
|
||||
]);
|
||||
assert.strictEqual(standardBesideFull[0].logging, "usb-runtime");
|
||||
assert.deepStrictEqual(standardBesideFull[0].loggingModes, ["none", "usb"]);
|
||||
|
||||
const v4Full = profile("heltec_v4_2_v4_3_companion_radio_full_femon");
|
||||
assert.strictEqual(v4Full.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(v4Full.loggingModes, ["none", "usb"]);
|
||||
@@ -192,6 +217,58 @@ assert.strictEqual(v4Full.dedicatedUsbLogging, true);
|
||||
}
|
||||
);
|
||||
|
||||
const combinedUsbBle = [
|
||||
picker.parseTargetProfile("Heltec_E290_companion_usb"),
|
||||
picker.parseTargetProfile("Heltec_E290_companion_ble"),
|
||||
picker.parseTargetProfile("Heltec_E290_companion_usb_ble"),
|
||||
];
|
||||
combinedUsbBle.forEach(function (item) {
|
||||
item.installKinds = ["bin"];
|
||||
});
|
||||
assert.strictEqual(combinedUsbBle[2].mode, "usb-ble");
|
||||
assert.strictEqual(combinedUsbBle[2].variant, "default");
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(combinedUsbBle).map(function (item) {
|
||||
return item.target;
|
||||
}),
|
||||
["Heltec_E290_companion_usb_ble"]
|
||||
);
|
||||
|
||||
const fullWithWiredTransports = [
|
||||
"ThinkNode_M7_companion_radio_full",
|
||||
"ThinkNode_M7_companion_radio_usb",
|
||||
"ThinkNode_M7_companion_radio_ble",
|
||||
"ThinkNode_M7_companion_radio_wifi",
|
||||
"ThinkNode_M7_companion_radio_serial",
|
||||
"ThinkNode_M7_companion_radio_ethernet",
|
||||
"ThinkNode_M7_terminal_chat",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["bin"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(fullWithWiredTransports).map(
|
||||
function (item) { return item.target; }
|
||||
),
|
||||
["ThinkNode_M7_companion_radio_full"]
|
||||
);
|
||||
|
||||
const terminalWithUsb = [
|
||||
"PicoW_terminal_chat",
|
||||
"PicoW_companion_radio_usb",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["uf2"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(terminalWithUsb).map(
|
||||
function (item) { return item.target; }
|
||||
),
|
||||
["PicoW_companion_radio_usb"]
|
||||
);
|
||||
|
||||
const companionBle = picker.parseTargetProfile(
|
||||
"Heltec_t096_companion_radio_ble_ps_femon"
|
||||
);
|
||||
@@ -323,10 +400,23 @@ assert.deepStrictEqual(
|
||||
const standardRepeater = profile("Station_G2_repeater");
|
||||
assert.strictEqual(standardRepeater.hardwareFamily, "Station_G2");
|
||||
assert.strictEqual(standardRepeater.role, "repeater");
|
||||
assert.strictEqual(standardRepeater.logging, "none");
|
||||
assert.strictEqual(standardRepeater.logging, "usb-runtime");
|
||||
assert.deepStrictEqual(standardRepeater.loggingModes, ["none", "usb"]);
|
||||
assert.strictEqual(standardRepeater.ota, "none");
|
||||
assert.strictEqual(standardRepeater.mode, "standard");
|
||||
assert.strictEqual(standardRepeater.feature, "standard");
|
||||
|
||||
const mergedLoggingProfiles = picker.applyMergedStandardUsbLoggingCapabilities([
|
||||
Object.assign(picker.parseTargetProfile("PicoW_room_server"), {
|
||||
ota: "none",
|
||||
}),
|
||||
Object.assign(picker.parseTargetProfile("PicoW_kiss_modem"), {
|
||||
ota: "none",
|
||||
}),
|
||||
]);
|
||||
assert.strictEqual(mergedLoggingProfiles[0].logging, "usb-runtime");
|
||||
assert.deepStrictEqual(mergedLoggingProfiles[0].loggingModes, ["none", "usb"]);
|
||||
assert.strictEqual(mergedLoggingProfiles[1].logging, "none");
|
||||
assert.strictEqual(
|
||||
picker.canonicalAsset(standardRepeater.files, "merged-bin").name,
|
||||
"Station_G2_repeater-" + family + "-merged.bin"
|
||||
@@ -390,6 +480,34 @@ assert.strictEqual(wio.hardware, "wio-e5");
|
||||
assert.strictEqual(wio.role, "repeater");
|
||||
assert.strictEqual(wio.mode, "rs232");
|
||||
|
||||
const mergedRs232 = [
|
||||
"RAK_4631_repeater",
|
||||
"RAK_4631_repeater_bridge_rs232_serial1",
|
||||
"RAK_4631_repeater_bridge_rs232_serial2",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["uf2"],
|
||||
});
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
picker.omitTransportsReplacedByFull(mergedRs232).map(function (item) {
|
||||
return item.target;
|
||||
}),
|
||||
["RAK_4631_repeater"]
|
||||
);
|
||||
const constrainedRs232 = [
|
||||
"wio-e5_repeater",
|
||||
"wio-e5-repeater_bridge_rs232",
|
||||
].map(function (target) {
|
||||
return Object.assign(picker.parseTargetProfile(target), {
|
||||
installKinds: ["hex"],
|
||||
});
|
||||
});
|
||||
assert.strictEqual(
|
||||
picker.omitTransportsReplacedByFull(constrainedRs232).length,
|
||||
2
|
||||
);
|
||||
|
||||
const matches = catalog.profiles.filter(function (item) {
|
||||
return picker.profileMatches(item, {
|
||||
hardware: "Station_G2",
|
||||
@@ -397,10 +515,12 @@ const matches = catalog.profiles.filter(function (item) {
|
||||
logging: "usb",
|
||||
});
|
||||
});
|
||||
assert.strictEqual(matches.length, 1);
|
||||
assert.strictEqual(
|
||||
matches[0].target,
|
||||
"Station_G2_repeater_observer_mqtt-full-usb-wifi"
|
||||
assert.deepStrictEqual(
|
||||
matches.map(function (item) { return item.target; }).sort(),
|
||||
[
|
||||
"Station_G2_repeater",
|
||||
"Station_G2_repeater_observer_mqtt-full-usb-wifi",
|
||||
]
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
|
||||
@@ -1396,6 +1396,24 @@ static bool gated_capture_send(void* ctx, const uint8_t* msg, uint16_t len, bool
|
||||
capture->items.emplace_back(msg, msg + len);
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(OtaMetrics, CountsOnlyPacketsAcceptedByTheRadioAdapter) {
|
||||
OtaManager manager;
|
||||
GatedCapture sent;
|
||||
manager.begin(0, gated_capture_send, &sent);
|
||||
|
||||
EXPECT_EQ(manager.packetsSent(), 0u);
|
||||
manager.announce();
|
||||
EXPECT_EQ(manager.packetsSent(), 0u);
|
||||
EXPECT_TRUE(sent.items.empty());
|
||||
|
||||
sent.accept = true;
|
||||
manager.announce();
|
||||
manager.announce();
|
||||
EXPECT_EQ(manager.packetsSent(), 2u);
|
||||
EXPECT_EQ(sent.items.size(), 2u);
|
||||
}
|
||||
|
||||
// Drive the bus to quiescence: deliver queued messages; when idle, advance the client's clock (monotonic
|
||||
// across calls, so a jittered query scheduled in a prior pump still comes due) and call loop() (fires the
|
||||
// scheduled catalog query / block re-requests). Two idle ticks in a row = quiescent.
|
||||
|
||||
Reference in new issue
Block a user