Message Model
Root Message
All protocol characteristic traffic is a Reaction protobuf message inside a SLIP frame.
message Reaction {
enum MESSAGE_TYPE {
Query = 0;
Command = 1;
Update = 2;
}
enum QUERY_TYPE {
Unset = 0;
DeviceInfo = 1;
}
optional MESSAGE_TYPE type = 1;
optional QUERY_TYPE queryType = 2;
uint32 sequenceNumber = 3;
uint32 timestamp = 4;
oneof message {
QuizboxMessage quizbox = 10;
DeviceInfo device_info = 11;
FirmwareUpdate firmware_update = 12;
ConfigMessage config = 13;
BleAccessMessage ble_access = 14;
}
}
sequenceNumber is present in the schema but the current firmware does not use it for request correlation.
Device notifications are encoded with type=Update. If firmware is building an outbound message that is not Query or Command, the encoder forces type=Update.
SLIP Framing
SLIP constants:
| Name | Byte |
|---|---|
END |
0xC0 |
ESC |
0xDB |
escaped END |
0xDB 0xDC |
escaped ESC |
0xDB 0xDD |
The firmware and Python helper encode frames with an END byte at both the start and end of each frame:
0xC0 + escaped protobuf bytes + 0xC0
Clients should tolerate empty frames and keep buffering until a complete non-empty frame is available.
Request Routing
The firmware routes incoming decoded Reaction messages like this:
| Incoming message | Firmware behavior |
|---|---|
type=Query, queryType=DeviceInfo |
Sends a device_info update |
firmware_update oneof set |
Processes OTA command and sends a response for all commands except protobuf DATA |
config oneof set |
Processes GET, SET, RESET, or NOTIFY and sends a config update |
anything else with quizbox.event |
Converts to an internal event and publishes it as source=External |
External events are intentionally not echoed directly by BLE notifications. The state changes they cause may publish new internal events, which are then notified to clients.
Event Payload Selection
EventMessage has one event type plus a oneof payload:
| Event type | Payload |
|---|---|
QUIZZER_ACTION |
quizzer_event |
BUTTON_ACTION |
button_event |
TIMER_COUNTDOWN |
timer_event |
QUIZZER_DISPLAYED |
active_quizzers |
QUIZZER_ACTIVE |
active_quizzers |
QUIZZERS_LIT_UP |
active_quizzers |
TIMER_EXPIRED, TIMER_STARTED, TIMER_CLEARED, QUIZZER_CLEARED, PAIRING_ACTIVE, PAIRING_INACTIVE, END |
no additional payload |
The firmware sets ActiveQuizzers.is_active to true when the active quizzer list has at least one member.