Repository navigation
RICRESTProtocol
RICREST is Raft's transport-agnostic message envelope that carries REST-style requests, responses and binary stream blocks over non-HTTP links (BLE, serial, sockets). On the wire it is normally wrapped by one of the framing protocols RICSerial, RICFrame or RICJSON; the RICREST payload itself is the same in all three.
For higher-level flow see Communications Stack Overview and ProtocolExchange.
┌────────────────────────────────────────────────────────────────┐
│ Outer framing : RICSerial (HDLC) | RICFrame (len-prefixed) | │
│ RICJSON (raw JSON) │
└────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ CommsChannelMsg header │
│ - msg number (1 byte for RICFrame; managed by RICSerial HDLC)│
│ - protocol code (RICREST, BridgeRICREST, RawCmdFrame, ...) │
│ - type code (Command, Response, Publish, Report) │
└────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ RICREST element (only when protocol == RICREST) │
│ - 1 byte element code │
│ - element-specific payload │
└────────────────────────────────────────────────────────────────┘
ProtocolRICSerial uses MiniHDLC to frame and deframe a byte stream:
E7 | esc(header + payload) | esc(crcHi) | esc(crcLo) | E7
-
0xE7is the frame boundary byte that starts and ends each frame. -
0xD7is the escape byte. A0xE7or0xD7in the frame (including in the CRC) is sent as0xD7followed by the byte XOR0x20; no other bytes are escaped. - The CRC is CRC-16/CCITT-FALSE (polynomial
0x1021, initial value0xFFFF,crc16("123456789") = 0x29B1) over the unescaped header and payload, sent high byte first. - The bytes inside the frame are the
CommsChannelMsgheader followed by its payload (see below). - The device drops a frame longer than
MaxRxMsgLen(5000 bytes, or 200000 with PSRAM) or with a bad CRC. The boundary and escape bytes and the limits can be changed in the SysType (see ProtocolExchange → Configuration).
This is the framing used over UART, BLE GATT characteristics and (by default) WebSockets. A frame can span several BLE writes or notifications.
-
Serial: the SerialConsole shares the port with log text and typed commands, so the HDLC bytes are re-encoded by
ProtocolOverAsciiso that every byte sent has its top bit set. Received bytes>= 0x80go to the OverAscii decoder and then to HDLC; bytes< 0x80are text.Byte Sent as 00-0F85,(b ^ 0x20) | 0x8010-7Fb | 0x8080-8F8E,b ^ 0x2090-FF8F,b -
WebSocket: each HDLC frame is sent as a binary WebSocket message at
ws://<host>/<pfix>(pfixdefaultws), with no OverAscii encoding. -
BLE: frames are written to the command characteristic and responses arrive on the response characteristic (see BLEManager).
Test vectors (a URL request with message number 1, the URL followed by a NUL as raftjs sends it; the device doesn't need the NUL):
| Request | Frame |
|---|---|
v |
e7 01 02 00 76 00 f4 ca e7 |
filelist |
e7 01 02 00 66 69 6c 65 6c 69 73 74 00 d1 dc e7 |
v, message number 0xE7 (escaped) |
e7 d7 c7 02 00 76 00 02 43 e7 |
ProtocolRICFrame is used where the transport already provides framing (TCP socket, WebSocket binary frame). It does not add HDLC byte-stuffing or a CRC. Each frame is just:
byte 0 : msgNumber (0..255)
byte 1 : (msgTypeCode << 6) | (msgProtocolCode & 0x3f)
bytes 2+ : payload (= RICREST element when protocol == RICREST)
msgTypeCode is 0=COMMAND, 1=RESPONSE, 2=PUBLISH, 3=REPORT.
msgProtocolCode is the CommsMsgProtocol enum (2=RICREST, 3=BridgeRICREST, 0x3e=RawCmdFrame, 0x3f=None, 0=ROSSerial).
ProtocolRICJSON carries pure JSON with no envelope. The JSON document itself encodes whatever metadata it needs and is delivered to the application as a CommsChannelMsg with the JSON in the payload.
When carried inside a RICSerial HDLC frame, the first two bytes are the same as RICFrame's header:
byte 0 : msgNumber
byte 1 : (msgTypeCode << 6) | (msgProtocolCode & 0x3f)
bytes 2+ : payload
So at the API level RICSerial and RICFrame produce identical CommsChannelMsg objects — they differ only in how the bytes are framed for the underlying transport. A RICREST command is therefore [msgNum, 0x02, elemCode, ...] and its response [msgNum, 0x42, 0x01, json..., 0x00].
When msgProtocolCode == MSG_PROTOCOL_RICREST (2), the payload begins with a one-byte element code:
| Code | Name | Inbound role | Outbound role |
|---|---|---|---|
0 |
URL |
Request as URL string (endpoint/path?query=...). |
— |
1 |
CMDRESPJSON |
Decoded but not acted on (no response). | Response wrapping the endpoint's JSON output. |
2 |
BODY |
POST-style body chunk. Decoded, but POST over RICREST isn't implemented (no response). | — |
3 |
COMMAND_FRAME |
JSON { "cmdName": "...", … } flattened into a query string. |
— |
4 |
FILEBLOCK |
A binary chunk of a file/stream session. | A binary chunk of a file/stream session. |
byte 0 : 0x00
bytes 1..n : ASCII URL, e.g. "subscription?action=update&topic=devjson&rateHz=10"
The whole tail of the message is the URL as a UTF-8 string (max length capped by RICRESTMsg::getMaxRestBodySize() — 5000 without PSRAM, 200000 with).
byte 0 : 0x01
bytes 1..n : ASCII JSON object
Outbound: the JSON is the endpoint's response document followed by a NUL byte, e.g.
{"req":"subscription","rslt":"ok"}The outbound message is marked as a response (msg type code 1) and reuses the original message's msgNumber so the requester can match request to response.
Inbound: a CMDRESPJSON element is decoded (its reqStr field is read as the request name) but ProtocolExchange doesn't act on it and sends no response. Send requests as URL or COMMAND_FRAME elements.
byte 0 : 0x02
bytes 1..4 : bufferPos (uint32, big-endian) ← offset of this chunk in the overall body
bytes 5..8 : totalBytes (uint32, big-endian) ← total body length
bytes 9..n : binary payload
Intended for a multi-chunk request body (POST over RICREST). It is decoded, but ProtocolExchange doesn't implement it and sends no response, so POST endpoints (e.g. fileupload, postsettings) can't be used over RICREST; uploads use the file transfer commands instead.
byte 0 : 0x03
bytes 1..n : ASCII JSON object with at least a "cmdName" field
The JSON is converted into a request string of the form cmdName?<flattened-fields> and dispatched via the same path as a URL element. This is convenient for senders that prefer to encode all parameters as JSON instead of a URL. The file transfer commands (ufStart, dfStart etc. - see File transfers) are command frames handled by the file/stream sessions rather than REST endpoints.
byte 0 : 0x04
byte 1 : streamID (uint8, non-zero session ID)
bytes 2..4 : filePos / offset (24-bit unsigned integer, big-endian)
bytes 5..n : binary payload
Used by the file/stream session machinery (see File Download Protocol, Real-Time Streams, and the upload/firmware-update flows owned by ProtocolExchange).
The streamID byte identifies the active file/stream session allocated by ufStart or dfStart. It is an 8-bit field, so non-zero session IDs are representable in the range 1..255. streamID value 0 is reserved internally as FILE_STREAM_ID_ANY ("unspecified / match by other criteria") and must not be used for normal FILEBLOCK data belonging to an allocated session.
The stream ID and position are stored as one big-endian 32-bit word in code:
(streamID << 24) | (filePos & 0x00ffffff)
This limits the per-block file/stream position field to 24 bits. Protocol implementations that need larger transfers must handle that at the session/protocol layer rather than placing position bits in the top byte.
Note — the precise byte offsets above match the constants in
RICRESTMsg.h(RICREST_BODY_*,RICREST_FILEBLOCK_*). For exact, copy-pasteable values see that header. If you are implementing a non-Raft client, refer directly to thedecode()andencode()functions inRICRESTMsg.cpprather than transcribing the table.
| Transport | Recommended framing |
|---|---|
| BLE GATT characteristic | RICSerial (HDLC) |
| UART / RS-232 | RICSerial (HDLC) |
| TCP socket | RICFrame |
| WebSocket (binary) | RICSerial (the WebServer default pcol, used by raftjs and RaftCLI) or RICFrame |
| HTTP REST | none — use the URL directly via RestAPIEndpointManager
|
A RICJSON channel passes each received JSON object ({"cmdName":...}) to the endpoint as a raw command frame, but the response isn't sent back, so it only suits commands whose reply isn't needed.
For request/response correlation:
- The sender numbers its requests 1..255, wrapping back to 1. Message number 0 means unnumbered; the device uses it for messages it sends on its own (e.g. upload timer acks, cancel notices, download blocks).
- The framework's response path calls
endpointMsg.setAsResponse(cmdMsg)which copies thechannelIDandmsgNumberfrom the original request. - Receivers match responses to requests on
(channelID, msgNumber, msgTypeCode == RESPONSE). - A response may come later than responses to requests sent after it: an endpoint can defer its response to a numbered request. If a request is resent with the same message number while its response is still deferred, the device ignores the resend, so a client can safely retry with the same number.
Asynchronous messages (publishes, reports) use MSG_TYPE_PUBLISH / MSG_TYPE_REPORT and a different msgNumber stream.
Files are uploaded and downloaded over RICREST with COMMAND_FRAME commands (cmdName below) and FILEBLOCK elements, handled by the file/stream sessions in ProtocolExchange. The replies are CMDRESPJSON responses.
-
ufStart, e.g.{"cmdName":"ufStart","reqStr":"fileupload","fileType":"fs","fileName":"/local/a.txt","fileLen":2048,"batchMsgSize":800,"batchAckSize":10}.-
fileType:fsorfile(or empty) for a file,fworricfwfor a firmware update (OTA Update Flow),rtstreamfor a real-time stream. -
batchMsgSize(block size, default 5000) andbatchAckSize(blocks per acknowledgement, default 40) are requests. The device clamps the block size to between 20 bytes and 2/3 of the channel'sinboundBlockLen(800 bytes on serial and on WebSocket without PSRAM, 16000 on WebSocket with PSRAM, 333 on BLE), limits block × batch to 50000 bytes, and reduces both if the memory isn't there. - The reply gives the values used and the session's stream ID:
{"req":"ufStart","batchMsgSize":800,"batchAckSize":10,"streamID":1,"rslt":"ok"}. Always use the values in the reply. - An optional
CRC16(e.g."CRC16":"0xa1f2", the CRC-16/CCITT of the whole file asfilehashgives it) has the file checked once written: if it doesn't match, the file is deleted and the upload cancelled (failCRC). See Checking the file. Firmware with RaftCore 1.63.0 or earlier ignores it.
-
-
Blocks:
FILEBLOCKelements carrying the stream ID and the position of the data, e.g.[msgNum, 0x02, 0x04, streamID, pos>>16, pos>>8, pos, data...]. -
Acks: the device replies
{"req":"ufBlock","okto":N,"rslt":"ok"}(as the response to a block, or with message number 0 when it is sent later) -Nis the number of bytes accepted so far - after the first block, after everybatchAckSizeblocks, and after the last block. File blocks are written on the file system worker (firmware by the firmware updater), and an ack is only sent once the blocks it covers are written. A block at an unexpected position is dropped (the first one is acked at once so the host can resend fromokto). If no blocks arrive the device sends timer acks: after 5 s while fewer than two blocks have arrived, then after 1 s; after 4 timer acks with no block it cancels the upload (failRetries). The host sends the first block, then batches ofbatchAckSize, waits foroktoand resumes from it (go-back-N). -
ufEndgets{"req":"ufEnd","rslt":"ok"}, with"crc16":"xxxx"added when the file was checked against theCRC16given (so the host needn't check it withfilehash). The session normally ends by itself after the last block, and aufEndthat finds no session still gets ok (and thecrc16, which is kept after the session ends). -
ufCancel(with an optionalreason) cancels an upload from the host. The device sends a response with"cmdName":"ufCancel","reason":...(message number 0) when it cancels one itself (e.g.failFileWrite,failOTAWrite,failCRCwhen the file doesn't match itsCRC16, orfailRetriesafter the timer acks).
dfStart opens a download session; the device then sends the file as FILEBLOCK elements (as commands with message number 0), at most one block every 100 ms, and the host acknowledges with dfAck (okto, no reply). dfEnd ends it, and dfCancel cancels it. A dfStart for a file that doesn't exist is refused with "reason":"sessionNotStarted". See File Download Protocol (OKTO) for the details.
Getting Started
- Quick Start
- Architecture at a Glance
- Writing Your First SysMod
- Adding a Comms Channel
- Adding an I2C Device Type
Scaffolding & Building
- Raft CLI
- Web Apps (Device Types, Flasher)
- SysTypes
- Top-Level SysType
- Build Process
- WebUI Build Pipeline
- File System
- Partitions & Flash
- Local Dev Libraries
- Library Developer Guide
Architecture
Built-in SysMods
- NetworkManager
- WiFi Troubleshooting
- BLEManager
- WebServer
- MQTTManager
- SerialConsole
- CommandSerial
- CommandSocket
- CommandFile
- FileManager
- LogManager
- ESPOTAUpdate
- StatePublisher
- Remote Logging
- Data Source Registration
Comms & Protocols
- Stack Overview
- Comms Channels
- ProtocolExchange
- RICREST Protocol
- Real-Time Streams
- Adding REST Endpoints
- Built-in REST Endpoints
- File Download (OKTO)
- OTA Update Flow
Devices & Buses
- DeviceManager
- Device Manager REST API
- Device Factory & Classes
- Device Type Records
- Adding an I2C Device Type
- Device Data Publishing
- Data Logger
- I2C Bus
- I2C Device Scanning
- I2C ID & Polling
- MotorControl Overview
- MotorControl Config
- MotorControl Commands
- Camera
ROS 2
Helpers
Reference