Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
86 changes: 86 additions & 0 deletions BLEFirmwareUpdateProtocol.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion include/BLE_Common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down Expand Up @@ -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*****************************

Expand Down
109 changes: 109 additions & 0 deletions include/BLE_Firmware_Update.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2020 Anthony Doud & Joel Baranick
* All rights reserved
*
* SPDX-License-Identifier: GPL-2.0-only
*/

#pragma once

#include <array>
#include <cstddef>
#include <cstdint>

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<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) | (static_cast<uint32_t>(data[2]) << 16) |
(static_cast<uint32_t>(data[3]) << 24);
}

inline void writeUint32LE(uint8_t* data, uint32_t value) {
data[0] = static_cast<uint8_t>(value);
data[1] = static_cast<uint8_t>(value >> 8);
data[2] = static_cast<uint8_t>(value >> 16);
data[3] = static_cast<uint8_t>(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<uint8_t>(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<uint8_t, STATUS_PACKET_SIZE> makeStatusPacket(State state, Error error, uint32_t receivedBytes, uint32_t imageSize) {
std::array<uint8_t, STATUS_PACKET_SIZE> packet{};
packet[0] = PROTOCOL_VERSION;
packet[1] = static_cast<uint8_t>(state);
packet[2] = static_cast<uint8_t>(error);
packet[3] = CAPABILITIES;
writeUint32LE(packet.data() + 4, receivedBytes);
writeUint32LE(packet.data() + 8, imageSize);
return packet;
}

} // namespace BleFirmwareUpdate
Loading
Loading