diff --git a/AGENTS.md b/AGENTS.md index 87a62a16..fd8d6e99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -589,6 +589,7 @@ Changing BLE server characteristics: - `spinDownFlag` is a state machine trigger, not just a bool: `1` means home/startup-ish, `2+` means full spindown/homing. - `externalControl` bypasses normal target calculation but final state can still be affected by sync/clamping code. - Firmware OTA paths validate the incoming `esp_image_header_t` chip ID before starting flash writes; filesystem images are intentionally exempt from application-image validation. +- BLE firmware OTA uses a length-aware versioned protocol documented in `BLEFirmwareUpdateProtocol.md`. It accepts variable data chunk sizes through writes with or without response, incrementally verifies CRC-32, and reports phase/error/byte-count status only through the firmware service control characteristic. Apps must wait for `Updating` before sending data; `Preparing` releases sensor links and erases the inactive partition outside the NimBLE callback. The server requests an ATT MTU exchange on connection and retries at OTA START, but transfers remain valid at MTU 23. Failed, aborted, disconnected, or 30-second-stalled transfers abort the inactive OTA handle and schedule a reboot; the boot partition is not changed until verification succeeds. - Stepper UART initialization drives TX high for 20 ms before starting hardware UART. TMC connection checks track `IFCNT` across calls to confirm intervening writes were accepted; a failed UART test or unchanged counter restarts UART with one idle-high recovery pulse and aborts the requested setup/power update if that retry fails. Initial setup also checks `OTP_IHOLD`; if its two-bit field is unprogrammed, firmware irreversibly programs byte 2/bit 5 for the 9% standalone hold-current default, while incompatible existing OTP values are never modified. - Many BLE and motor changes cannot be fully validated without hardware. diff --git a/BLEFirmwareUpdateProtocol.md b/BLEFirmwareUpdateProtocol.md new file mode 100644 index 00000000..161ac56f --- /dev/null +++ b/BLEFirmwareUpdateProtocol.md @@ -0,0 +1,86 @@ +# BLE Firmware Update Protocol + +SmartSpin2k exposes the firmware update service `4fafc201-1fb5-459e-8fcc-c5c9c331914b` with two characteristics: + +- Control/status (`62ec0272-3ec5-11eb-b378-0242ac130003`): read, write, notify. +- Firmware data (`62ec0272-3ec5-11eb-b378-0242ac130005`): write with or without response. + +All multi-byte integers are unsigned little-endian values. Protocol packets are at most 12 bytes so control and status traffic fits the minimum ATT MTU of 23. + +The firmware advertises a local ATT MTU of 515 and makes a best-effort MTU exchange request on connection and again when START is accepted. The negotiated value is still limited by the peer and operating system; transfer remains functional with an MTU of 23. + +## Update sequence + +1. Subscribe to control/status notifications and read its current value. A 12-byte status packet with protocol version `1` indicates support for this protocol. +2. Calculate the firmware file's standard CRC-32 and write START to the control characteristic. +3. START first reports `Preparing` while sensor connections are released and the inactive partition is erased. Wait for `Updating`, then write the firmware bytes in order to the data characteristic. Any non-empty chunk size is accepted. Use `min(512, negotiated ATT MTU - 3)` bytes; this is 20 bytes when Windows reports MTU 23. Write-without-response is the preferred fast path; writes with response remain available as a conservative fallback. +4. After exactly the declared image length has been written, write FINISH to the control characteristic. +5. Follow status notifications through `Verifying` and `Rebooting`. A disconnect after `Rebooting` is expected. + +The firmware buffers only the ESP image header. It calculates CRC-32 incrementally and writes incoming data directly to the inactive OTA partition. + +## Control commands + +| Command | Value | Packet | +| --- | ---: | --- | +| START | `0x01` | `[command, version, image_size:u32, crc32:u32]` (10 bytes) | +| FINISH | `0x02` | `[command]` | +| ABORT | `0x03` | `[command]` | +| QUERY | `0x04` | `[command]` | + +START requires protocol version `1`. `crc32` is the standard reflected CRC-32 used by common ZIP/zlib implementations (polynomial `0xedb88320`; the `123456789` test vector produces `0xcbf43926`). + +## Status packet + +Every status read or notification is 12 bytes: + +| Offset | Size | Meaning | +| ---: | ---: | --- | +| 0 | 1 | Protocol version (`1`) | +| 1 | 1 | State | +| 2 | 1 | Error code (`0` when no error) | +| 3 | 1 | Capability flags | +| 4 | 4 | Bytes received | +| 8 | 4 | Declared image size | + +Capability flags are `0x01` length-aware EOF, `0x02` CRC-32 verification, `0x04` variable data chunks, and `0x08` write-without-response support. Version 1 reports all four (`0x0f`). Progress notifications are throttled to approximately every 64 KiB; the app can show immediate progress from queued writes and use the firmware's received-byte count as confirmation. + +### States + +| Value | State | Meaning | +| ---: | --- | --- | +| `0x00` | Waiting | Ready for a START command | +| `0x01` | Preparing | Metadata accepted; sensor links are being released and the inactive OTA partition is being erased | +| `0x02` | Updating | OTA partition is ready; firmware data writes may begin | +| `0x03` | Flashing | Firmware data is being written; byte counters report received progress | +| `0x04` | Verifying | FINISH received; length, CRC, and ESP image validation are running | +| `0x05` | Rebooting | New boot partition selected; disconnect is expected | +| `0xff` | Error | Update stopped; inspect the error code | + +### Errors + +| Value | Error | +| ---: | --- | +| `0x00` | None | +| `0x01` | Invalid command | +| `0x02` | Unsupported protocol version | +| `0x03` | Invalid START packet | +| `0x04` | Another update is active | +| `0x05` | Invalid image size | +| `0x06` | No OTA partition available | +| `0x07` | Command/data came from the wrong connection | +| `0x08` | Update has not been started | +| `0x09` | Empty data write | +| `0x0a` | More bytes received than declared | +| `0x0b` | Invalid image header or wrong ESP chip | +| `0x0c` | OTA begin failed | +| `0x0d` | Flash write failed | +| `0x0e` | FINISH received before the declared byte count | +| `0x0f` | CRC-32 mismatch | +| `0x10` | ESP image verification failed | +| `0x11` | New boot partition could not be selected | +| `0x12` | No firmware data received for 30 seconds | +| `0x13` | Update aborted by the client | +| `0x14` | Update connection was lost | + +Any transfer failure, 30-second data timeout, ABORT, or update-connection loss aborts the inactive OTA write and schedules a reboot. The boot partition is changed only after complete image verification, so these failures continue booting the existing known-good firmware. diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cad1c3..12b66323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added new BLE firmware update protocol. ### Changed diff --git a/include/BLE_Common.h b/include/BLE_Common.h index 34cbe012..cc1f1f6c 100644 --- a/include/BLE_Common.h +++ b/include/BLE_Common.h @@ -81,7 +81,7 @@ const BLEServiceInfo* getDeviceServiceInfo(const NimBLEAdvertisedDevice* adverti class MyServerCallbacks : public NimBLEServerCallbacks { public: void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo); - void onDisconnect(NimBLEServer* pServer); + void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) override; void onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo); bool onConnParamsUpdateRequest(uint16_t handle, const ble_gap_upd_params* params); }; @@ -121,6 +121,9 @@ void calculateInstPwrFromHR(); // BLE FIRMWARE UPDATER void BLEFirmwareSetup(NimBLEServer* pServer); +void BLEFirmwareUpdateLoop(); +void BLEFirmwareUpdateOnDisconnect(uint16_t connectionHandle); +void BLERequestMtuExchange(uint16_t connectionHandle); // *****************************Client***************************** diff --git a/include/BLE_Firmware_Update.h b/include/BLE_Firmware_Update.h new file mode 100644 index 00000000..40985198 --- /dev/null +++ b/include/BLE_Firmware_Update.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#pragma once + +#include +#include +#include + +namespace BleFirmwareUpdate { + +constexpr uint8_t PROTOCOL_VERSION = 1; + +enum class Command : uint8_t { + Start = 0x01, + Finish = 0x02, + Abort = 0x03, + Query = 0x04, +}; + +enum class State : uint8_t { + Waiting = 0x00, + Preparing = 0x01, + Updating = 0x02, + Flashing = 0x03, + Verifying = 0x04, + Rebooting = 0x05, + Error = 0xff, +}; + +enum class Error : uint8_t { + None = 0x00, + InvalidCommand = 0x01, + UnsupportedVersion = 0x02, + InvalidStartPacket = 0x03, + Busy = 0x04, + InvalidImageSize = 0x05, + NoUpdatePartition = 0x06, + WrongConnection = 0x07, + NotStarted = 0x08, + EmptyData = 0x09, + TooMuchData = 0x0a, + InvalidImageHeader = 0x0b, + OtaBeginFailed = 0x0c, + OtaWriteFailed = 0x0d, + IncompleteImage = 0x0e, + ChecksumMismatch = 0x0f, + ImageVerifyFailed = 0x10, + SetBootFailed = 0x11, + TransferTimedOut = 0x12, + Aborted = 0x13, + ConnectionLost = 0x14, +}; + +constexpr uint8_t CAP_LENGTH_AWARE = 1U << 0; +constexpr uint8_t CAP_CRC32 = 1U << 1; +constexpr uint8_t CAP_VARIABLE_CHUNK = 1U << 2; +constexpr uint8_t CAP_WRITE_NO_RSP = 1U << 3; +constexpr uint8_t CAPABILITIES = CAP_LENGTH_AWARE | CAP_CRC32 | CAP_VARIABLE_CHUNK | CAP_WRITE_NO_RSP; + +constexpr size_t START_PACKET_SIZE = 10; +constexpr size_t STATUS_PACKET_SIZE = 12; +constexpr size_t MAX_DATA_CHUNK_SIZE = 512; +constexpr uint32_t TRANSFER_TIMEOUT_MS = 30000; + +struct StartRequest { + uint32_t imageSize; + uint32_t imageCrc32; +}; + +inline uint32_t readUint32LE(const uint8_t* data) { + return static_cast(data[0]) | (static_cast(data[1]) << 8) | (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); +} + +inline void writeUint32LE(uint8_t* data, uint32_t value) { + data[0] = static_cast(value); + data[1] = static_cast(value >> 8); + data[2] = static_cast(value >> 16); + data[3] = static_cast(value >> 24); +} + +inline bool parseStartRequest(const uint8_t* data, size_t length, StartRequest& request) { + if (data == nullptr || length != START_PACKET_SIZE || data[0] != static_cast(Command::Start) || data[1] != PROTOCOL_VERSION) return false; + request.imageSize = readUint32LE(data + 2); + request.imageCrc32 = readUint32LE(data + 6); + return true; +} + +inline bool hasTransferTimedOut(uint32_t nowMs, uint32_t lastActivityMs) { + return nowMs - lastActivityMs >= TRANSFER_TIMEOUT_MS; +} + +inline std::array makeStatusPacket(State state, Error error, uint32_t receivedBytes, uint32_t imageSize) { + std::array packet{}; + packet[0] = PROTOCOL_VERSION; + packet[1] = static_cast(state); + packet[2] = static_cast(error); + packet[3] = CAPABILITIES; + writeUint32LE(packet.data() + 4, receivedBytes); + writeUint32LE(packet.data() + 8, imageSize); + return packet; +} + +} // namespace BleFirmwareUpdate diff --git a/src/BLE_Firmware_Update.cpp b/src/BLE_Firmware_Update.cpp index b8a7b6a2..31b1f9bf 100644 --- a/src/BLE_Firmware_Update.cpp +++ b/src/BLE_Firmware_Update.cpp @@ -5,204 +5,513 @@ * SPDX-License-Identifier: GPL-2.0-only */ -#include "Main.h" -#include "SS2KLog.h" -#include "BLE_Common.h" -#include "FirmwareImageValidation.h" +#include "BLE_Firmware_Update.h" -#include -#include +#include +#include + +#include #include #include -#include +#include +#include +#include +#include +#include + +#include "FirmwareImageValidation.h" +#include "Main.h" +#include "SS2KLog.h" #define BLE_OTA_LOG_TAG "BLE_OTA" -/*------------------------------------------------------------------------------ - BLE instances & variables - ----------------------------------------------------------------------------*/ -BLECharacteristic *pTxCharacteristic; -BLECharacteristic *pOtaCharacteristic; - -bool deviceConnected = false; -bool oldDeviceConnected = false; - -String fileExtension = ""; - -/*------------------------------------------------------------------------------ - OTA instances & variables - ----------------------------------------------------------------------------*/ -static esp_ota_handle_t otaHandler = 0; -static const esp_partition_t *update_partition = NULL; - -int bufferCount = 0; -bool downloadFlag = false; - -/*------------------------------------------------------------------------------ - BLE Peripheral callback(s) - ----------------------------------------------------------------------------*/ - -class otaCallback : public BLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override { - std::string rxData = pCharacteristic->getValue(); - bufferCount++; - - if (!downloadFlag) { - const FirmwareImageHeaderValidation validation = - validateFirmwareImageHeader(reinterpret_cast(rxData.data()), rxData.length()); - if (validation.result != FirmwareImageHeaderResult::Valid) { - SS2K_LOGE(BLE_OTA_LOG_TAG, "Rejected firmware image: %s (expected chip 0x%04x, image chip 0x%04x)", - firmwareImageHeaderResultName(validation.result), CONFIG_IDF_FIRMWARE_CHIP_ID, static_cast(validation.imageChipId)); - ss2k->isUpdating = false; - downloadFlag = false; - bufferCount = 0; - const uint8_t failureStatus = 0x04; - pTxCharacteristic->notify(&failureStatus, sizeof(failureStatus), connInfo.getConnHandle()); - return; - } +namespace { - ss2k->isUpdating = true; - //----------------------------------------------- - // First BLE bytes have arrived - //----------------------------------------------- - // update the connection interval so that it provides enough time for the long writes - - Serial.printf("1. BeginOTA"); - BLEDevice::getServer()->updateConnParams(connInfo.getConnHandle(), 12, 12, 0, 1000); - const esp_partition_t *configured = esp_ota_get_boot_partition(); - const esp_partition_t *running = esp_ota_get_running_partition(); - - if (configured != running) { - SS2K_LOG(BLE_OTA_LOG_TAG, "ERROR: Configured OTA boot partition at offset 0x%08x, but running from offset 0x%08x", configured->address, running->address); - SS2K_LOG(BLE_OTA_LOG_TAG, "(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)"); - downloadFlag = false; - esp_ota_end(otaHandler); - } else { - SS2K_LOG(BLE_OTA_LOG_TAG, "2. Running partition type %d subtype %d (offset 0x%08x) \n", running->type, running->subtype, running->address); - } +constexpr uint32_t PROGRESS_NOTIFICATION_INTERVAL = 64U * 1024U; +constexpr uint32_t PROGRESS_LOG_INTERVAL = 512U * 1024U; +constexpr uint32_t OTA_PREPARE_DELAY_MS = 300; - update_partition = esp_ota_get_next_update_partition(NULL); - if (update_partition == NULL) { - SS2K_LOG(BLE_OTA_LOG_TAG, "ERROR: No valid partition found"); - downloadFlag = false; - ss2k->rebootFlag = true; - return; - } +NimBLECharacteristic* statusCharacteristic = nullptr; - SS2K_LOG(BLE_OTA_LOG_TAG, "3. Writing to partition subtype %d at offset 0x%x \n", update_partition->subtype, update_partition->address); - - //------------------------------------------------------------------------------------------ - // esp_ota_begin can take a while to complete as it erase the flash partition (3-5 seconds) - // so make sure there's no timeout on the client side (iOS) that triggers before that. - //------------------------------------------------------------------------------------------ - esp_task_wdt_config_t wdt_config = {.timeout_ms = 20000, // 20 seconds - .idle_core_mask = 0, - .trigger_panic = false}; - esp_task_wdt_init(&wdt_config); - - // if (BLECommunicationTask != NULL) { - // SS2K_LOG(MAIN_LOG_TAG, "Stop BLE Tasks"); - // if (NimBLEDevice::getScan()->isScanning()) { - // NimBLEDevice::getScan()->stop(); - // } - // vTaskDelete(BLECommunicationTask); - // BLECommunicationTask = NULL; - // } - // if (BLEClientTask != NULL) { - // vTaskDelete(BLEClientTask); - // BLEClientTask = NULL; - // } - - // vTaskDelay(5); - - if (esp_ota_begin(update_partition, OTA_SIZE_UNKNOWN, &otaHandler) != ESP_OK) { - downloadFlag = false; - ss2k->isUpdating = false; - SS2K_LOG(BLE_OTA_LOG_TAG, "OTA begin failed"); - return; - } - downloadFlag = true; - } +struct TransferContext { + esp_ota_handle_t otaHandle = 0; + const esp_partition_t* updatePartition = nullptr; + uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE; + uint32_t imageSize = 0; + uint32_t expectedCrc32 = 0; + uint32_t receivedBytes = 0; + uint32_t runningCrc32 = 0; + uint32_t nextProgressNotification = PROGRESS_NOTIFICATION_INTERVAL; + uint32_t nextProgressLog = PROGRESS_LOG_INTERVAL; + uint32_t startedMs = 0; + uint32_t lastActivityMs = 0; + size_t headerBytes = 0; + uint8_t header[sizeof(esp_image_header_t)]{}; + BleFirmwareUpdate::State state = BleFirmwareUpdate::State::Waiting; + BleFirmwareUpdate::Error error = BleFirmwareUpdate::Error::None; + bool active = false; + bool otaBegun = false; + bool headerValidated = false; +}; - if (bufferCount >= 1 || rxData.length() > 0) { - if (esp_ota_write(otaHandler, (uint8_t *)rxData.c_str(), rxData.length()) != ESP_OK) { - SS2K_LOG(BLE_OTA_LOG_TAG, "Error: write to flash failed"); - downloadFlag = false; - pTxCharacteristic->notify(0x04, 1); - ss2k->rebootFlag = true; - return; - } else { - bufferCount = 1; - // Serial.printf("%d bytes", rxData.length()); - // Notify the iOS app so next batch can be sent - // Serial.printf("."); - pTxCharacteristic->notify(0x02, sizeof(uint8_t)); - } +TransferContext transfer; +StaticSemaphore_t transferMutexStorage; +SemaphoreHandle_t transferMutex = nullptr; + +const char* stateName(BleFirmwareUpdate::State state) { + switch (state) { + case BleFirmwareUpdate::State::Waiting: + return "Waiting"; + case BleFirmwareUpdate::State::Preparing: + return "Preparing"; + case BleFirmwareUpdate::State::Updating: + return "Updating"; + case BleFirmwareUpdate::State::Flashing: + return "Flashing"; + case BleFirmwareUpdate::State::Verifying: + return "Verifying"; + case BleFirmwareUpdate::State::Rebooting: + return "Rebooting"; + case BleFirmwareUpdate::State::Error: + return "Error"; + } + return "Unknown"; +} + +const char* errorName(BleFirmwareUpdate::Error error) { + switch (error) { + case BleFirmwareUpdate::Error::None: + return "None"; + case BleFirmwareUpdate::Error::InvalidCommand: + return "InvalidCommand"; + case BleFirmwareUpdate::Error::UnsupportedVersion: + return "UnsupportedVersion"; + case BleFirmwareUpdate::Error::InvalidStartPacket: + return "InvalidStartPacket"; + case BleFirmwareUpdate::Error::Busy: + return "Busy"; + case BleFirmwareUpdate::Error::InvalidImageSize: + return "InvalidImageSize"; + case BleFirmwareUpdate::Error::NoUpdatePartition: + return "NoUpdatePartition"; + case BleFirmwareUpdate::Error::WrongConnection: + return "WrongConnection"; + case BleFirmwareUpdate::Error::NotStarted: + return "NotStarted"; + case BleFirmwareUpdate::Error::EmptyData: + return "EmptyData"; + case BleFirmwareUpdate::Error::TooMuchData: + return "TooMuchData"; + case BleFirmwareUpdate::Error::InvalidImageHeader: + return "InvalidImageHeader"; + case BleFirmwareUpdate::Error::OtaBeginFailed: + return "OtaBeginFailed"; + case BleFirmwareUpdate::Error::OtaWriteFailed: + return "OtaWriteFailed"; + case BleFirmwareUpdate::Error::IncompleteImage: + return "IncompleteImage"; + case BleFirmwareUpdate::Error::ChecksumMismatch: + return "ChecksumMismatch"; + case BleFirmwareUpdate::Error::ImageVerifyFailed: + return "ImageVerifyFailed"; + case BleFirmwareUpdate::Error::SetBootFailed: + return "SetBootFailed"; + case BleFirmwareUpdate::Error::TransferTimedOut: + return "TransferTimedOut"; + case BleFirmwareUpdate::Error::Aborted: + return "Aborted"; + case BleFirmwareUpdate::Error::ConnectionLost: + return "ConnectionLost"; + } + return "Unknown"; +} + +class TransferGuard { + public: + explicit TransferGuard(TickType_t waitTicks) : locked(transferMutex != nullptr && xSemaphoreTake(transferMutex, waitTicks) == pdTRUE) {} + ~TransferGuard() { + if (locked) xSemaphoreGive(transferMutex); + } + explicit operator bool() const { return locked; } + + private: + bool locked; +}; + +void publishStatus(uint16_t connectionHandle = BLE_HS_CONN_HANDLE_NONE, bool notify = true) { + const auto packet = BleFirmwareUpdate::makeStatusPacket(transfer.state, transfer.error, transfer.receivedBytes, transfer.imageSize); + statusCharacteristic->setValue(packet.data(), packet.size()); + if (notify && connectionHandle != BLE_HS_CONN_HANDLE_NONE) { + statusCharacteristic->notify(packet.data(), packet.size(), connectionHandle); + } +} + +void publishTransientError(uint16_t connectionHandle, BleFirmwareUpdate::Error error) { + SS2K_LOGW(BLE_OTA_LOG_TAG, "Rejected OTA request: conn=%u state=%s error=%s(%u) bytes=%lu/%lu", connectionHandle, stateName(transfer.state), errorName(error), + static_cast(error), static_cast(transfer.receivedBytes), static_cast(transfer.imageSize)); + const auto packet = BleFirmwareUpdate::makeStatusPacket(BleFirmwareUpdate::State::Error, error, transfer.receivedBytes, transfer.imageSize); + statusCharacteristic->notify(packet.data(), packet.size(), connectionHandle); + // A control write temporarily replaces the characteristic's readable value + // with the command bytes. Restore the persistent transfer status after + // sending this connection-specific error. + publishStatus(BLE_HS_CONN_HANDLE_NONE, false); +} + +void abortOtaHandle() { + if (transfer.otaBegun) { + esp_ota_abort(transfer.otaHandle); + transfer.otaBegun = false; + transfer.otaHandle = 0; + } +} + +void resetTransfer(BleFirmwareUpdate::State state = BleFirmwareUpdate::State::Waiting) { + abortOtaHandle(); + transfer = TransferContext{}; + transfer.state = state; + if (ss2k != nullptr) ss2k->isUpdating = false; +} + +void failTransfer(BleFirmwareUpdate::Error error, const char* message, esp_err_t espError = ESP_OK, bool notify = true) { + const BleFirmwareUpdate::State failedState = transfer.state; + const uint32_t elapsedMs = transfer.startedMs == 0 ? 0 : millis() - transfer.startedMs; + if (espError == ESP_OK) { + SS2K_LOGE(BLE_OTA_LOG_TAG, "FAILED: conn=%u state=%s error=%s(%u) bytes=%lu/%lu elapsed=%lums detail=%s", transfer.connectionHandle, stateName(failedState), + errorName(error), static_cast(error), static_cast(transfer.receivedBytes), static_cast(transfer.imageSize), + static_cast(elapsedMs), message); + } else { + SS2K_LOGE(BLE_OTA_LOG_TAG, "FAILED: conn=%u state=%s error=%s(%u) bytes=%lu/%lu elapsed=%lums detail=%s esp=%s(%d)", transfer.connectionHandle, + stateName(failedState), errorName(error), static_cast(error), static_cast(transfer.receivedBytes), + static_cast(transfer.imageSize), static_cast(elapsedMs), message, esp_err_to_name(espError), espError); + } + + abortOtaHandle(); + transfer.active = false; + transfer.state = BleFirmwareUpdate::State::Error; + transfer.error = error; + if (ss2k != nullptr) ss2k->isUpdating = false; + publishStatus(notify ? transfer.connectionHandle : BLE_HS_CONN_HANDLE_NONE, notify); + if (ss2k != nullptr) ss2k->rebootFlag = true; +} + +bool ownsTransfer(uint16_t connectionHandle) { + return transfer.active && transfer.connectionHandle == connectionHandle; +} + +bool validateAndWriteHeader() { + const FirmwareImageHeaderValidation validation = validateFirmwareImageHeader(transfer.header, transfer.headerBytes); + if (validation.result != FirmwareImageHeaderResult::Valid) { + failTransfer(BleFirmwareUpdate::Error::InvalidImageHeader, firmwareImageHeaderResultName(validation.result)); + return false; + } + + SS2K_LOG(BLE_OTA_LOG_TAG, "Image header valid: conn=%u chip=0x%04x expected=0x%04x header=%u bytes", transfer.connectionHandle, + static_cast(validation.imageChipId), static_cast(CONFIG_IDF_FIRMWARE_CHIP_ID), static_cast(transfer.headerBytes)); + const esp_err_t writeResult = esp_ota_write(transfer.otaHandle, transfer.header, transfer.headerBytes); + if (writeResult != ESP_OK) { + failTransfer(BleFirmwareUpdate::Error::OtaWriteFailed, "Unable to write firmware image header", writeResult); + return false; + } + transfer.headerValidated = true; + return true; +} + +bool prepareOtaPartition() { + const uint32_t prepareStartedMs = millis(); + SS2K_LOG(BLE_OTA_LOG_TAG, "Preparing flash: conn=%u partition=%s size=%lu", transfer.connectionHandle, transfer.updatePartition->label, + static_cast(transfer.imageSize)); + const esp_err_t result = esp_ota_begin(transfer.updatePartition, transfer.imageSize, &transfer.otaHandle); + if (result != ESP_OK) { + failTransfer(BleFirmwareUpdate::Error::OtaBeginFailed, "Unable to begin BLE firmware update", result); + return false; + } + + transfer.otaBegun = true; + transfer.state = BleFirmwareUpdate::State::Updating; + transfer.error = BleFirmwareUpdate::Error::None; + transfer.lastActivityMs = millis(); + SS2K_LOG(BLE_OTA_LOG_TAG, "Flash ready: conn=%u partition=%s prepare=%lums; accepting firmware data", transfer.connectionHandle, transfer.updatePartition->label, + static_cast(millis() - prepareStartedMs)); + // OTA normally pauses log draining. Flush preparation diagnostics while the + // app is still waiting for Updating, before firmware data starts competing + // for BLE airtime. + logHandler.writeLogs(); + publishStatus(transfer.connectionHandle); + return true; +} - //------------------------------------------------------------------- - // check if this was the last data chunk? (normally the last chunk is - // smaller than the maximum MTU size). For improvement: let iOS app send byte - // length instead of hardcoding "510" - //------------------------------------------------------------------- - if (rxData.length() < 512) // TODO Asumes at least 511 data bytes (@BLE 4.2). - { - SS2K_LOG(BLE_OTA_LOG_TAG, "4. Final byte arrived"); - //----------------------------------------------------------------- - // Final chunk arrived. Now check that - // the length of total file is correct - //----------------------------------------------------------------- - if (esp_ota_end(otaHandler) != ESP_OK) { - SS2K_LOG(BLE_OTA_LOG_TAG, "OTA end failed "); - downloadFlag = false; - pTxCharacteristic->notify(0x04, sizeof(uint8_t)); - ss2k->rebootFlag = true; - return; +bool writeFirmwareBytes(const uint8_t* data, size_t length) { + if (length == 0) return true; + const esp_err_t result = esp_ota_write(transfer.otaHandle, data, length); + if (result != ESP_OK) { + failTransfer(BleFirmwareUpdate::Error::OtaWriteFailed, "Unable to write firmware data", result); + return false; + } + return true; +} + +void startTransfer(const uint8_t* data, size_t length, NimBLEConnInfo& connInfo) { + BleFirmwareUpdate::StartRequest request{}; + if (length != BleFirmwareUpdate::START_PACKET_SIZE) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidStartPacket); + return; + } + if (data[1] != BleFirmwareUpdate::PROTOCOL_VERSION) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::UnsupportedVersion); + return; + } + if (!BleFirmwareUpdate::parseStartRequest(data, length, request)) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidStartPacket); + return; + } + if (transfer.active || (ss2k != nullptr && ss2k->rebootFlag)) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::Busy); + return; + } + + resetTransfer(); + transfer.updatePartition = esp_ota_get_next_update_partition(nullptr); + if (transfer.updatePartition == nullptr) { + transfer.connectionHandle = connInfo.getConnHandle(); + failTransfer(BleFirmwareUpdate::Error::NoUpdatePartition, "No OTA update partition is available"); + return; + } + if (request.imageSize < sizeof(esp_image_header_t) || request.imageSize > transfer.updatePartition->size) { + transfer.connectionHandle = connInfo.getConnHandle(); + transfer.imageSize = request.imageSize; + failTransfer(BleFirmwareUpdate::Error::InvalidImageSize, "Firmware image size does not fit the OTA partition"); + return; + } + + transfer.connectionHandle = connInfo.getConnHandle(); + transfer.imageSize = request.imageSize; + transfer.expectedCrc32 = request.imageCrc32; + transfer.active = true; + transfer.state = BleFirmwareUpdate::State::Preparing; + transfer.error = BleFirmwareUpdate::Error::None; + transfer.startedMs = millis(); + transfer.lastActivityMs = millis(); + if (ss2k != nullptr) ss2k->isUpdating = true; + + // Retry the best-effort exchange here in case the request made from the + // connection callback was too early for the peer. EALREADY is harmless. + BLERequestMtuExchange(connInfo.getConnHandle()); + NimBLEDevice::getServer()->updateConnParams(connInfo.getConnHandle(), 12, 12, 0, 1000); + SS2K_LOG(BLE_OTA_LOG_TAG, "START: conn=%u peer=%s mtu=%u size=%lu crc32=0x%08lx partition=%s", transfer.connectionHandle, + connInfo.getAddress().toString().c_str(), connInfo.getMTU(), static_cast(transfer.imageSize), static_cast(transfer.expectedCrc32), + transfer.updatePartition->label); + publishStatus(transfer.connectionHandle); +} + +void finishTransfer(uint16_t connectionHandle) { + if (!ownsTransfer(connectionHandle)) { + publishTransientError(connectionHandle, transfer.active ? BleFirmwareUpdate::Error::WrongConnection : BleFirmwareUpdate::Error::NotStarted); + return; + } + SS2K_LOG(BLE_OTA_LOG_TAG, "FINISH: conn=%u bytes=%lu/%lu crc32=0x%08lx expected=0x%08lx", connectionHandle, static_cast(transfer.receivedBytes), + static_cast(transfer.imageSize), static_cast(transfer.runningCrc32), static_cast(transfer.expectedCrc32)); + if (!transfer.otaBegun || transfer.receivedBytes != transfer.imageSize) { + failTransfer(BleFirmwareUpdate::Error::IncompleteImage, "Firmware transfer ended before the declared image size was received"); + return; + } + if (transfer.runningCrc32 != transfer.expectedCrc32) { + failTransfer(BleFirmwareUpdate::Error::ChecksumMismatch, "Firmware CRC-32 does not match the declared checksum"); + return; + } + + transfer.state = BleFirmwareUpdate::State::Verifying; + SS2K_LOG(BLE_OTA_LOG_TAG, "Verifying image: conn=%u partition=%s", connectionHandle, transfer.updatePartition->label); + publishStatus(transfer.connectionHandle); + + const esp_ota_handle_t completedHandle = transfer.otaHandle; + transfer.otaBegun = false; + transfer.otaHandle = 0; + esp_err_t result = esp_ota_end(completedHandle); + if (result != ESP_OK) { + failTransfer(BleFirmwareUpdate::Error::ImageVerifyFailed, "Firmware image verification failed", result); + return; + } + + result = esp_ota_set_boot_partition(transfer.updatePartition); + if (result != ESP_OK) { + failTransfer(BleFirmwareUpdate::Error::SetBootFailed, "Unable to select the new firmware boot partition", result); + return; + } + + transfer.active = false; + transfer.state = BleFirmwareUpdate::State::Rebooting; + transfer.error = BleFirmwareUpdate::Error::None; + publishStatus(transfer.connectionHandle); + SS2K_LOG(BLE_OTA_LOG_TAG, "SUCCESS: conn=%u bytes=%lu elapsed=%lums; boot partition=%s, rebooting", connectionHandle, + static_cast(transfer.receivedBytes), static_cast(millis() - transfer.startedMs), transfer.updatePartition->label); + if (ss2k != nullptr) ss2k->rebootFlag = true; +} + +class ControlCallback : public NimBLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { + TransferGuard guard(portMAX_DELAY); + if (!guard) return; + + const NimBLEAttValue value = characteristic->getValue(); + if (value.size() == 0) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidCommand); + return; + } + + const uint8_t* data = value.data(); + switch (static_cast(data[0])) { + case BleFirmwareUpdate::Command::Start: + startTransfer(data, value.size(), connInfo); + break; + case BleFirmwareUpdate::Command::Finish: + if (value.size() != 1) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidCommand); + } else { + finishTransfer(connInfo.getConnHandle()); } - pTxCharacteristic->notify(0x05, sizeof(uint8_t)); - //----------------------------------------------------------------- - // Clear download flag and restart the ESP32 if the firmware - // update was successful - //----------------------------------------------------------------- - SS2K_LOG(BLE_OTA_LOG_TAG, "Set Boot partition"); - if (ESP_OK == esp_ota_set_boot_partition(update_partition)) { - esp_ota_end(otaHandler); - downloadFlag = false; - SS2K_LOG(BLE_OTA_LOG_TAG, "Restarting..."); - ss2k->rebootFlag = true; - return; + break; + case BleFirmwareUpdate::Command::Abort: + if (value.size() != 1) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidCommand); + } else if (ownsTransfer(connInfo.getConnHandle())) { + failTransfer(BleFirmwareUpdate::Error::Aborted, "BLE firmware update aborted by client"); } else { - //------------------------------------------------------------ - // Something went wrong, the upload was not successful - //------------------------------------------------------------ - SS2K_LOG(BLE_OTA_LOG_TAG, "Upload Error"); - pTxCharacteristic->notify(0x04, sizeof(uint8_t)); - downloadFlag = false; - esp_ota_end(otaHandler); - ss2k->rebootFlag = true; - return; + publishTransientError(connInfo.getConnHandle(), transfer.active ? BleFirmwareUpdate::Error::WrongConnection : BleFirmwareUpdate::Error::NotStarted); } + break; + case BleFirmwareUpdate::Command::Query: + if (value.size() != 1) { + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidCommand); + } else { + publishStatus(connInfo.getConnHandle()); + } + break; + default: + publishTransientError(connInfo.getConnHandle(), BleFirmwareUpdate::Error::InvalidCommand); + break; + } + } +}; + +class DataCallback : public NimBLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic* characteristic, NimBLEConnInfo& connInfo) override { + TransferGuard guard(portMAX_DELAY); + if (!guard) return; + + const NimBLEAttValue value = characteristic->getValue(); + if (!ownsTransfer(connInfo.getConnHandle())) { + publishTransientError(connInfo.getConnHandle(), transfer.active ? BleFirmwareUpdate::Error::WrongConnection : BleFirmwareUpdate::Error::NotStarted); + return; + } + if (!transfer.otaBegun) { + publishStatus(connInfo.getConnHandle()); + return; + } + const size_t valueSize = value.size(); + if (valueSize == 0) { + failTransfer(BleFirmwareUpdate::Error::EmptyData, "Received an empty firmware data packet"); + return; + } + if (valueSize > transfer.imageSize - transfer.receivedBytes) { + failTransfer(BleFirmwareUpdate::Error::TooMuchData, "Received more firmware data than declared by START"); + return; + } + + transfer.lastActivityMs = millis(); + const bool firstDataPacket = transfer.receivedBytes == 0; + const uint8_t* data = value.data(); + transfer.runningCrc32 = esp_rom_crc32_le(transfer.runningCrc32, data, valueSize); + transfer.receivedBytes += valueSize; + if (firstDataPacket) { + SS2K_LOG(BLE_OTA_LOG_TAG, "First data: conn=%u mtu=%u chunk=%u", transfer.connectionHandle, connInfo.getMTU(), static_cast(valueSize)); + } + + size_t offset = 0; + if (!transfer.headerValidated) { + const size_t headerRemaining = sizeof(transfer.header) - transfer.headerBytes; + const size_t headerToCopy = std::min(headerRemaining, valueSize); + memcpy(transfer.header + transfer.headerBytes, data, headerToCopy); + transfer.headerBytes += headerToCopy; + offset = headerToCopy; + + if (transfer.headerBytes < sizeof(transfer.header)) return; + if (!validateAndWriteHeader()) return; + } + + if (!writeFirmwareBytes(data + offset, valueSize - offset)) return; + + if (transfer.state != BleFirmwareUpdate::State::Flashing) { + transfer.state = BleFirmwareUpdate::State::Flashing; + SS2K_LOG(BLE_OTA_LOG_TAG, "Flashing: conn=%u bytes=%lu/%lu", transfer.connectionHandle, static_cast(transfer.receivedBytes), + static_cast(transfer.imageSize)); + publishStatus(transfer.connectionHandle); + } else if (transfer.receivedBytes >= transfer.nextProgressNotification || transfer.receivedBytes == transfer.imageSize) { + while (transfer.nextProgressNotification <= transfer.receivedBytes) { + transfer.nextProgressNotification += PROGRESS_NOTIFICATION_INTERVAL; + } + publishStatus(transfer.connectionHandle); + } + + if (transfer.receivedBytes >= transfer.nextProgressLog || transfer.receivedBytes == transfer.imageSize) { + const uint32_t elapsedMs = std::max(1, millis() - transfer.startedMs); + const uint32_t percent = transfer.imageSize == 0 ? 0 : static_cast((static_cast(transfer.receivedBytes) * 100U) / transfer.imageSize); + const uint32_t rateKiBs = static_cast((static_cast(transfer.receivedBytes) * 1000U) / elapsedMs / 1024U); + SS2K_LOG(BLE_OTA_LOG_TAG, "Progress: conn=%u bytes=%lu/%lu (%lu%%) rate=%lu KiB/s", transfer.connectionHandle, + static_cast(transfer.receivedBytes), static_cast(transfer.imageSize), static_cast(percent), + static_cast(rateKiBs)); + while (transfer.nextProgressLog <= transfer.receivedBytes) { + transfer.nextProgressLog += PROGRESS_LOG_INTERVAL; } - } else { - SS2K_LOG(BLE_OTA_LOG_TAG, "Data Length < 1"); - ss2k->isUpdating = false; - downloadFlag = false; - ss2k->rebootFlag = true; } } }; -void BLEFirmwareSetup(NimBLEServer *pServer) { - // 3. Create BLE Service - NimBLEService *pService = pServer->createService(FIRMWARE_SERVICE_UUID); +ControlCallback controlCallback; +DataCallback dataCallback; + +} // namespace + +void BLEFirmwareSetup(NimBLEServer* server) { + transferMutex = xSemaphoreCreateMutexStatic(&transferMutexStorage); + if (transferMutex == nullptr) { + SS2K_LOGE(BLE_OTA_LOG_TAG, "Unable to create BLE firmware update mutex"); + return; + } - // 4. Create BLE Characteristics inside the service(s) - pTxCharacteristic = pService->createCharacteristic(FIRMWARE_CHARACTERISTIC_TX_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY); + NimBLEService* service = server->createService(FIRMWARE_SERVICE_UUID); - pOtaCharacteristic = pService->createCharacteristic(FIRMWARE_CHARACTERISTIC_OTA_UUID, NIMBLE_PROPERTY::WRITE); - pOtaCharacteristic->setCallbacks(new otaCallback()); + statusCharacteristic = service->createCharacteristic( + FIRMWARE_CHARACTERISTIC_TX_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY, BleFirmwareUpdate::STATUS_PACKET_SIZE); + statusCharacteristic->setCallbacks(&controlCallback); - // 6. Start advertising - // spinBLEServer.pServer->getAdvertising()->addServiceUUID(pService->getUUID()); + NimBLECharacteristic* dataCharacteristic = + service->createCharacteristic(FIRMWARE_CHARACTERISTIC_OTA_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR, BleFirmwareUpdate::MAX_DATA_CHUNK_SIZE); + dataCharacteristic->setCallbacks(&dataCallback); + + resetTransfer(); + publishStatus(BLE_HS_CONN_HANDLE_NONE, false); +} + +void BLEFirmwareUpdateLoop() { + TransferGuard guard(0); + if (!guard || !transfer.active) return; + if (!transfer.otaBegun) { + if (millis() - transfer.lastActivityMs < OTA_PREPARE_DELAY_MS) return; + prepareOtaPartition(); + return; + } + if (!BleFirmwareUpdate::hasTransferTimedOut(millis(), transfer.lastActivityMs)) return; + + failTransfer(BleFirmwareUpdate::Error::TransferTimedOut, "BLE firmware update timed out waiting for data"); +} - downloadFlag = false; +void BLEFirmwareUpdateOnDisconnect(uint16_t connectionHandle) { + TransferGuard guard(portMAX_DELAY); + if (!guard) return; + if (transfer.connectionHandle != connectionHandle && transfer.state == BleFirmwareUpdate::State::Waiting) return; + SS2K_LOGW(BLE_OTA_LOG_TAG, "Disconnect: conn=%u owner=%u match=%u active=%u state=%s error=%s(%u) bytes=%lu/%lu", connectionHandle, transfer.connectionHandle, + ownsTransfer(connectionHandle), transfer.active, stateName(transfer.state), errorName(transfer.error), static_cast(transfer.error), + static_cast(transfer.receivedBytes), static_cast(transfer.imageSize)); + if (!ownsTransfer(connectionHandle)) return; + failTransfer(BleFirmwareUpdate::Error::ConnectionLost, "BLE firmware update connection was lost", ESP_OK, false); } diff --git a/src/BLE_Server.cpp b/src/BLE_Server.cpp index 918fe0cd..877efaf1 100644 --- a/src/BLE_Server.cpp +++ b/src/BLE_Server.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include "BLE_Cycling_Speed_Cadence.h" @@ -59,6 +61,15 @@ void addIpAddressToAdvertisement(NimBLEAdvertising* advertising) { } } // namespace +void BLERequestMtuExchange(uint16_t connectionHandle) { + const int result = ble_gattc_exchange_mtu(connectionHandle, nullptr, nullptr); + if (result == 0) { + SS2K_LOG(BLE_SERVER_LOG_TAG, "Requested ATT MTU exchange for connection %u", connectionHandle); + } else if (result != BLE_HS_EALREADY) { + SS2K_LOGW(BLE_SERVER_LOG_TAG, "Unable to request ATT MTU exchange for connection %u: %d", connectionHandle, result); + } +} + void startBLEServer() { // Server Setup SS2K_LOG(BLE_SERVER_LOG_TAG, "Starting BLE Server"); @@ -172,6 +183,7 @@ void SpinBLEServer::updateWheelAndCrankRev() { // Creating Server Connection Callbacks void MyServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) { SS2K_LOG(BLE_SERVER_LOG_TAG, "Bluetooth Remote Client Connected: %s Connected Clients: %d", connInfo.getAddress().toString().c_str(), pServer->getConnectedCount()); + BLERequestMtuExchange(connInfo.getConnHandle()); if (pServer->getConnectedCount() < CONFIG_BT_NIMBLE_MAX_CONNECTIONS - NUM_BLE_DEVICES) { BLEDevice::startAdvertising(); @@ -181,18 +193,15 @@ void MyServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInf } } -void MyServerCallbacks::onDisconnect(NimBLEServer* pServer) { - SS2K_LOG(BLE_SERVER_LOG_TAG, "Bluetooth Remote Client Disconnected. Remaining Clients: %d", pServer->getConnectedCount()); +void MyServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) { + SS2K_LOG(BLE_SERVER_LOG_TAG, "Bluetooth Remote Client Disconnected. Reason: %d (%s) Remaining Clients: %d", reason, NimBLEUtils::returnCodeToString(reason), + pServer->getConnectedCount()); + BLEFirmwareUpdateOnDisconnect(connInfo.getConnHandle()); BLEDevice::startAdvertising(); - // client disconnected while trying to write fw - reboot to clear the faulty upload. - if (ss2k->isUpdating) { - SS2K_LOG(BLE_SERVER_LOG_TAG, "Rebooting because of update interruption.", pServer->getConnectedCount()); - ss2k->rebootFlag = true; - } } void MyServerCallbacks::onMTUChange(uint16_t MTU, NimBLEConnInfo& connInfo) { - // SS2K_LOG(BLE_SERVER_LOG_TAG, "MTU updated: %u for connection ID: %u", MTU, connInfo.getConnHandle()); + SS2K_LOG(BLE_SERVER_LOG_TAG, "ATT MTU updated to %u for connection %u", MTU, connInfo.getConnHandle()); } bool MyServerCallbacks::onConnParamsUpdateRequest(uint16_t handle, const ble_gap_upd_params* params) { diff --git a/src/Main.cpp b/src/Main.cpp index e6393569..700a35ad 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -230,6 +230,7 @@ void SS2K::maintenanceLoop(void* pvParameters) { while (true) { delay(10); + BLEFirmwareUpdateLoop(); // be quiet while updating via BLE if (!ss2k->isUpdating) { diff --git a/test/test.h b/test/test.h index 428a32d0..b27a1e31 100644 --- a/test/test.h +++ b/test/test.h @@ -57,3 +57,11 @@ class TestAdevName2UniqueName { static void test_backward_compatibility(void); static void test_case_insensitive_device_matching(void); }; + +class TestBleFirmwareUpdateProtocol { + public: + static void test_parses_start_packet(void); + static void test_rejects_invalid_start_packets(void); + static void test_encodes_status_packet(void); + static void test_transfer_timeout(void); +}; diff --git a/test/test_BLEFirmwareUpdateProtocol.cpp b/test/test_BLEFirmwareUpdateProtocol.cpp new file mode 100644 index 00000000..fdcfe7e3 --- /dev/null +++ b/test/test_BLEFirmwareUpdateProtocol.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 Anthony Doud & Joel Baranick + * All rights reserved + * + * SPDX-License-Identifier: GPL-2.0-only + */ + +#include + +#include "BLE_Firmware_Update.h" +#include "test.h" + +void TestBleFirmwareUpdateProtocol::test_parses_start_packet() { + uint8_t packet[BleFirmwareUpdate::START_PACKET_SIZE] = { + static_cast(BleFirmwareUpdate::Command::Start), BleFirmwareUpdate::PROTOCOL_VERSION, 0x78, 0x56, 0x34, 0x12, 0xef, 0xcd, 0xab, 0x90, + }; + BleFirmwareUpdate::StartRequest request{}; + + TEST_ASSERT_TRUE(BleFirmwareUpdate::parseStartRequest(packet, sizeof(packet), request)); + TEST_ASSERT_EQUAL_HEX32(0x12345678, request.imageSize); + TEST_ASSERT_EQUAL_HEX32(0x90abcdef, request.imageCrc32); +} + +void TestBleFirmwareUpdateProtocol::test_rejects_invalid_start_packets() { + uint8_t packet[BleFirmwareUpdate::START_PACKET_SIZE]{}; + packet[0] = static_cast(BleFirmwareUpdate::Command::Start); + packet[1] = BleFirmwareUpdate::PROTOCOL_VERSION; + BleFirmwareUpdate::StartRequest request{}; + + TEST_ASSERT_FALSE(BleFirmwareUpdate::parseStartRequest(nullptr, sizeof(packet), request)); + TEST_ASSERT_FALSE(BleFirmwareUpdate::parseStartRequest(packet, sizeof(packet) - 1, request)); + packet[1]++; + TEST_ASSERT_FALSE(BleFirmwareUpdate::parseStartRequest(packet, sizeof(packet), request)); +} + +void TestBleFirmwareUpdateProtocol::test_encodes_status_packet() { + const auto packet = BleFirmwareUpdate::makeStatusPacket(BleFirmwareUpdate::State::Flashing, BleFirmwareUpdate::Error::None, 0x12345678, 0x90abcdef); + + TEST_ASSERT_EQUAL(BleFirmwareUpdate::STATUS_PACKET_SIZE, packet.size()); + TEST_ASSERT_LESS_OR_EQUAL(20, packet.size()); + TEST_ASSERT_EQUAL(BleFirmwareUpdate::PROTOCOL_VERSION, packet[0]); + TEST_ASSERT_EQUAL_HEX8(static_cast(BleFirmwareUpdate::State::Flashing), packet[1]); + TEST_ASSERT_EQUAL_HEX8(static_cast(BleFirmwareUpdate::Error::None), packet[2]); + TEST_ASSERT_EQUAL_HEX8(BleFirmwareUpdate::CAPABILITIES, packet[3]); + TEST_ASSERT_NOT_EQUAL(0, packet[3] & BleFirmwareUpdate::CAP_WRITE_NO_RSP); + TEST_ASSERT_EQUAL_HEX32(0x12345678, BleFirmwareUpdate::readUint32LE(packet.data() + 4)); + TEST_ASSERT_EQUAL_HEX32(0x90abcdef, BleFirmwareUpdate::readUint32LE(packet.data() + 8)); +} + +void TestBleFirmwareUpdateProtocol::test_transfer_timeout() { + TEST_ASSERT_FALSE(BleFirmwareUpdate::hasTransferTimedOut(29999, 0)); + TEST_ASSERT_TRUE(BleFirmwareUpdate::hasTransferTimedOut(30000, 0)); + TEST_ASSERT_TRUE(BleFirmwareUpdate::hasTransferTimedOut(100, UINT32_MAX - 30000)); +} diff --git a/test/test_unity.cpp b/test/test_unity.cpp index f32de69b..553e8476 100644 --- a/test/test_unity.cpp +++ b/test/test_unity.cpp @@ -85,6 +85,15 @@ void setup() { RUN_TEST(test.test_case_insensitive_device_matching); } + // BLE Firmware Update Protocol Tests + { + TestBleFirmwareUpdateProtocol test; + RUN_TEST(test.test_parses_start_packet); + RUN_TEST(test.test_rejects_invalid_start_packets); + RUN_TEST(test.test_encodes_status_packet); + RUN_TEST(test.test_transfer_timeout); + } + UNITY_END(); }