Add get leds and set leds on|off for Companion, repeater, room server and sensor builds. Preserve the setting across reboots and cover GPIO, RGB, Bluetooth, notification and expander indicators without changing radio amplifier controls or screen backlights. RAK Companions blink while advertising and keep the Bluetooth LED off once connected, including late timer callbacks. Adapted from meshcore-dev/MeshCore#3605 and #2694. Validated with host regressions and firmware builds for SenseCAP Solar, RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini. Co-authored-by: R0ck <develop@hectospark.co.uk> Co-authored-by: Nick Dunklee <nick@dunklee.org>
287 lines
12 KiB
Python
287 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the actual task wrapper and BLE startup failure handling."""
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_t096_full_memory import method
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
RTOS = r'''
|
|
#pragma once
|
|
#include <cstdint>
|
|
#define PINS_COUNT 64
|
|
#define LED_STATE_ON 1
|
|
#define LED_BLUE 12
|
|
#define OUTPUT 1
|
|
inline int digitalPinToPinName(int pin) { return pin; }
|
|
inline void digitalWrite(int, int) {}
|
|
inline void pinMode(int, int) {}
|
|
using BaseType_t = int;
|
|
using UBaseType_t = unsigned;
|
|
using TaskHandle_t = void*;
|
|
using TaskFunction_t = void (*)(void*);
|
|
using configSTACK_DEPTH_TYPE = uint16_t;
|
|
constexpr BaseType_t pdPASS = 1;
|
|
'''
|
|
|
|
HARNESS = r'''
|
|
#include "Arduino.h"
|
|
#include <cassert>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <helpers/BluetoothMac.h>
|
|
#include <helpers/BleMotaProtocol.h>
|
|
#include <helpers/nrf52/SecuritySessionTimer.h>
|
|
#include <helpers/nrf52/BleDebugTrace.h>
|
|
#include <helpers/nrf52/SleepCycleState.h>
|
|
#ifndef COMPANION_FEATURE_BLE_MOTA_SOURCE
|
|
#define COMPANION_FEATURE_BLE_MOTA_SOURCE 1
|
|
#endif
|
|
#ifndef COMPANION_FEATURE_BLE_DFU
|
|
#define COMPANION_FEATURE_BLE_DFU 1
|
|
#endif
|
|
#ifndef COMPANION_BLE_PRPH_MTU
|
|
#define COMPANION_BLE_PRPH_MTU 247
|
|
#endif
|
|
#ifndef COMPANION_BLE_PRPH_EVENT_LENGTH
|
|
#define COMPANION_BLE_PRPH_EVENT_LENGTH 100
|
|
#endif
|
|
#ifndef COMPANION_BLE_PRPH_HVN_QUEUE
|
|
#define COMPANION_BLE_PRPH_HVN_QUEUE 3
|
|
#endif
|
|
#ifndef COMPANION_BLE_PRPH_WRCMD_QUEUE
|
|
#define COMPANION_BLE_PRPH_WRCMD_QUEUE 1
|
|
#endif
|
|
#define BLE_DEBUG_PRINTLN(...) do {} while (0)
|
|
constexpr int BANDWIDTH_MAX = 3, NRF_SUCCESS = 0, ERROR_NONE = 0;
|
|
constexpr int NRF_ERROR_INVALID_STATE = 8;
|
|
constexpr int BLE_GAP_ADDR_TYPE_RANDOM_STATIC = 1, BLE_TX_POWER = 4;
|
|
constexpr int BLE_MIN_CONN_INTERVAL = 12, BLE_MAX_CONN_INTERVAL = 24;
|
|
constexpr int BLE_SLAVE_LATENCY = 4, BLE_CONN_SUP_TIMEOUT = 200;
|
|
constexpr int SECMODE_ENC_WITH_MITM = 1, SECMODE_NO_ACCESS = 0;
|
|
constexpr int CHR_PROPS_NOTIFY = 1, CHR_PROPS_WRITE = 2;
|
|
constexpr int BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED = 0;
|
|
constexpr int BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE = 2;
|
|
constexpr int BLE_ADV_INTERVAL_MIN = 32, BLE_ADV_INTERVAL_MAX = 244, BLE_ADV_FAST_TIMEOUT = 30;
|
|
struct ble_gap_addr_t { int addr_type; uint8_t addr[6]; };
|
|
struct ble_gap_conn_params_t { unsigned min_conn_interval, max_conn_interval, slave_latency, conn_sup_timeout; };
|
|
int sd_ble_gap_addr_set(ble_gap_addr_t*) { return NRF_SUCCESS; }
|
|
int sd_ble_gap_addr_get(ble_gap_addr_t* a) { memset(a, 0, sizeof(*a)); return NRF_SUCCESS; }
|
|
int sd_softdevice_is_enabled(uint8_t* enabled) { *enabled = 0; return NRF_SUCCESS; }
|
|
namespace mesh_nrf52 {
|
|
int softdeviceIsEnabled(uint8_t& enabled) {
|
|
return sd_softdevice_is_enabled(&enabled);
|
|
}
|
|
}
|
|
int sd_ble_gap_ppcp_set(ble_gap_conn_params_t*) { return NRF_SUCCESS; }
|
|
namespace mesh {
|
|
struct Logging { void println(const char*) {} };
|
|
Logging& usbLoggingPort() { static Logging port; return port; }
|
|
}
|
|
|
|
static int fail_service = 0;
|
|
static bool fail_softdevice = false;
|
|
static unsigned last_depth;
|
|
extern "C" BaseType_t __wrap_xTaskCreate(TaskFunction_t, const char*,
|
|
configSTACK_DEPTH_TYPE, void*, UBaseType_t, TaskHandle_t*);
|
|
extern "C" BaseType_t __real_xTaskCreate(TaskFunction_t, const char* name,
|
|
configSTACK_DEPTH_TYPE depth, void*, UBaseType_t, TaskHandle_t* out) {
|
|
last_depth = depth;
|
|
if (out) *out = reinterpret_cast<void*>(1);
|
|
return pdPASS;
|
|
}
|
|
struct Settings {
|
|
bool isRunning() const { return false; }
|
|
template<class... T> void clearBonds(T...) {}
|
|
template<class... T> void setMITM(T...) {}
|
|
template<class... T> void setPIN(T...) {}
|
|
template<class... T> void setIOCaps(T...) {}
|
|
template<class... T> void setPairPasskeyCallback(T...) {}
|
|
template<class... T> void setPairCompleteCallback(T...) {}
|
|
template<class... T> void setConnectCallback(T...) {}
|
|
template<class... T> void setDisconnectCallback(T...) {}
|
|
template<class... T> void setSecuredCallback(T...) {}
|
|
template<class... T> void setType(T...) {}
|
|
template<class... T> void addFlags(T...) {}
|
|
template<class... T> void addTxPower(T...) {}
|
|
template<class... T> void addService(T...) {}
|
|
template<class... T> void addName(T...) {}
|
|
template<class... T> void setInterval(T...) {}
|
|
template<class... T> void setFastTimeout(T...) {}
|
|
template<class... T> void restartOnDisconnect(T...) {}
|
|
};
|
|
struct BluefruitStub {
|
|
Settings Security, Periph, Advertising, ScanResponse;
|
|
int starts = 0;
|
|
uint16_t mtu = 0, event_length = 0;
|
|
uint8_t hvn_queue = 0, wrcmd_queue = 0;
|
|
void configPrphConn(uint16_t configured_mtu, uint16_t configured_event_length,
|
|
uint8_t configured_hvn_queue,
|
|
uint8_t configured_wrcmd_queue) {
|
|
mtu = configured_mtu;
|
|
event_length = configured_event_length;
|
|
hvn_queue = configured_hvn_queue;
|
|
wrcmd_queue = configured_wrcmd_queue;
|
|
}
|
|
bool begin() {
|
|
++starts;
|
|
if (fail_softdevice) return false;
|
|
TaskHandle_t task;
|
|
// The pinned LTO core resolves these internal calls directly, rather than
|
|
// through the application's linker wrapper.
|
|
__wrap_xTaskCreate(nullptr, "BLE", 1280, nullptr, 3, &task);
|
|
__wrap_xTaskCreate(nullptr, "SOC", 200, nullptr, 3, &task);
|
|
return true;
|
|
}
|
|
void setTxPower(int) {}
|
|
void setName(const char*) {}
|
|
void autoConnLed(bool) {}
|
|
void _setConnLed(bool) {}
|
|
void _startConnLed() {}
|
|
unsigned connected() const { return 0; }
|
|
template<class T> void setEventCallback(T) {}
|
|
} Bluefruit;
|
|
#include <helpers/nrf52/BluetoothLed.h>
|
|
struct Service {
|
|
int id;
|
|
int begin() { return fail_service == id ? 1 : ERROR_NONE; }
|
|
template<class... T> void setPermission(T...) {}
|
|
template<class... T> void setRxCallback(T...) {}
|
|
template<class... T> void setProperties(T...) {}
|
|
template<class... T> void setMaxLen(T...) {}
|
|
template<class... T> void setUserDescriptor(T...) {}
|
|
template<class... T> void setWriteCallback(T...) {}
|
|
};
|
|
struct MotaStream {
|
|
template<class... T> void setSender(T...) {}
|
|
void setActive(bool) {}
|
|
};
|
|
@FORMAT_DEVICE_NAME@
|
|
struct SerialBLEInterface {
|
|
@STARTUP_FIELDS@
|
|
bool _isEnabled = false;
|
|
std::atomic<bool> _companionDataSeen{false};
|
|
SecuritySessionTimer _companion_start_timer;
|
|
std::atomic<bool> _successfulConnectionPending{false};
|
|
std::atomic<uint32_t> _successfulConnectionStarted{0};
|
|
std::atomic<bool> _bondedOnlyRecoveryPending{false}, _advertisingSuppressed{false};
|
|
bool _stealth_pair_once = false, _bonded_only = false, _bonded_only_configure_pending = false;
|
|
mesh::companion::BluetoothPeerIdentity _pending_bonded_peer;
|
|
Service bleuart{1}, _mota_service{2}, _mota_request{3}, _mota_response{4}, bledfu{5};
|
|
MotaStream _mota_stream;
|
|
static void onPairingPasskey() {}
|
|
static void onPairingComplete() {}
|
|
static void onConnect() {}
|
|
static void onDisconnect() {}
|
|
static void onSecured() {}
|
|
static void onBLEEvent() {}
|
|
static void onBleUartRX() {}
|
|
static void sendMotaRequest() {}
|
|
static void onMotaResponse() {}
|
|
void configureBondedOnlyAdvertising(const mesh::companion::BluetoothPeerIdentity&, bool) {}
|
|
bool begin(const char*, const char*, uint32_t, const uint8_t* = nullptr,
|
|
bool = false, bool = false,
|
|
const mesh::companion::BluetoothPeerIdentity* = nullptr);
|
|
};
|
|
static SerialBLEInterface* instance = nullptr;
|
|
@BEGIN@
|
|
int main() {
|
|
__wrap_xTaskCreate(nullptr, "loop", 1024, nullptr, 1, nullptr);
|
|
assert(last_depth == 2048);
|
|
__wrap_xTaskCreate(nullptr, "callback", 768, nullptr, 1, nullptr);
|
|
assert(last_depth == 768);
|
|
__wrap_xTaskCreate(nullptr, nullptr, 100, nullptr, 1, nullptr);
|
|
assert(last_depth == 100);
|
|
for (int fault = 0; fault <= 6; ++fault) {
|
|
#if !COMPANION_FEATURE_BLE_MOTA_SOURCE
|
|
if (fault >= 3 && fault <= 5) continue;
|
|
#endif
|
|
fail_softdevice = fault == 1;
|
|
fail_service = fault >= 2 ? fault - 1 : 0;
|
|
SerialBLEInterface port;
|
|
const int starts = Bluefruit.starts;
|
|
// UART is the required Companion transport. The mOTA and Nordic DFU
|
|
// services are extensions: registration failure must leave UART BLE
|
|
// available for pairing, diagnostics, and recovery.
|
|
const bool expected = fault != 1 && fault != 2;
|
|
auto& sleep_state = mesh_nrf52::sleepCycleState();
|
|
const uint32_t before_start = sleep_state.begin();
|
|
assert(sleep_state.publish(before_start, false, 42));
|
|
mesh_nrf52::SleepCycleSnapshot snapshot;
|
|
assert(sleep_state.snapshot(snapshot));
|
|
assert(snapshot.generation == before_start);
|
|
assert(port.begin("MC-", "T096", 123456) == expected);
|
|
// Even a failed/partial SoftDevice start invalidates the pre-start view:
|
|
// the next sleep decision must query and publish fresh SDK state.
|
|
assert(!sleep_state.current(before_start));
|
|
assert(!sleep_state.snapshot(snapshot));
|
|
assert(Bluefruit.mtu == COMPANION_BLE_PRPH_MTU);
|
|
assert(Bluefruit.event_length == COMPANION_BLE_PRPH_EVENT_LENGTH);
|
|
assert(Bluefruit.hvn_queue == COMPANION_BLE_PRPH_HVN_QUEUE);
|
|
assert(Bluefruit.wrcmd_queue == COMPANION_BLE_PRPH_WRCMD_QUEUE);
|
|
assert(Bluefruit.starts == starts + 1);
|
|
assert((instance == &port) == expected);
|
|
// Retrying a failed partial init must never allocate another stack/FIFO.
|
|
// Likewise, an already-attempted begin does not invalidate a fresh sleep
|
|
// snapshot because it never attempts SoftDevice startup again.
|
|
for (int retry = 0; retry < 4; ++retry) {
|
|
const uint32_t before_retry = sleep_state.begin();
|
|
assert(sleep_state.publish(before_retry, expected, 43 + retry));
|
|
assert(port.begin("MC-", "T096", 123456) == expected);
|
|
assert(Bluefruit.starts == starts + 1);
|
|
assert(sleep_state.current(before_retry));
|
|
assert(sleep_state.snapshot(snapshot));
|
|
assert(snapshot.generation == before_retry);
|
|
assert(snapshot.softdevice_enabled == expected);
|
|
assert(snapshot.usb_status == unsigned(43 + retry));
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class Nrf52BleStartupTest(unittest.TestCase):
|
|
def test_reported_startup_failures_are_not_reinitialized(self):
|
|
serial_source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text()
|
|
formatter = method(serial_source, "static bool formatDeviceName(")
|
|
begin = method(serial_source, "bool SerialBLEInterface::begin(")
|
|
header = (ROOT / "src/helpers/nrf52/SerialBLEInterface.h").read_text()
|
|
# Use the real in-class startup initializers, too.
|
|
fields = "\n".join(line for line in header.splitlines()
|
|
if "bool _begin_" in line
|
|
or "bool _mota_available" in line
|
|
or "char _begin_failure" in line
|
|
or "char _active_name" in line)
|
|
self.assertTrue(fields)
|
|
source = (HARNESS.replace("@BEGIN@", begin)
|
|
.replace("@STARTUP_FIELDS@", fields)
|
|
.replace("@FORMAT_DEVICE_NAME@", formatter))
|
|
with tempfile.TemporaryDirectory(prefix="meshcore-ble-start-") as temp:
|
|
temp = Path(temp)
|
|
(temp / "Arduino.h").write_text(RTOS)
|
|
(temp / "bluefruit.h").write_text("#pragma once\n")
|
|
for name in ("FreeRTOS.h", "task.h"):
|
|
(temp / name).write_text('#include "Arduino.h"\n')
|
|
cpp = temp / "startup.cpp"
|
|
cpp.write_text(source)
|
|
binary = temp / "startup"
|
|
for mota in (0, 1):
|
|
with self.subTest(mota=mota):
|
|
built = subprocess.run([
|
|
"c++", "-std=c++17", "-DNRF52_PLATFORM=1",
|
|
f"-DCOMPANION_FEATURE_BLE_MOTA_SOURCE={mota}", "-I" + str(temp),
|
|
"-I" + str(ROOT / "src"), str(cpp), str(ROOT / "src/Nrf52LoopStack.cpp"),
|
|
"-o", str(binary)], text=True, capture_output=True)
|
|
self.assertEqual(built.returncode, 0, built.stderr)
|
|
result = subprocess.run([str(binary)], text=True, capture_output=True)
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|