From 531e33715f36af653c9c32be09bf8fa41d0bf2e3 Mon Sep 17 00:00:00 2001 From: Anton Weber <anton@antweb.me> Date: Wed, 7 Aug 2019 22:54:11 +0200 Subject: [PATCH] docs(ble): Add file transfer documentation --- Documentation/bluetooth/file-transfer.rst | 189 +++++++++++++++++++ Documentation/index.rst | 6 + Documentation/static/ble-low-effort-flow.dia | Bin 0 -> 2331 bytes Documentation/static/ble-low-effort-flow.png | Bin 0 -> 13295 bytes 4 files changed, 195 insertions(+) create mode 100644 Documentation/bluetooth/file-transfer.rst create mode 100644 Documentation/static/ble-low-effort-flow.dia create mode 100644 Documentation/static/ble-low-effort-flow.png diff --git a/Documentation/bluetooth/file-transfer.rst b/Documentation/bluetooth/file-transfer.rst new file mode 100644 index 00000000..c098c6cd --- /dev/null +++ b/Documentation/bluetooth/file-transfer.rst @@ -0,0 +1,189 @@ +Bluetooth File Transfer +======================= + +.. warning:: + The file transfer specification is still work in progress + +File transfer to the card10 is implemented using the custom Low Effort File +Transfer Protocol. + +BLE Service +----------- +The service consists of two GATT characteristics that act as a bidrectional +link, similar to many BLE UART implementations. The two channels are seen from +the Central perspective and hence named Central TX and Central RX. + +The current draft uses following service specification: + +- Service + + UUID: 00422342-2342-2342-2342-234223422342 + +- Central TX characteristic: + + UUID: 01422342-2342-2342-2342-234223422342 + write + +- Central RX characteristic: + + UUID 02422342-2342-2342-2342-234223422342 + read, notify + +Low Effort File Transfer Protocol +--------------------------------- +(Version 1) + +This protocol was designed to strike a balance between ease of implementation +and reasonable levels of functionality. + +Features: + +- File push from Central (e.g. Android) to Peripheral (card10) +- Path and file name support +- Chunked data transfer for variable MTUs +- CRC32 error-detection +- Basic error handling + +All communication between Central and Peripheral is packet based. The first +byte specifies the packet type using a char followed by an optional CRC and/or +payload, depending on the packet type. + +START: + +===== ==== + 0 1-N +----- ---- + s path +===== ==== + +START_ACK: + +===== === + 0 1-4 +----- --- + S CRC +===== === + +CHUNK: + +===== ====== ======= + 0 1-4 4-N +----- ------ ------- + c offset payload +===== ====== ======= + +CHUNK_ACK: + +===== === + 0 1-4 +----- --- + C CRC +===== === + +FINISH: + +=== === + 0 +--- --- + f +=== === + +FINISH_ACK: + +=== === + 0 +--- --- + F +=== === + +ERROR: + +=== === + 0 +--- --- + e +=== === + + +ERROR_ACK: + +=== === + 0 +--- --- + E +=== === + +Flow +---- + +The file transfer process can be described as a series of states from the view +of the Central role: + + +.. image:: ../static/ble-low-effort-flow.png + + +``IDLE`` state: + + - Send ``START`` to initiate transfer + +``START_SENT`` state: + + - Wait for ``START_ACK`` + +``SEND_READY`` state: + + - Send first ``CHUNK`` + +``CHUNK_SENT`` state: + + - Wait for ``CHUNK_ACK`` + +``SEND_READY`` state: + + - Repeat previous two steps until all data is sent + - If the last chunk was sent, send ``FINISH`` + +``FINISH_SENT`` state: + + - Wait for ``FINISH_ACK`` + +After ``FINISH_ACK`` was received, the transfer is complete and the process can +return to ``IDLE``. + +Error Handling +-------------- +Three types of errors are currently supported: + +- CRC errors: + + If an ``ACK`` packet contains a CRC that fails the verification, then the + original packet must be retransmitted. If three consecutive attempts to + send a packet fail, then the transfer is aborted. + + +- ACK timeouts: + + If the Central does not receive a required ``ACK`` within 10 seconds, then + the original packet must be retransmitted. If three consecutive attempts to + send a packet fail, then the transfer is aborted. + + +- Unexpected response: + + All steps in the flow described above have exactly one expected response. + If any other packet is received, then the transfer is aborted. + +Aborting Transfer +----------------- +To abort the transfer, the Central role sends an ``ERROR`` packet and returns +to ``IDLE`` after receiving the ``ERROR_ACK``. +If the Peripheral role aborts the transfer, i.e. the Central receives an +``ERROR`` at any point, then it responds with ``ERROR_ACK`` and returns to +``IDLE`` + +.. warning:: + As this is a custom file transfer protocol developed under less than ideal + circumstances, it does not provide any guarantees, especially not regarding + reliability or security. The protocol assumes a secure link and a + trustworthy peer, amongst many other things. Use with caution. diff --git a/Documentation/index.rst b/Documentation/index.rst index 50fdc4af..a5be0e5c 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -48,6 +48,12 @@ Last but not least, if you want to start hacking the lower-level firmware, the epicardium/api epicardium-guide +.. toctree:: + :maxdepth: 1 + :caption: Bluetooth + + bluetooth/file-transfer + Indices and tables ================== diff --git a/Documentation/static/ble-low-effort-flow.dia b/Documentation/static/ble-low-effort-flow.dia new file mode 100644 index 0000000000000000000000000000000000000000..7dfc86360869474e57806bf5da189e728d210876 GIT binary patch literal 2331 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 Biv|Dy literal 0 HcmV?d00001 diff --git a/Documentation/static/ble-low-effort-flow.png b/Documentation/static/ble-low-effort-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..da33b299973316817a8e16574564d50dd9af7aaa GIT binary patch literal 13295 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 WW5X+acm7{{P^87>ALczU@cCaeL@WOQ literal 0 HcmV?d00001 -- GitLab