diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 291e955..73701d7 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -21,8 +21,11 @@ jobs: - name: Install ESP32 core and NimBLE run: | arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json - arduino-cli core install esp32:esp32@3.3.10 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json - arduino-cli lib install NimBLE-Arduino@2.5.0 + arduino-cli core install esp32:esp32@3.3.12 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json + arduino-cli lib install NimBLE-Arduino@2.5.1 + + - name: Run host regression tests + run: bash tests/run.sh - name: Compile TrainerBridge run: arduino-cli compile --warnings all --fqbn esp32:esp32:esp32 TrainerBridge diff --git a/README.md b/README.md index c5299b3..81b91a2 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ $$P_{\text{wheel}} = \left(C_{\text{rr}} \cdot m_{\text{total}} \cdot g\right) v ## Requirements -- ESP32 Arduino core **3.3.10** -- [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) **2.5.0** +- ESP32 Arduino core **3.3.12** +- [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) **2.5.1** - Arduino CLI or Arduino IDE --- @@ -103,14 +103,14 @@ When operating headless without a serial monitor, the onboard LED provides immed ## Build and Flash -Install dependencies and compile with zero warnings: +Install the pinned dependencies and compile with all warnings enabled: ```bash arduino-cli core update-index \ --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json -arduino-cli core install esp32:esp32@3.3.10 \ +arduino-cli core install esp32:esp32@3.3.12 \ --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json -arduino-cli lib install NimBLE-Arduino@2.5.0 +arduino-cli lib install NimBLE-Arduino@2.5.1 arduino-cli compile --warnings all --fqbn esp32:esp32:esp32 TrainerBridge ``` @@ -122,6 +122,26 @@ arduino-cli upload --port /dev/ttyUSB0 --fqbn esp32:esp32:esp32 TrainerBridge --- +## Software Checks and Hardware Validation + +Run the host regression harness with a C++17 compiler: + +```bash +bash tests/run.sh +``` + +The harness executes the firmware callbacks and loop with Arduino/NimBLE test doubles, +including the existing module self-checks, scan retry timing, repeated reconnects, +multi-client telemetry routing, and FTMS transactions. CI also compiles against the +real pinned libraries with `--warnings all`. Test doubles do not validate the BLE +stack, FreeRTOS concurrency, radio delivery, or scan-response timer lifetime. + +See [the patch upgrade validation notes](tests/VALIDATION.md) for coverage, known +limitations, and the physical ESP32/trainer/Garmin/app checks still required for +ESP32 Arduino 3.3.12 and NimBLE-Arduino 2.5.1. + +--- + ## Pairing Guide ### Connecting to Garmin Edge or Watch diff --git a/tests/VALIDATION.md b/tests/VALIDATION.md new file mode 100644 index 0000000..32357c9 --- /dev/null +++ b/tests/VALIDATION.md @@ -0,0 +1,89 @@ +# Stable patch upgrade validation + +## Dependency and API review + +- [ESP32 Arduino 3.3.11](https://github.com/espressif/arduino-esp32/releases/tag/3.3.11) + and [3.3.12](https://github.com/espressif/arduino-esp32/releases/tag/3.3.12): + reviewed both intervening release notes, including ESP-IDF changes. The sketch + uses Arduino timing, Serial, GPIO, FreeRTOS critical sections and the classic + ESP32 brownout register. It uses NimBLE-Arduino, not the core's separate BLE + library, so the core BLE fixes are not proof of a TrainerBridge behavior change. +- [NimBLE-Arduino 2.5.1](https://github.com/h2zero/NimBLE-Arduino/releases/tag/2.5.1) + and its [2.5.0 comparison](https://github.com/h2zero/NimBLE-Arduino/compare/2.5.0...2.5.1): + reviewed scan timer lifecycle changes and the public signatures used by this + sketch. Scan callbacks/start, client callbacks/connect/subscriptions/writes, + server callbacks, broadcast `notify(data, length)` and connection-specific + `indicate(data, length, handle)` remain compatible. No firmware migration needed. +- The scan-response timer fix initializes the callout lazily and tears it down + before the host event queue. TrainerBridge calls `NimBLEDevice::init` once and + never calls `deinit`; ordinary reconnect reuses the host/client. This does not + reproduce or establish exposure to the upstream reinitialization crash. + +## Reproducible software checks + +```bash +bash tests/run.sh +arduino-cli compile --warnings all --fqbn esp32:esp32:esp32 TrainerBridge +``` + +`tests/run.sh` includes the actual sketch and links the actual helper modules. +The stubs only provide hardware/stack boundaries; they are not a NimBLE emulator. +The Arduino compile is the compatibility check against the real dependencies. + +Host assertions cover: + +- Existing matcher, LED and virtual-speed self-checks. +- Nonmatching/matching advertisements, address copying, scan completion, 8-second + scans and 3-second retry boundaries; no extra retry on a matched scan stopping. +- Twenty disconnect/rediscovery/reconnect cycles, without another host init, + including queue/filter cleanup, remote-pointer clearing, zero-power telemetry, + failed reconnect and failed telemetry subscription. +- Two subscriber handles receive identical CPS/CSC/FTMS payloads; advertising + pauses at two clients and resumes below two after its existing throttle. +- FTMS Request Control, Start and Reset responses; queued ERG and simulation + writes; response routing to the originating handle; successful/failed trainer + replies; failed writes; timeout and late-response suppression; a departing app's + pending response; empty/oversized writes; truncated telemetry; queue capacity, + payload bounds and existing coalescing behavior. + +## Results (2026-10-07) + +- Host regression harness: PASS, including twenty simulated reconnect cycles. +- Arduino CLI 1.5.2-rc.1, ESP32 Arduino 3.3.12 and NimBLE-Arduino 2.5.1: + the exact compile command above passed with no warnings. Flash: 627,988 / + 1,310,720 bytes (47%); static RAM: 37,172 / 327,680 bytes (11%). +- AddressSanitizer and UndefinedBehaviorSanitizer: PASS for the host harness with + `ASAN_OPTIONS=detect_leaks=0`. LeakSanitizer cannot inspect processes in this + execution environment; neither this run nor the mocks validate physical leaks. +- Hardware checks below: not executed. + +## Existing behavior outside this compatibility change + +These are not claims of complete FTMS correctness: the queue coalesces by opcode +across clients, retaining the latest handle; pending trainer commands are cleared +on trainer disconnect without an immediate failed indication. The initial Request +Control attempt occurs before `realConnected` is set, and the write helper declines +it. The response state machine does not compare a received opcode with the pending +opcode. These pre-existing semantics are unchanged; concurrent controllers, +out-of-order/late replies and trainer control acquisition require separate work +if physical tests expose failures. This patch does not change protocol policy. + +## Physical checks still required + +No physical ESP32, trainer, Garmin or app is available in the software harness. + +1. Flash the build on the supported standard ESP32 and D100. Confirm serial module + self-checks, discovery by name/MAC, active scan responses and LED states. +2. Power-cycle the trainer at least 20 times with Garmin and an app still paired. + Check zero power on loss, scan retry after three seconds, rediscovery, fresh + telemetry, recovered advertising, and no crash/watchdog/heap growth. +3. Connect Garmin and an app concurrently. Verify CPS, CSC and FTMS subscriptions, + power/speed/distance delivery, disconnect/rejoin and continued delivery to the + remaining subscriber. Exercise subscription changes and MTU negotiation. +4. Exercise Request Control, ERG target power, simulation grade, Start/Stop and + Reset against the real trainer. Verify resistance changes and response/status + delivery only as intended; disconnect each side during a pending transaction, + provoke timeout/late replies, and repeat reconnect plus control acquisition. +5. The upstream init/deinit timer fix is not exercised by this firmware's normal + reconnect. A separate upstream reproduction is needed to validate that fix on + hardware; the host harness cannot certify the NimBLE timer or event queue. diff --git a/tests/firmware_test.cpp b/tests/firmware_test.cpp new file mode 100644 index 0000000..27aaf4e --- /dev/null +++ b/tests/firmware_test.cpp @@ -0,0 +1,169 @@ +#include +#include +#include "../TrainerBridge/TrainerBridge.ino" + +// Call private overrides through the public NimBLE callback interface. +static void disconnectTrainer() { + static_cast(&realClientCallbacks)->onDisconnect(realClient,19); +} +static void writeCp(std::initializer_list bytes,uint16_t handle=11) { + std::vector data(bytes); + virtualControlPointChr->setValue(data.data(),data.size()); + NimBLEConnInfo info(handle); + static_cast(&virtualControlPointCallbacks)->onWrite(virtualControlPointChr,info); +} +static void clearDeliveries() { + for(auto* c:{virtualCpsMeasurementChr,virtualCscMeasurementChr,virtualIndoorBikeDataChr,virtualStatusChr,virtualControlPointChr}) { + c->notifications.clear(); c->indications.clear(); + } +} +static void reply(uint8_t opcode,uint8_t result=FTMS_CP_RES_SUCCESS) { + uint8_t bytes[]={FTMS_CP_OP_RESPONSE_CODE,opcode,result}; + realControlPointCallback(nullptr,bytes,sizeof(bytes),false); +} +static void assertCp(uint8_t opcode,uint8_t result,uint16_t handle) { + const auto& d=virtualControlPointChr->indications.back(); + assert(d.handle==handle); + assert((d.bytes==std::vector{FTMS_CP_OP_RESPONSE_CODE,opcode,result})); +} +static void completeScan() { + static_cast(&realScanCallbacks)->onScanEnd(NimBLEScanResults{},0); +} +int main() { + assert(TrainerMatcher::selfCheck()); + assert(StatusLed::selfCheck()); + assert(VirtualSpeedModel::selfCheck()); + setup(); + assert(NimBLEDevice::initCalls==1); + loop(); + auto* scan=NimBLEDevice::getScan(); + assert(scanning && scan->starts==1 && scan->duration==8000 && scan->clears==1); + NimBLEAdvertisedDevice other; other.name="Other trainer"; + static_cast(&realScanCallbacks)->onResult(&other); + assert(!doConnectReal && scanning); + completeScan(); + assert(doScan && !scanning); + const auto retry=lastScanAt; + testNow=retry-1; loop(); assert(scan->starts==1); + testNow=retry; loop(); assert(scan->starts==2); + NimBLEAdvertisedDevice trainer; + static_cast(&realScanCallbacks)->onResult(&trainer); + assert(doConnectReal && !doScan && !scanning); + assert(realTrainerAddress.toString()==trainer.address.toString()); + completeScan(); assert(!doScan); // Stop completion must not schedule another scan after match. + loop(); + assert(realConnected && proxyReady && realClient); + assert(realIndoorBikeDataChr->callback==realIndoorBikeDataCallback); + assert(realControlPointChr->callback==realControlPointCallback); + proxyServer->connectedCount=2; loop(); assert(!NimBLEDevice::advertising.active); + proxyServer->connectedCount=1; testNow+=500; loop(); assert(NimBLEDevice::advertising.active); + + clearDeliveries(); + uint8_t telemetry[]={0x40,0,0,0,200,0}; // Speed and instantaneous power. + realIndoorBikeDataCallback(nullptr,telemetry,sizeof(telemetry),true); + for(auto* c:{virtualCpsMeasurementChr,virtualCscMeasurementChr,virtualIndoorBikeDataChr}) { + assert(c->notifications.size()==2); + assert(c->notifications[0].handle==11 && c->notifications[1].handle==22); + assert(c->notifications[0].bytes==c->notifications[1].bytes); + } + assert(readS16(virtualCpsMeasurementChr->notifications[0].bytes.data(),2)==200); + assert(readS16(virtualIndoorBikeDataChr->notifications[0].bytes.data(),7)==200); + int16_t parsed=0; + assert(!parseRealIndoorBikeData(telemetry,5,parsed)); + assert(!parseRealIndoorBikeData(telemetry,1,parsed)); + + clearDeliveries(); + writeCp({FTMS_CP_OP_REQUEST_CONTROL}); + assertCp(FTMS_CP_OP_REQUEST_CONTROL,FTMS_CP_RES_SUCCESS,11); + assert(commandQueue.isEmpty()); + writeCp({FTMS_CP_OP_START_RESUME}); + assertCp(FTMS_CP_OP_START_RESUME,FTMS_CP_RES_SUCCESS,11); + writeCp({FTMS_CP_OP_RESET}); + assertCp(FTMS_CP_OP_RESET,FTMS_CP_RES_SUCCESS,11); + writeCp({}); + std::vector oversized(21,FTMS_CP_OP_SET_TARGET_POWER); + virtualControlPointChr->setValue(oversized.data(),oversized.size()); + NimBLEConnInfo info(22); + static_cast(&virtualControlPointCallbacks)->onWrite(virtualControlPointChr,info); + assertCp(FTMS_CP_OP_SET_TARGET_POWER,FTMS_CP_RES_INVALID_PARAMETER,22); + writeCp({FTMS_CP_OP_SET_TARGET_POWER,250,0},22); + processQueuedCommands(); + assert(cpTxState==CpTxState::WAITING_RESPONSE); + assert(realControlPointChr->writes.back()==std::vector({FTMS_CP_OP_SET_TARGET_POWER,250,0})); + auto statuses=virtualStatusChr->notifications.size(); + reply(FTMS_CP_OP_SET_TARGET_POWER); + assertCp(FTMS_CP_OP_SET_TARGET_POWER,FTMS_CP_RES_SUCCESS,22); + assert(cpTxState==CpTxState::IDLE && activeTargetPower==250); + assert(virtualStatusChr->notifications.size()==statuses+2); + writeCp({FTMS_CP_OP_SET_INDOOR_BIKE_SIM,0,0,1,0,4,32}); + processQueuedCommands(); reply(FTMS_CP_OP_SET_INDOOR_BIKE_SIM); + assertCp(FTMS_CP_OP_SET_INDOOR_BIKE_SIM,FTMS_CP_RES_SUCCESS,11); + assert(virtualStatusChr->notifications.back().bytes.size()==7); + writeCp({FTMS_CP_OP_SET_TARGET_POWER,100,0}); + processQueuedCommands(); statuses=virtualStatusChr->notifications.size(); + reply(FTMS_CP_OP_SET_TARGET_POWER,FTMS_CP_RES_OPERATION_FAILED); + assertCp(FTMS_CP_OP_SET_TARGET_POWER,FTMS_CP_RES_OPERATION_FAILED,11); + assert(virtualStatusChr->notifications.size()==statuses && activeTargetPower==250); + realControlPointChr->writeOk=false; + writeCp({FTMS_CP_OP_STOP_PAUSE,1}); processQueuedCommands(); + assertCp(FTMS_CP_OP_STOP_PAUSE,FTMS_CP_RES_OPERATION_FAILED,11); + realControlPointChr->writeOk=true; + writeCp({FTMS_CP_OP_STOP_PAUSE,1}); processQueuedCommands(); + testNow+=CP_TIMEOUT_MS+1; processQueuedCommands(); + assertCp(FTMS_CP_OP_STOP_PAUSE,FTMS_CP_RES_OPERATION_FAILED,11); + const auto indications=virtualControlPointChr->indications.size(); + reply(FTMS_CP_OP_STOP_PAUSE); + assert(virtualControlPointChr->indications.size()==indications && cpTxState==CpTxState::IDLE); + + // A departing app must not receive the pending trainer response. + writeCp({FTMS_CP_OP_SET_TARGET_POWER,150,0}); processQueuedCommands(); + NimBLEConnInfo departing(11); + static_cast(&proxyServerCallbacks)->onDisconnect(proxyServer,departing,19); + reply(FTMS_CP_OP_SET_TARGET_POWER); + assert(virtualControlPointChr->indications.size()==indications); + + // Repeat disconnect -> delayed scan -> rediscovery -> reconnect, keeping the same host/client. + auto* client=realClient; + for(int cycle=0;cycle<20;++cycle) { + writeCp({FTMS_CP_OP_SET_TARGET_POWER,180,0}); + processQueuedCommands(); + assert(cpTxState==CpTxState::WAITING_RESPONSE); + writeCp({FTMS_CP_OP_STOP_PAUSE,1}); + assert(!commandQueue.isEmpty()); + clearDeliveries(); + disconnectTrainer(); + assert(!realConnected && !proxyReady && !realIndoorBikeDataChr && !realControlPointChr); + assert(commandQueue.isEmpty() && cpTxState==CpTxState::IDLE); + assert(powerFilter.getLastOutput()==0 && virtualSpeedModel.getSpeedMps()==0); + assert(!NimBLEDevice::advertising.active && doScan); + assert(readS16(virtualCpsMeasurementChr->notifications.back().bytes.data(),2)==0); + assert(readS16(virtualIndoorBikeDataChr->notifications.back().bytes.data(),7)==0); + writeCp({FTMS_CP_OP_SET_TARGET_POWER,200,0}); + assertCp(FTMS_CP_OP_SET_TARGET_POWER,FTMS_CP_RES_OPERATION_FAILED,11); + const auto starts=scan->starts; + testNow=lastScanAt-1; loop(); assert(scan->starts==starts); + testNow=lastScanAt; loop(); assert(scan->starts==starts+1); + static_cast(&realScanCallbacks)->onResult(&trainer); + loop(); assert(realConnected && proxyReady && realClient==client); + assert(NimBLEDevice::initCalls==1); + } + CommandQueue queue; + uint8_t command[]={FTMS_CP_OP_SET_TARGET_POWER,1,0}; + assert(queue.push(command,3,command[0],1,11)); + command[1]=2; assert(queue.push(command,3,command[0],2,22)); + QueuedCommand out{}; assert(queue.count()==1 && queue.pop(out)); + assert(out.targetValue==2 && out.connHandle==22); // Existing cross-client coalescing behavior. + assert(!queue.push(nullptr,3,command[0])); + assert(!queue.push(command,21,command[0])); + // Failed reconnect and failed telemetry subscription return to the scan retry path. + disconnectTrainer(); realClient->connectOk=false; doConnectReal=true; + loop(); assert(!realClient && !realConnected && doScan); + realClient=NimBLEDevice::createClient(); + realClient->service.data.subscribeOk=false; doConnectReal=true; + loop(); assert(!realClient && !proxyReady && doScan); + assert(queue.push(command,3,FTMS_CP_OP_STOP_PAUSE)); + for(size_t i=1;i +#include +#include +#include +#include +#include +#include +inline unsigned long testNow = 0; +inline unsigned long millis() { return testNow; } +inline void delay(unsigned long ms) { testNow += ms; } +constexpr int OUTPUT=1, HIGH=1, LOW=0, HEX=16; +inline void pinMode(int, int) {} +inline void digitalWrite(int, int) {} +class String { + std::string s; +public: + String(const char* v=""): s(v) {} + size_t length() const { return s.size(); } + void toLowerCase() { std::transform(s.begin(),s.end(),s.begin(),[](unsigned char c){return std::tolower(c);}); } + int indexOf(const String& v) const { auto p=s.find(v.s); return p==std::string::npos?-1:static_cast(p); } + bool operator==(const String& v) const { return s==v.s; } + bool startsWith(const char* v) const { return s.rfind(v,0)==0; } + String substring(size_t i) const { return String(s.substr(i).c_str()); } + void trim() { auto a=s.find_first_not_of(" \t\r\n"); if(a==std::string::npos) {s.clear(); return;} s=s.substr(a,s.find_last_not_of(" \t\r\n")-a+1); } + float toFloat() const { return std::strtof(s.c_str(),nullptr); } + int toInt() const { return std::atoi(s.c_str()); } +}; +struct TestSerial { + void begin(int) {} + template void print(T...) {} + template void println(T...) {} + template void printf(T...) {} + int available() { return 0; } + int read() { return 0; } +}; +inline TestSerial Serial; diff --git a/tests/stubs/NimBLEDevice.h b/tests/stubs/NimBLEDevice.h new file mode 100644 index 0000000..ea1a98c --- /dev/null +++ b/tests/stubs/NimBLEDevice.h @@ -0,0 +1,173 @@ +#pragma once +// Behavioral doubles only: the real Arduino build validates upstream API compatibility. +#include +#include +#include +#include +#include +constexpr int ESP_PWR_LVL_P9=9; +namespace NIMBLE_PROPERTY { constexpr int READ=1, WRITE=2, NOTIFY=4, INDICATE=8; } +class NimBLEUUID { + std::string value; +public: + NimBLEUUID(uint16_t v): value(std::to_string(v)) {} + NimBLEUUID(const char* v): value(v) {} + std::string toString() const {return value;} +}; +class NimBLEAddress { + std::string value; +public: + NimBLEAddress(const char* v=""): value(v) {} + std::string toString() const {return value;} +}; +class NimBLEConnInfo { + uint16_t handle; +public: + explicit NimBLEConnInfo(uint16_t h=1): handle(h) {} + uint16_t getConnHandle() const { return handle; } + NimBLEAddress getAddress() const { return NimBLEAddress("aa:bb"); } +}; +class NimBLECharacteristic; +struct NimBLECharacteristicCallbacks { + virtual ~NimBLECharacteristicCallbacks()=default; + virtual void onWrite(NimBLECharacteristic*,NimBLEConnInfo&) {} + virtual void onSubscribe(NimBLECharacteristic*,NimBLEConnInfo&,uint16_t) {} + virtual void onStatus(NimBLECharacteristic*,NimBLEConnInfo&,int) {} +}; +struct Delivery { std::vector bytes; uint16_t handle; }; +class NimBLECharacteristic { + std::string value; +public: + std::vector notifications, indications; + NimBLECharacteristicCallbacks* callbacks=nullptr; + std::vector subscribers{11,22}; + void setCallbacks(NimBLECharacteristicCallbacks* c) {callbacks=c;} + void setValue(const uint8_t* d,size_t n) {value.assign(reinterpret_cast(d),n);} + void setValue(const char* d) {value=d;} + std::string getValue() const {return value;} + NimBLEUUID getUUID() const {return NimBLEUUID(uint16_t{0});} + bool notify(const uint8_t* d,size_t n) { + for(auto h:subscribers) notifications.push_back({{d,d+n},h}); + return true; + } + bool indicate(const uint8_t* d,size_t n,uint16_t h=0xffff) { + if(h==0xffff) {for(auto s:subscribers) indications.push_back({{d,d+n},s});} + else indications.push_back({{d,d+n},h}); + return true; + } +}; +struct NimBLEService { + std::vector> characteristics; + NimBLECharacteristic* createCharacteristic(const NimBLEUUID&,int) { + characteristics.emplace_back(new NimBLECharacteristic); return characteristics.back().get(); + } +}; +class NimBLEServer; +struct NimBLEServerCallbacks { + virtual ~NimBLEServerCallbacks()=default; + virtual void onConnect(NimBLEServer*,NimBLEConnInfo&) {} + virtual void onDisconnect(NimBLEServer*,NimBLEConnInfo&,int) {} + virtual void onMTUChange(uint16_t,NimBLEConnInfo&) {} +}; +struct NimBLEServer { + int connectedCount=0; + std::vector> services; + void setCallbacks(NimBLEServerCallbacks*) {} + void advertiseOnDisconnect(bool) {} + void start() {} + int getConnectedCount() {return connectedCount;} + NimBLEService* createService(const NimBLEUUID&) {services.emplace_back(new NimBLEService);return services.back().get();} +}; +struct NimBLEAdvertising { + bool active=false; + bool isAdvertising() const {return active;} + bool start() {active=true;return true;} + void stop() {active=false;} + void reset() {active=false;} + void enableScanResponse(bool) {} + void setName(const char*) {} + void setAppearance(int) {} + void addServiceUUID(const NimBLEUUID&) {} + void setServiceData(const NimBLEUUID&,const uint8_t*,size_t) {} + void setMinInterval(int) {} + void setMaxInterval(int) {} +}; +struct NimBLEAdvertisedDevice { + std::string name="Van Rysel D100"; + NimBLEAddress address{"aa:bb:cc:dd:ee:ff"}; + std::string getName() const {return name;} + NimBLEAddress getAddress() const {return address;} + int getRSSI() const {return -40;} + bool haveServiceUUID() const {return true;} + bool isAdvertisingService(const NimBLEUUID&) const {return true;} + NimBLEUUID getServiceUUID() const {return NimBLEUUID(0x1826);} +}; +struct NimBLEScanResults {int getCount() const {return 0;}}; +struct NimBLEScanCallbacks { + virtual ~NimBLEScanCallbacks()=default; + virtual void onResult(const NimBLEAdvertisedDevice*) {} + virtual void onScanEnd(const NimBLEScanResults&,int) {} +}; +struct NimBLEScan { + NimBLEScanCallbacks* callbacks=nullptr; + unsigned starts=0,clears=0; + uint32_t duration=0; + bool active=false; + void setScanCallbacks(NimBLEScanCallbacks* c,bool) {callbacks=c;} + void setActiveScan(bool) {} + void setInterval(int) {} + void setWindow(int) {} + void clearResults() {++clears;} + bool start(uint32_t ms,bool,bool) {++starts;duration=ms;active=true;return true;} + void stop() {active=false;} +}; +class NimBLERemoteCharacteristic; +using RemoteCallback=void(*)(NimBLERemoteCharacteristic*,uint8_t*,size_t,bool); +struct NimBLERemoteCharacteristic { + bool writable=true,notifiable=true,indicatable=true,subscribeOk=true,writeOk=true; + RemoteCallback callback=nullptr; + std::vector> writes; + bool canWrite() const {return writable;} + bool canNotify() const {return notifiable;} + bool canIndicate() const {return indicatable;} + bool subscribe(bool,RemoteCallback c) {callback=c;return subscribeOk;} + bool writeValue(uint8_t* d,size_t n,bool) {writes.push_back({d,d+n});return writeOk;} +}; +struct NimBLERemoteService { + NimBLERemoteCharacteristic data,control; + NimBLERemoteCharacteristic* getCharacteristic(const NimBLEUUID& id) {return id.toString()=="2AD2"?&data:&control;} +}; +struct ble_gap_upd_params {}; +class NimBLEClient; +struct NimBLEClientCallbacks { + virtual ~NimBLEClientCallbacks()=default; + virtual void onConnect(NimBLEClient*) {} + virtual void onDisconnect(NimBLEClient*,int) {} + virtual void onMTUChange(NimBLEClient*,uint16_t) {} + virtual bool onConnParamsUpdateRequest(NimBLEClient*,const ble_gap_upd_params*) {return true;} +}; +struct NimBLEClient { + NimBLEClientCallbacks* callbacks=nullptr; + NimBLERemoteService service; + bool connectOk=true; + void setClientCallbacks(NimBLEClientCallbacks* c,bool) {callbacks=c;} + void setConnectionParams(int,int,int,int) {} + void setConnectTimeout(int) {} + bool connect(NimBLEAddress) {return connectOk;} + NimBLERemoteService* getService(const NimBLEUUID&) {return &service;} + void disconnect() {if(callbacks) callbacks->onDisconnect(this,0);} +}; +struct NimBLEDevice { + inline static NimBLEAdvertising advertising; + inline static NimBLEScan scan; + inline static NimBLEServer server; + inline static unsigned initCalls=0; + static void init(const char*) {++initCalls;} + static void setPower(int) {} + static void setMTU(int) {} + static NimBLEAdvertising* getAdvertising() {return &advertising;} + static NimBLEScan* getScan() {return &scan;} + static NimBLEServer* createServer() {return &server;} + static NimBLEClient* createClient() {return new NimBLEClient;} + static void deleteClient(NimBLEClient* c) {delete c;} +}; diff --git a/tests/stubs/freertos/FreeRTOS.h b/tests/stubs/freertos/FreeRTOS.h new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/tests/stubs/freertos/FreeRTOS.h @@ -0,0 +1 @@ +#pragma once diff --git a/tests/stubs/freertos/portmacro.h b/tests/stubs/freertos/portmacro.h new file mode 100644 index 0000000..74a1537 --- /dev/null +++ b/tests/stubs/freertos/portmacro.h @@ -0,0 +1,5 @@ +#pragma once +using portMUX_TYPE = int; +#define portMUX_INITIALIZER_UNLOCKED 0 +#define portENTER_CRITICAL(x) ((void)(x)) +#define portEXIT_CRITICAL(x) ((void)(x)) diff --git a/tests/stubs/soc/rtc_cntl_reg.h b/tests/stubs/soc/rtc_cntl_reg.h new file mode 100644 index 0000000..7b6a0bb --- /dev/null +++ b/tests/stubs/soc/rtc_cntl_reg.h @@ -0,0 +1,2 @@ +#pragma once +#define RTC_CNTL_BROWN_OUT_REG 0 diff --git a/tests/stubs/soc/soc.h b/tests/stubs/soc/soc.h new file mode 100644 index 0000000..1d9a71b --- /dev/null +++ b/tests/stubs/soc/soc.h @@ -0,0 +1,2 @@ +#pragma once +#define WRITE_PERI_REG(a,b) ((void)0)