6.7 KiB
NetworkValue Protobuf Serialization
1. Motivation
The current implementation of NetworkValue uses JSON for serializing data updates, which are broadcast over WebSockets by RestServer. While JSON is human-readable and easy to debug, it can be verbose and relatively slow to parse, especially on resource-constrained microcontrollers.
For high-frequency updates, a more efficient serialization format is needed. Protocol Buffers (Protobuf) offer a binary format that is:
- Smaller: Payloads are significantly more compact than their JSON counterparts.
- Faster: Parsing binary data requires less CPU effort than parsing text.
- Strongly Typed: The schema, even if not defined in a
.protofile, provides a clear structure.
The goal is to add Protobuf as an optional serialization mechanism for NetworkValue updates to improve performance for real-time client communication.
2. High-Level Design
We will introduce a new, optional feature to the NetworkValue class, similar to the existing NV_Logging, NV_Modbus, and NV_Notify features.
-
New Feature Class: A new class,
NV_Protobuf, will be created in a separate header,NetworkValuePB.h. This class will contain the logic for encoding aNetworkValue's state into the Protobuf binary format. -
Compile-Time & Runtime Activation: The feature will be enabled by a compile-time flag (
NETWORKVALUE_ENABLE_PROTOBUF) and can be managed at runtime via theNetworkValue's feature flags, just like other features. -
Schema-less Approach: To avoid the complexity of managing
.protofiles and a pre-compilation step, we will not use.protodefinitions. Instead, we will perform direct encoding and decoding using thenanopblibrary's helper functions (pb_encode_*). The message structure will be consistently defined and implemented in the firmware and client-side code. -
Integration: The
NetworkValue::update()method will be modified. When a value changes, and the Protobuf feature is enabled, it will encode the update into a binary payload and send it to its owner (typically an application component) via theonMessagesystem. -
Broadcasting: The
RestServerwill be updated to handle these new binary messages. Upon receiving a Protobuf-encoded update, it will broadcast the raw binary payload to all connected WebSocket clients, which will be responsible for decoding it.
3. Protobuf Message Structure
We will define a standard message structure for a single NetworkValue update. The client-side decoder must be built to expect this exact structure.
| Field Name | Field Number | Type | Description |
|---|---|---|---|
address |
1 | uint32 |
The Modbus address, used as the unique identifier for the NetworkValue. |
timestamp |
2 | uint64 |
The timestamp (millis()) when the update was generated. |
| oneof value | The actual value of the update. Only one of these will be present. | ||
sint_value |
3 | sint64 |
For all integer types (int, uint, short, enums). Zig-zag encoded. |
bool_value |
4 | bool |
For boolean values. |
float_value |
5 | float |
For 32-bit floating-point values. |
bytes_value |
6 | bytes |
For std::array or other raw byte data. |
4. Implementation Details
NetworkValuePB.h
This new file will contain the NV_Protobuf class with a series of overloaded/templated encode methods to handle the different data types supported by NetworkValue<T>. It will use nanopb's pb_encode.h functions directly.
// Example of an encode function in NV_Protobuf
template<typename T>
bool encode(pb_ostream_t* stream, const MB_Registers& regInfo, const T& value) const {
// 1. Encode address
pb_encode_tag(stream, PB_WT_VARINT, 1);
pb_encode_varint(stream, regInfo.startAddress);
// 2. Encode timestamp
pb_encode_tag(stream, PB_WT_VARINT, 2);
pb_encode_varint(stream, millis());
// 3. Encode the specific value based on its type
encode_value(stream, value);
return true;
}
NetworkValue.h Modifications
- Add
NETWORKVALUE_ENABLE_PROTOBUFandE_NetworkValueFeatureFlags::E_NVFF_PROTOBUF. - Include
NetworkValuePB.hand inherit frommaybe<..., NV_Protobuf>. - In
update_impl(), add logic to check if the Protobuf feature is enabled. If so, it will:- Create a
pb_ostream_tfrom a temporary buffer. - Call the
NV_Protobuf::encode()method. - Create a new
PB_UpdateDatastruct containing the encoded data and its length. - Send this struct via
owner->onMessage()using a new verb,E_CALLS::EC_PROTOBUF_UPDATE.
- Create a
enums.h and ModbusTypes.h
E_CALLSenum inenums.hwill be extended withEC_PROTOBUF_UPDATE.- A new struct
PB_UpdateData { uint8_t* data; size_t len; }will be defined inModbusTypes.hto carry the binary payload through theonMessagesystem.
RestServer.cpp Modifications
- The
onMessagehandler inRestServerwill be updated to handle theEC_PROTOBUF_UPDATEverb. - When a message with this verb is received, it will cast the
void* userdata toPB_UpdateData*. - It will then call
ws.binaryAll(pb_msg->data, pb_msg->len)to broadcast the binary payload to all connected WebSocket clients.
This approach provides an efficient, decoupled, and optional mechanism for pushing high-performance updates to clients without the overhead of JSON.
5. Sequence Diagrams
Value Update and Encoding Sequence
This diagram shows how a NetworkValue change triggers the Protobuf encoding process and dispatches the message.
sequenceDiagram
actor UserCode
participant NV as "NetworkValue"
participant NVPB as "NV_Protobuf"
participant App as "Application/Owner"
UserCode->>NV: update(newValue)
activate NV
Note right of NV: 1. Check if value changed
NV->>NVPB: encode(stream, regInfo, value)
activate NVPB
Note over NVPB: 2. Encode address, ts, value
NVPB-->>NV: encoded_data
deactivate NVPB
Note right of NV: 3. Create PB_UpdateData struct
NV->>App: onMessage(EC_PROTOBUF_UPDATE, &pb_data)
deactivate NV
Broadcast Sequence
This diagram illustrates how the RestServer receives the encoded message and broadcasts it to WebSocket clients.
sequenceDiagram
participant App as "Application/Owner"
participant RS as "RestServer"
participant WSC as "WebSocket Client"
App->>RS: onMessage(EC_PROTOBUF_UPDATE, &pb_data)
activate RS
Note right of RS: 1. Cast void* to PB_UpdateData*
RS->>WSC: ws.binaryAll(pb_data.data, pb_data.len)
Note over WSC: 2. Client receives<br/>binary payload
deactivate RS