Proton C
protonc is the C implementation and code generation toolchain for the Proton protocol. It's designed specifically for embedded systems and resource-constrained microcontrollers where memory and CPU are limited. It uses nanopb to encode and decode the protobuf bundles.
protonc is build-time configurable, meaning that the YAML configuration is built into the application binary, and cannot be modified without recompiling the application.
C Runtime Library
libprotonc is the core C implementation providing:
- Signal/Bundle handling - Initialization, encoding/decoding via nanopb
- Node management - Peer initialization, configuration, activation
- Transport abstraction - Connect/disconnect/read/write interfaces
- Serial framing helper functions - Magic bytes, length encoding, CRC16 validation
Core functionality
The core of protonc revolves around wrapping the generated Bundle and Signal protobuf implementations that are generated by nanopb, and implementing functions for initializing, encoding, and decoding Proton Bundles and Signals.
The proton.h and proton.c files define the C structs, definitions, and functions for this. The proton_field_callbacks.c file implements the callback functions used by nanopb for encoding or decoding a Proton Bundle or Signal into protobuf.
Node implementation
The node implementation in protonc enables automatic transport management and Bundle handling. The node.c and node.h files implement the function prototypes, structs, and definitions to enable node and transport functionality.
Proton C Generator
The Proton C generator is a Python tool that reads YAML configuration files and automatically generates:
- C header files (
.h) with type definitions, structs, enums, and function prototypes - C source files (
.c) with initialization, encoding, decoding, and send/receive logic
The generator is a Python package with the following files:
| File | Purpose |
|---|---|
protonc_generator.py | Main code generation logic |
config.py | YAML parsing and configuration representation |
source_writer.py | Helper for writing C code with proper formatting |
Code Generation Process
Code can be generated for a target node by manually calling the protonc_generator.py script, or using the protonc_generator CMake function in a CMake project.
Manually generating code
The usage for the python script is as follows:
python3 protonc_generator.py [-h] [-c CONFIG] [-d DESTINATION] [-t TARGET] [-n NODE]
where CONFIG is the path to your Proton configuration YAML file, DESTINATION is the folder in which generated files will be created, and TARGET is the name of the node for which you are generating code. The NODE flag is used to indicate whether you want node functionality to be generated or not. If set to False, only the base structs and encode/decode functions will be generated.
The generated files will be called proton__<CONFIG>_<TARGET>.c and proton__<CONFIG>_<TARGET>.h
CMake code generation
For CMake projects, it can be useful to have the protonc files automatically generated as part of the build process. This way, any updates to the configuration YAML file will be reflected in the application on rebuild.
Fetch Proton
To include Proton, it is recommended to use FetchContent to fetch the Proton repository from github as part of your build process.
include(FetchContent)
FetchContent_Declare(
proton
GIT_REPOSITORY https://github.com/clearpathrobotics/proton.git
GIT_TAG main
)
set(PROTON_BUILD_PROTONCPP OFF)
set(PROTON_BUILD_EXAMPLES OFF)
set(PROTON_BUILD_TESTS OFF)
set(PROTON_INSTALL OFF)
FetchContent_MakeAvailable(proton)
Set the GIT_TAG to a specific commit hash or tag for consistency, otherwise you will always pull in the latest changes.
Generate files
Now that Proton is available, we can define where the YAML configuration file is, and where generated files should go.
# Generate Proton sources and headers
set(PROTON_CONFIG_NAME "config")
set(PROTON_TARGET "target")
set(PROTON_GENERATE_NODE True)
set(PROTON_CONFIG_FILE ${CMAKE_CURRENT_SOURCE_DIR}/path/to/${PROTON_CONFIG_NAME}.yaml)
set(PROTON_GENERATED_FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/generated)
# Make generated directory
file(MAKE_DIRECTORY "${PROTON_GENERATED_FOLDER}")
set(PROTON_GENERATED_C_FILES
"${PROTON_GENERATED_FOLDER}/proton__${PROTON_CONFIG_NAME}_${PROTON_TARGET}.c"
"${PROTON_GENERATED_FOLDER}/proton__${PROTON_CONFIG_NAME}_${PROTON_TARGET}.h"
)
With everything defined, we can now call protonc_generator:
protonc_generator("${PROTON_GENERATED_C_FILES}" ${PROTON_CONFIG_FILE} ${PROTON_GENERATED_FOLDER} ${PROTON_TARGET} ${PROTON_GENERATE_NODE})
Including protonc in your project
With the files generated, you can now add the source and header files to your project, and link the protonc library:
# Add source files
add_executable(${PROJECT_NAME}
# Your sources
${PROTON_GENERATED_C_FILES}
)
# Add include directory
target_include_directories(${PROJECT_NAME} PRIVATE
# Your include paths
${PROTON_GENERATED_FOLDER}
)
# Link library
target_link_libraries(${PROJECT_NAME}
# Your libraries
proton::protonc
)
Now you can include the generated header file in your project and begin working with Proton:
#include "proton__config_target.h"
Generated code
To explain the generated code, a sample configuration will be used. In this configuration, there are two nodes: node1 and node2 which communicate with each other over UDP sockets on localhost. Two bundles are defined, one that node1 produces, and one that node2 produces. The target for generation will be node1.
Sample Proton configuration
nodes:
- name: node1
heartbeat:
enabled: true
period: 1000
endpoints:
- id: 0
type: udp4
ip: 127.0.0.1
port: 11416
- name: node2
heartbeat:
enabled: true
period: 100
endpoints:
- id: 0
type: udp4
ip: 127.0.0.1
port: 11417
connections:
- first: {node: node1, id: 0}
second: {node: node2, id: 0}
bundles:
- name: produced_bundle
id: 0x100
producers: node1
consumers: node2
signals:
- {name: integer, type: int32}
- {name: floats, type: list_float, length: 5}
- {name: name, type: string, capacity: 32}
- {name: hex_codes, type: list_bytes, length: 8, capacity: 2}
- name: consumed_bundle
id: 0x101
producers: node2
consumers: node1
signals:
- {name: booleans, type: list_bool, length: 3}
- {name: BOOL0, type: uint32, value: 0}
- {name: BOOL1, type: uint32, value: 1}
- {name: BOOL2, type: uint32, value: 2}
Header file
The full generated header file can be viewed below:
Generated header file
/*
* Copyright 2026 Rockwell Automation Technologies, Inc., All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* THIS FILE WAS GENERATED BY THE PROTONC CODE GENERATOR.
*/
#ifndef PROTONC__PROTON__SAMPLE_NODE1_H
#define PROTONC__PROTON__SAMPLE_NODE1_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stdint.h"
#include "stdbool.h"
#include "protonc/node.h"
// node1 Node Defines
#define PROTON__NODE__NODE1__NAME "node1"
#define PROTON__NODE__NODE1__ID 0x1
#define PROTON__NODE__NODE1__DEFAULT_VALUE PROTON_NODE_DEFAULT(PROTON__NODE__NODE1__NAME)
#define PROTON__NODE__NODE1__HEARTBEAT__ENABLED true
#define PROTON__NODE__NODE1__HEARTBEAT__PERIOD 1000
#define PROTON__NODE__NODE1__HEARTBEAT__DEFAULT_VALUE {PROTON__NODE__NODE1__HEARTBEAT__ENABLED, PROTON__NODE__NODE1__HEARTBEAT__PERIOD}
#define PROTON__NODE__NODE1__ENDPOINT__0__ID 0
#define PROTON__NODE__NODE1__ENDPOINT__0__TYPE "udp4"
#define PROTON__NODE__NODE1__ENDPOINT__0__IP "127.0.0.1"
#define PROTON__NODE__NODE1__ENDPOINT__0__IPHL 0x7f000001
#define PROTON__NODE__NODE1__ENDPOINT__0__IPNL 0x100007f
#define PROTON__NODE__NODE1__ENDPOINT__0__PORT 11416
// node2 Node Defines
#define PROTON__NODE__NODE2__NAME "node2"
#define PROTON__NODE__NODE2__ID 0x2
#define PROTON__NODE__NODE2__PEER__DEFAULT_VALUE PROTON_PEER_DEFAULT(PROTON__NODE__NODE2__NAME)
#define PROTON__NODE__NODE2__TRANSPORT__DEFAULT_VALUE {PROTON_TRANSPORT_DISCONNECTED, proton_node_node2_transport_connect, proton_node_node2_transport_disconnect, proton_node_node2_transport_read, proton_node_node2_transport_write}
#define PROTON__NODE__NODE2__HEARTBEAT__ENABLED true
#define PROTON__NODE__NODE2__HEARTBEAT__PERIOD 100
#define PROTON__NODE__NODE2__HEARTBEAT__DEFAULT_VALUE {PROTON__NODE__NODE2__HEARTBEAT__ENABLED, PROTON__NODE__NODE2__HEARTBEAT__PERIOD}
#define PROTON__NODE__NODE2__ENDPOINT__0__ID 0
#define PROTON__NODE__NODE2__ENDPOINT__0__TYPE "udp4"
#define PROTON__NODE__NODE2__ENDPOINT__0__IP "127.0.0.1"
#define PROTON__NODE__NODE2__ENDPOINT__0__IPHL 0x7f000001
#define PROTON__NODE__NODE2__ENDPOINT__0__IPNL 0x100007f
#define PROTON__NODE__NODE2__ENDPOINT__0__PORT 11417
// Bundle IDs
typedef enum PROTON__BUNDLE {
PROTON__BUNDLE__PRODUCED_BUNDLE = 0x100,
PROTON__BUNDLE__CONSUMED_BUNDLE = 0x101,
} PROTON__BUNDLE_e;
// Signal Enums
typedef enum PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL {
PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__INTEGER,
PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS,
PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME,
PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES,
PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__COUNT
} PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL_e;
typedef enum PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL {
PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS,
PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__COUNT
} PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL_e;
typedef enum PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL {
PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__HEARTBEAT,
PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__COUNT
} PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL_e;
typedef enum PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL {
PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__HEARTBEAT,
PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__COUNT
} PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL_e;
// Constant definitions
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__INTEGER__DEFAULT_VALUE 0
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS__LENGTH 5
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS__DEFAULT_VALUE {}
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__CAPACITY 32
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__DEFAULT_VALUE ""
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__LENGTH 8
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__CAPACITY 2
#define PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__DEFAULT_VALUE {}
#define PROTON__BUNDLE__PRODUCED_BUNDLE__DEFAULT_VALUE {PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__INTEGER__DEFAULT_VALUE, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS__DEFAULT_VALUE, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__DEFAULT_VALUE, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__DEFAULT_VALUE}
#define PROTON__BUNDLE__PRODUCED_BUNDLE__PRODUCERS (PROTON__NODE__NODE1__ID)
#define PROTON__BUNDLE__PRODUCED_BUNDLE__CONSUMERS (PROTON__NODE__NODE2__ID)
#define PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS__LENGTH 3
#define PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS__DEFAULT_VALUE {}
#define PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL0__DEFAULT_VALUE 0
#define PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL1__DEFAULT_VALUE 1
#define PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL2__DEFAULT_VALUE 2
#define PROTON__BUNDLE__CONSUMED_BUNDLE__DEFAULT_VALUE {PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS__DEFAULT_VALUE, PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL0__DEFAULT_VALUE, PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL1__DEFAULT_VALUE, PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOL2__DEFAULT_VALUE}
#define PROTON__BUNDLE__CONSUMED_BUNDLE__PRODUCERS (PROTON__NODE__NODE2__ID)
#define PROTON__BUNDLE__CONSUMED_BUNDLE__CONSUMERS (PROTON__NODE__NODE1__ID)
#define PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__HEARTBEAT__DEFAULT_VALUE 0
#define PROTON__BUNDLE__NODE1_HEARTBEAT__DEFAULT_VALUE {PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__HEARTBEAT__DEFAULT_VALUE}
#define PROTON__BUNDLE__NODE1_HEARTBEAT__PRODUCERS (PROTON__NODE__NODE1__ID)
#define PROTON__BUNDLE__NODE1_HEARTBEAT__CONSUMERS (PROTON__NODE__NODE2__ID)
#define PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__HEARTBEAT__DEFAULT_VALUE 0
#define PROTON__BUNDLE__NODE2_HEARTBEAT__DEFAULT_VALUE {PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__HEARTBEAT__DEFAULT_VALUE}
#define PROTON__BUNDLE__NODE2_HEARTBEAT__PRODUCERS (PROTON__NODE__NODE2__ID)
#define PROTON__BUNDLE__NODE2_HEARTBEAT__CONSUMERS (PROTON__NODE__NODE1__ID)
#define PROTON__BUNDLES__NODE1__DEFAULT_VALUE {PROTON__BUNDLE__PRODUCED_BUNDLE__DEFAULT_VALUE, PROTON__BUNDLE__CONSUMED_BUNDLE__DEFAULT_VALUE, PROTON__BUNDLE__NODE1_HEARTBEAT__DEFAULT_VALUE, PROTON__BUNDLE__NODE2_HEARTBEAT__DEFAULT_VALUE}
// Bundle Structure Definitions
typedef struct proton_bundle_produced_bundle {
int32_t integer;
float floats[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS__LENGTH];
char name[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__CAPACITY];
uint8_t hex_codes[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__LENGTH][PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__CAPACITY];
} proton_bundle_produced_bundle_t;
typedef struct proton_bundle_consumed_bundle {
bool booleans[PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS__LENGTH];
const uint32_t BOOL0;
const uint32_t BOOL1;
const uint32_t BOOL2;
} proton_bundle_consumed_bundle_t;
// Bundles Structure Definitions
typedef struct proton_bundles_node1 {
proton_bundle_produced_bundle_t produced_bundle_bundle;
proton_bundle_consumed_bundle_t consumed_bundle_bundle;
proton_bundle_heartbeat_t node1_heartbeat_bundle;
proton_bundle_heartbeat_t node2_heartbeat_bundle;
} proton_bundles_node1_t;
// Bundle Init Prototype
proton_status_e proton_bundle_produced_bundle_init(proton_bundle_produced_bundle_t * bundle);
proton_status_e proton_bundle_consumed_bundle_init(proton_bundle_consumed_bundle_t * bundle);
proton_status_e proton_bundle_node1_heartbeat_init(proton_bundle_heartbeat_t * bundle);
proton_status_e proton_bundle_node2_heartbeat_init(proton_bundle_heartbeat_t * bundle);
proton_status_e proton_bundles_node1_init(proton_bundles_node1_t * bundles);
// Bundle Decode Prototype
proton_status_e proton_bundle_decode(proton_buffer_t buffer, proton_producer_t producer, uint32_t * id, size_t length);
// Bundle Encode Function
proton_status_e proton_bundle_encode(proton_buffer_t buffer, uint32_t id, size_t * bytes_encoded);
// Bundle Print Prototype
void proton_bundle_print(PROTON__BUNDLE_e bundle);
// Peers
typedef enum PROTON__PEER {
PROTON__PEER__NODE2 = 0x0,
PROTON__PEER__COUNT = 0x1,
} PROTON__PEER_e;
// Bundle Receive Prototype
proton_status_e proton_node_node2_receive(proton_node_t * node, size_t length);
// Bundle Send Prototype
proton_status_e proton_bundle_send(proton_node_t * node, uint32_t id);
// Consumer callbacks
void proton_bundle_consumed_bundle_callback(void * context);
void proton_bundle_node2_heartbeat_callback(void * context);
// Transport Prototypes
proton_status_e proton_node_node2_transport_connect(void * context);
proton_status_e proton_node_node2_transport_disconnect(void * context);
proton_status_e proton_node_node2_transport_read(void * context, uint8_t * buf, size_t len, size_t * bytes_read);
proton_status_e proton_node_node2_transport_write(void * context, const uint8_t * buf, size_t len, size_t * bytes_written);
// Mutex prototypes
proton_status_e proton_node_node2_lock(void * context);
proton_status_e proton_node_node2_unlock(void * context);
proton_status_e proton_node_node1_lock(void * context);
proton_status_e proton_node_node1_unlock(void * context);
// Peer Init Prototype
proton_status_e proton_peer_node2_init(proton_peer_t * peer, proton_buffer_t buffer);
// Proton Node Init Prototype
proton_status_e proton_node_node1_init(proton_node_t * node, proton_peer_t * peers, proton_buffer_t buffer, void * context);
#ifdef __cplusplus
}
#endif
#endif // PROTONC__PROTON__SAMPLE_NODE1_H
File banner + header guard
- The top comment marks the file as generated.
#ifndef PROTONC__PROTON__SAMPLE_NODE1_H/#define .../#endifprevent multiple inclusion.
C/C++ linkage block
extern "C"makes the C API usable from C++ without name mangling.
Core includes
- Standard integer/bool types plus
protonc/node.hwhich defines the runtimeproton_node_t, transport, and bundle APIs the generated code targets.
Node definitions (local node)
- Macros describing the target node (
node1): name, ID, default node struct initializer, heartbeat defaults, and endpoint metadata (type/IP/port). - These macros are used by generated init helpers and by the application to configure node state consistently.
Node definitions (peers)
- Peer node (
node2) metadata and defaultproton_peer_tinitializer. - The peer’s default transport vtable uses function prototypes that you must implement (
proton_node_node2_transport_*).
Bundle ID enum
PROTON__BUNDLE_eassigns each bundle a numeric ID from the YAML (non-zero). Heartbeats usePROTON_HEARTBEAT_IDfrom the runtime library.
Signal enums per bundle
- One enum per bundle lists the signals in a stable order. These indices are used to address signal handles and to generate constant arrays with correct lengths.
Constant definitions
- Lengths/capacities for array/list/bytes/string signals.
- Default values for scalars and composite bundle defaults.
- Producer/consumer bitmasks encode which node IDs are allowed to send/receive each bundle.
Bundle struct definitions
- A
structper bundle that reflects the signal layout and types (e.g.,float[LEN],uint8_t[L][C]). - Constant signals become
constfields in the struct (read-only to user code).
Bundles aggregate struct
- A
proton_bundles_<target>_tthat groups all bundles (including heartbeat bundles) for the target node.
Bundle init prototypes
- Functions like
proton_bundle_<name>_initinitialize a bundle struct and bind signal handles. proton_bundles_<target>_initinitializes the full bundle set for the target.
Encode/decode and print prototypes
proton_bundle_encodeserializes a bundle into aproton_buffer_t.proton_bundle_decodedeserializes and validates producers/IDs.proton_bundle_printis a helper to print a bundle for debug.
Peer enum
PROTON__PEER_eassigns an index for each peer in thepeers[]array used by the runtime node.
Receive/send prototypes
proton_node_<peer>_receiveis called after transport read to decode and dispatch callbacks for that peer.proton_bundle_sendencodes and sends a bundle to all consumers.
Consumer callback prototypes
- Weak callbacks for each consumed bundle and heartbeat. You override these in your application to handle incoming data.
Transport prototypes
- Functions you must implement for each peer:
connect,disconnect,read,write. - The generated peer default transport vtable references these.
Mutex prototypes
- Optional lock/unlock hooks for atomic buffer access. If you don’t implement them, weak defaults return
PROTON_OK.
Peer and node init prototypes
proton_peer_<peer>_initinitializes peer state with its ID and defaults.proton_node_<target>_initconfigures and activates the node with peers, buffers, and callbacks.
Source file
The full generated source file can be viewed below:
Generated source file
/*
* Copyright 2026 Rockwell Automation Technologies, Inc., All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* THIS FILE WAS GENERATED BY THE PROTONC CODE GENERATOR.
*/
#include "proton__sample_node1.h"
// Internal Signals
static proton_signal_handle_t _produced_bundle_signal_handles[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__COUNT];
static proton_signal_handle_t _consumed_bundle_signal_handles[PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__COUNT];
static proton_signal_handle_t _node1_heartbeat_signal_handles[PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__COUNT];
static proton_signal_handle_t _node2_heartbeat_signal_handles[PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__COUNT];
// Internal Bundles
static proton_bundle_handle_t _produced_bundle_bundle_handle;
static proton_bundle_handle_t _consumed_bundle_bundle_handle;
static proton_bundle_handle_t _node1_heartbeat_bundle_handle;
static proton_bundle_handle_t _node2_heartbeat_bundle_handle;
// Bundle Init Functions
proton_status_e proton_bundle_produced_bundle_init(proton_bundle_produced_bundle_t * bundle)
{
proton_status_e status;
status = proton_init_signal(&_produced_bundle_signal_handles[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__INTEGER], proton_Signal_int32_value_tag, &bundle->integer, 0, 0);
if (status != PROTON_OK)
{
return status;
}
status = proton_init_signal(&_produced_bundle_signal_handles[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS], proton_Signal_list_float_value_tag, bundle->floats, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__FLOATS__LENGTH, 0);
if (status != PROTON_OK)
{
return status;
}
status = proton_init_signal(&_produced_bundle_signal_handles[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME], proton_Signal_string_value_tag, bundle->name, 0, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__CAPACITY);
if (status != PROTON_OK)
{
return status;
}
status = proton_init_signal(&_produced_bundle_signal_handles[PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES], proton_Signal_list_bytes_value_tag, bundle->hex_codes, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__LENGTH, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__HEX_CODES__CAPACITY);
if (status != PROTON_OK)
{
return status;
}
return proton_init_bundle(&_produced_bundle_bundle_handle, PROTON__BUNDLE__PRODUCED_BUNDLE, _produced_bundle_signal_handles, PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__COUNT, PROTON__BUNDLE__PRODUCED_BUNDLE__PRODUCERS, PROTON__BUNDLE__PRODUCED_BUNDLE__CONSUMERS);
}
proton_status_e proton_bundle_consumed_bundle_init(proton_bundle_consumed_bundle_t * bundle)
{
proton_status_e status;
status = proton_init_signal(&_consumed_bundle_signal_handles[PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS], proton_Signal_list_bool_value_tag, bundle->booleans, PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__BOOLEANS__LENGTH, 0);
if (status != PROTON_OK)
{
return status;
}
return proton_init_bundle(&_consumed_bundle_bundle_handle, PROTON__BUNDLE__CONSUMED_BUNDLE, _consumed_bundle_signal_handles, PROTON__BUNDLE__CONSUMED_BUNDLE__SIGNAL__COUNT, PROTON__BUNDLE__CONSUMED_BUNDLE__PRODUCERS, PROTON__BUNDLE__CONSUMED_BUNDLE__CONSUMERS);
}
proton_status_e proton_bundle_node1_heartbeat_init(proton_bundle_heartbeat_t * bundle)
{
proton_status_e status;
status = proton_init_signal(&_node1_heartbeat_signal_handles[PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__HEARTBEAT], proton_Signal_uint32_value_tag, &bundle->heartbeat, 0, 0);
if (status != PROTON_OK)
{
return status;
}
return proton_init_bundle(&_node1_heartbeat_bundle_handle, PROTON_HEARTBEAT_ID, _node1_heartbeat_signal_handles, PROTON__BUNDLE__NODE1_HEARTBEAT__SIGNAL__COUNT, PROTON__BUNDLE__NODE1_HEARTBEAT__PRODUCERS, PROTON__BUNDLE__NODE1_HEARTBEAT__CONSUMERS);
}
proton_status_e proton_bundle_node2_heartbeat_init(proton_bundle_heartbeat_t * bundle)
{
proton_status_e status;
status = proton_init_signal(&_node2_heartbeat_signal_handles[PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__HEARTBEAT], proton_Signal_uint32_value_tag, &bundle->heartbeat, 0, 0);
if (status != PROTON_OK)
{
return status;
}
return proton_init_bundle(&_node2_heartbeat_bundle_handle, PROTON_HEARTBEAT_ID, _node2_heartbeat_signal_handles, PROTON__BUNDLE__NODE2_HEARTBEAT__SIGNAL__COUNT, PROTON__BUNDLE__NODE2_HEARTBEAT__PRODUCERS, PROTON__BUNDLE__NODE2_HEARTBEAT__CONSUMERS);
}
proton_status_e proton_bundles_node1_init(proton_bundles_node1_t * bundles)
{
proton_status_e status;
status = proton_bundle_produced_bundle_init(&bundles->produced_bundle_bundle);
if (status != PROTON_OK)
{
return status;
}
status = proton_bundle_consumed_bundle_init(&bundles->consumed_bundle_bundle);
if (status != PROTON_OK)
{
return status;
}
status = proton_bundle_node1_heartbeat_init(&bundles->node1_heartbeat_bundle);
if (status != PROTON_OK)
{
return status;
}
status = proton_bundle_node2_heartbeat_init(&bundles->node2_heartbeat_bundle);
if (status != PROTON_OK)
{
return status;
}
return PROTON_OK;
}
// Bundle Decode Function
proton_status_e proton_bundle_decode(proton_buffer_t buffer, proton_producer_t producer, uint32_t * id, size_t length)
{
// Check that buffer is not NULL
if (buffer.data == NULL || id == NULL)
{
return PROTON_NULL_PTR_ERROR;
}
proton_status_e status;
proton_bundle_handle_t * handle;
// Decode bundle ID
status = proton_decode_id(buffer, id);
if (status != PROTON_OK)
{
return status;
}
switch (*id)
{
case PROTON__BUNDLE__PRODUCED_BUNDLE:
{
handle = &_produced_bundle_bundle_handle;
break;
}
case PROTON__BUNDLE__CONSUMED_BUNDLE:
{
handle = &_consumed_bundle_bundle_handle;
break;
}
case PROTON_HEARTBEAT_ID:
{
switch (producer)
{
case PROTON__NODE__NODE2__ID:
{
handle = &_node2_heartbeat_bundle_handle;
break;
}
case PROTON__NODE__NODE1__ID:
{
handle = &_node1_heartbeat_bundle_handle;
break;
}
default:
{
return PROTON_ERROR;
}
}
break;
}
default:
{
return PROTON_ERROR;
}
}
// Check that the producer produces this bundle
if (!(handle->producers & producer))
{
return PROTON_ERROR;
}
// Decode bundle
status = proton_decode(handle, buffer, length);
if (status != PROTON_OK)
{
return status;
}
return PROTON_OK;
}
// Bundle Encode Function
proton_status_e proton_bundle_encode(proton_buffer_t buffer, uint32_t id, size_t * bytes_encoded)
{
// Check that bytes_encoded is not NULL
if (bytes_encoded == NULL)
{
return PROTON_NULL_PTR_ERROR;
}
proton_bundle_handle_t * handle;
// Find correct bundle handle
switch (id)
{
case PROTON__BUNDLE__PRODUCED_BUNDLE:
{
handle = &_produced_bundle_bundle_handle;
break;
}
case PROTON_HEARTBEAT_ID:
{
handle = &_node1_heartbeat_bundle_handle;
break;
}
default:
{
return PROTON_ERROR;
}
}
return proton_encode(handle, buffer, bytes_encoded);
}
// Bundle Send Function
void proton_bundle_print(PROTON__BUNDLE_e bundle)
{
proton_bundle_handle_t * handle;
switch (bundle)
{
case PROTON__BUNDLE__PRODUCED_BUNDLE:
{
handle = &_produced_bundle_bundle_handle;
break;
}
case PROTON__BUNDLE__CONSUMED_BUNDLE:
{
handle = &_consumed_bundle_bundle_handle;
break;
}
default:
{
return;
}
}
// Print bundle
proton_print_bundle(handle->bundle);
}
// Bundle Receive Function
proton_status_e proton_node_node2_receive(proton_node_t * node, size_t length)
{
// Check that the node is not NULL
if (node == NULL || node->peers == NULL || node->peer_count <= PROTON__PEER__NODE2)
{
return PROTON_NULL_PTR_ERROR;
}
uint32_t id;
proton_callback_t callback;
proton_status_e status;
proton_peer_t peer = node->peers[PROTON__PEER__NODE2];
// Check that peer mutex functions are not NULL
if (peer.state == PROTON_NODE_UNCONFIGURED)
{
return PROTON_INVALID_STATE_ERROR;
}
// Decode bundle
status = proton_bundle_decode(peer.atomic_buffer.buffer, peer.id, &id, length);
if (status != PROTON_OK)
{
return status;
}
switch (id)
{
case PROTON__BUNDLE__CONSUMED_BUNDLE:
{
callback = proton_bundle_consumed_bundle_callback;
break;
}
case PROTON_HEARTBEAT_ID:
{
callback = proton_bundle_node2_heartbeat_callback;
break;
}
default:
{
return PROTON_ERROR;
}
}
// Execute callback
if (callback)
{
callback(node->context);
}
return PROTON_OK;
}
// Bundle Send Function
proton_status_e proton_bundle_send(proton_node_t * node, uint32_t id)
{
// Check that peers are not NULL
if (node == NULL || node->peers == NULL)
{
return PROTON_NULL_PTR_ERROR;
}
// Check that write functions are not NULL
for (int i = 0; i < node->peer_count; i += 1)
{
if (node->peers[i].transport.write == NULL)
{
return PROTON_NULL_PTR_ERROR;
}
}
// Check that node is connected
if (node->state != PROTON_NODE_ACTIVE)
{
return PROTON_INVALID_STATE_ERROR;
}
proton_bundle_handle_t * handle;
proton_status_e status;
size_t bytes_encoded;
size_t bytes_written;
// Find correct bundle handle
switch (id)
{
case PROTON__BUNDLE__PRODUCED_BUNDLE:
{
handle = &_produced_bundle_bundle_handle;
break;
}
case PROTON_HEARTBEAT_ID:
{
handle = &_node1_heartbeat_bundle_handle;
break;
}
default:
{
return PROTON_ERROR;
}
}
status = node->atomic_buffer.lock(node->context);
if (status == PROTON_OK)
{
status = proton_bundle_encode(node->atomic_buffer.buffer, id, &bytes_encoded);
if (status == PROTON_OK)
{
// Send bundle
for (int i = 0; i < node->peer_count; i += 1)
{
// Send to all bundle consumers
if (handle->consumers & node->peers[i].id)
{
proton_status_e write_status = node->peers[i].transport.write(node->context, node->atomic_buffer.buffer.data, bytes_encoded, &bytes_written);
if (write_status != PROTON_OK)
{
status = write_status;
}
}
}
}
status = node->atomic_buffer.unlock(node->context);
}
return status;
}
// Weak Consumer callbacks
__attribute__((weak)) void proton_bundle_consumed_bundle_callback(void * context)
{
(void)context;
}
__attribute__((weak)) void proton_bundle_node2_heartbeat_callback(void * context)
{
(void)context;
}
// Weak Mutex functions
__attribute__((weak)) proton_status_e proton_node_node2_lock(void * context)
{
(void)context;
return PROTON_OK;
}
__attribute__((weak)) proton_status_e proton_node_node2_unlock(void * context)
{
(void)context;
return PROTON_OK;
}
__attribute__((weak)) proton_status_e proton_node_node1_lock(void * context)
{
(void)context;
return PROTON_OK;
}
__attribute__((weak)) proton_status_e proton_node_node1_unlock(void * context)
{
(void)context;
return PROTON_OK;
}
// Peer Init Functions
proton_status_e proton_peer_node2_init(proton_peer_t * peer, proton_buffer_t buffer)
{
return proton_init_peer(peer, PROTON__NODE__NODE2__ID, (proton_heartbeat_t)PROTON__NODE__NODE2__HEARTBEAT__DEFAULT_VALUE, (proton_transport_t)PROTON__NODE__NODE2__TRANSPORT__DEFAULT_VALUE, proton_node_node2_receive, proton_node_node2_lock, proton_node_node2_unlock, buffer);
}
// Proton Node Init
proton_status_e proton_node_node1_init(proton_node_t * node, proton_peer_t * peers, proton_buffer_t buffer, void * context)
{
// Check that the node is not NULL
if (node == NULL || peers == NULL)
{
return PROTON_NULL_PTR_ERROR;
}
proton_status_e status;
status = proton_configure(node, (proton_heartbeat_t)PROTON__NODE__NODE1__HEARTBEAT__DEFAULT_VALUE, proton_node_node1_lock, proton_node_node1_unlock, buffer, peers, PROTON__PEER__COUNT, context);
if (status != PROTON_OK)
{
return status;
}
return proton_activate(node);
}
Internal signal handle arrays
static proton_signal_handle_t _<bundle>_signal_handles[...]are per-bundle storage for signal metadata (type, length, capacity) used by the runtime encoder/decoder.
Internal bundle handle instances
static proton_bundle_handle_t _<bundle>_bundle_handleare runtime handles that bind bundle IDs, signal handles, and producer/consumer masks.
Per-bundle init functions
- Functions like
proton_bundle_produced_bundle_initcallproton_init_signalfor each signal in order, thenproton_init_bundleto assemble the bundle handle.
Aggregate init function
proton_bundles_<target>_initinitializes all bundles (including heartbeat bundles) for the target node.
Bundle decode
proton_bundle_decodereads the bundle ID, selects the correct handle (including heartbeat by producer), validates producer permissions, then callsproton_decode.
Bundle encode
proton_bundle_encodechooses the appropriate handle by bundle ID and callsproton_encodeto serialize into a buffer.
Bundle print helper
proton_bundle_printselects the handle and forwards toproton_print_bundlefor debugging.
Peer receive function
proton_node_<peer>_receivedecodes a buffer received from a peer, chooses the correct callback, and invokes it.
Bundle send function
proton_bundle_sendencodes the bundle, then writes it to all peers that are listed as consumers, with atomic buffer locking.
Weak consumer callbacks
- Default (weak) callbacks for incoming bundles and heartbeats; you override these in application code to process data.
Weak mutex hooks
- Default (weak) lock/unlock functions used by the atomic buffer; replace if your platform needs real locking.
Peer init function
proton_peer_<peer>_initfills a peer struct with IDs, heartbeat defaults, transport vtable, callbacks, and buffer.
Node init function
proton_node_<target>_initconfigures the node with heartbeat settings, peers, buffers, and then activates it.
Usage
Once proton has been included into your project, you can begin to define proton structs and buffers to be used by your application.
Core implementation
If you generate protonc code with NODE set to False, you will only have the core encode and decode functionality. Transport and node management will be up to your application to implement. protonc will only enable you to encode and decode Bundles to and from data buffers.
Bundle structs
Bundle structs can be defined and initialized individually, or as part of the aggregate proton_bundles_<target>_t struct, which is recommended.
Bundles should be initialized with the default value initialiser list generated in the header file, and the generated init function.
proton_bundles_node1_t bundles = PROTON__BUNDLES__NODE1__DEFAULT_VALUE;
proton_bundles_node1_init(&bundles);
Always check the return value of proton functions to ensure no errors occurred.
The individual Bundle structs within bundles will now be bound to the corresponding _<bundle_name>_bundle_handle struct defined in the source file, and will be used by proton for encoding/decoding.
Buffers
protonc uses the proton_buffer_t struct for data buffers. It is defined as:
typedef struct {
uint8_t * data;
size_t len;
} proton_buffer_t;
Typically you should create one buffer for encoding, and another for decoding. They should be of sufficient size to encode any of your Bundles.
uint8_t encode_buffer[ENCODE_BUFFER_SIZE];
proton_buffer_t proton_encode_buffer = {encode_buffer, sizeof(encode_buffer)};
Encoding a Bundle
If you are just using the core functionality of protonc, then at this point you can fill out the Bundle data, and manually use the generated proton_bundle_encode function with your encode buffer to encode your Bundle.
bundles.produced_bundle_bundle.integer = 12;
bundles.produced_bundle_bundle.floats[3] = -12.53f;
strncpy(bundles.produced_bundle_bundle.name, "NAME", PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__CAPACITY);
bundles.produced_bundle_bundle.hex_codes[4][0] = 0xFF;
size_t bytes_encoded = 0;
if (proton_bundle_encode(proton_encode_buffer, PROTON__BUNDLE__PRODUCED_BUNDLE, &bytes_encoded) == PROTON_OK)
{
// Send the first bytes_encoded bytes of encode_buffer
}
Decoding a Bundle
The decoding process requires knowing which peer sent the Bundle, and what the length of the received serialized packet is. The decode function will also indicate the ID of the received Bundle.
uint32_t id = 0;
size_t packet_length = 0;
uint8_t decode_buffer[DECODE_BUFFER_SIZE];
proton_buffer_t proton_decode_buffer = {decode_buffer, sizeof(decode_buffer)};
// Receive packet_length bytes into decode_buffer
if (proton_bundle_decode(proton_decode_buffer, PROTON__NODE__NODE2__ID, &id, packet_length) == PROTON_OK)
{
switch(id)
{
case PROTON__BUNDLE__CONSUMED_BUNDLE:
{
// Handle receiving consumed_bundle
break;
}
case PROTON_HEARTBEAT_ID:
{
// Handle receiving heartbeat from node2
break;
}
default:
{
break;
}
}
}
Node implementation
If generated with NODE set to True, your application will have to define the transport layer, Bundle callbacks, and run the node spin function periodically. Bundle encoding and decoding will be handled automatically.
Context
It is recommended to create a custom context struct which holds your node and bundle structs. This context will then be passed to transport, mutex, and callback calls when spinning your node, allowing direct access to Bundle data, or other useful information.
typedef struct {
proton_node_t node;
proton_bundles_node1_t bundles;
} context_t;
Initialisation
On top of initialising Bundle Structs, you will need to also initialise the target node, and each of its peers. Use the default value initialiser list for each:
// Define all structs and variables
proton_peer_t node1_peers[PROTON__PEER__COUNT] = {PROTON__NODE__NODE2__PEER__DEFAULT_VALUE};
context_t context = {
.node = PROTON__NODE__NODE1__DEFAULT_VALUE,
.bundles = PROTON__BUNDLES__NODE1__DEFAULT_VALUE
};
uint8_t decode_buffer[DECODE_BUFFER_SIZE];
proton_buffer_t proton_decode_buffer = {decode_buffer, sizeof(decode_buffer)};
uint8_t encode_buffer[ENCODE_BUFFER_SIZE];
proton_buffer_t proton_encode_buffer = {encode_buffer, sizeof(encode_buffer)};
// Initialise Bundles
proton_bundles_mcu_init(&context.bundles);
// Initialise Peers
proton_peer_node2_init(&node1_peers[PROTON__PEER__NODE2], proton_decode_buffer);
// Initialise Node
proton_node_mcu_init(&context.node, node1_peers, proton_encode_buffer, &context);
Transport
The transport functions for each peer must be defined in your application source. They should adhere to the function prototypes, and return an appropriate status. The read and write functions must correctly populate the bytes_read or bytes_written variables on success.
proton_status_e proton_node_node2_transport_connect(void * context);
proton_status_e proton_node_node2_transport_disconnect(void * context);
proton_status_e proton_node_node2_transport_read(void * context, uint8_t * buf, size_t len, size_t * bytes_read);
proton_status_e proton_node_node2_transport_write(void * context, const uint8_t * buf, size_t len, size_t * bytes_written);
The Proton node does handle transport timeouts. These should be implemented within your transport functions.
Serial Transport
For serial transport, the read and write functions must implement Proton's framing to correctly extract the serialized Bundle from the serial stream, and correctly send a Bundle to another peer. The protoncpp implementation of serial transport can be used as reference.
Thread Safety
The protonc generator will also generate optional mutex lock and unlock functions. These can also be implemented, using platform specific mutexes, to provide atomic access to the encode and decode buffers.
proton_status_e proton_node_node2_lock(void * context);
proton_status_e proton_node_node2_unlock(void * context);
proton_status_e proton_node_node1_lock(void * context);
proton_status_e proton_node_node1_unlock(void * context);
Callbacks
Each Bundle that is defined as being consumed by the target node will have a generated callback function. These are called every time the corresponding Bundle is decoded. Your application should implement these as required.
void proton_bundle_consumed_bundle_callback(void * context)
{
if (context == NULL) return;
context_t * c = (context_t *)context;
if (c->bundles.consumed_bundle_bundle.booleans[c->bundles.consumed_bundle_bundle.BOOL0])
{
// Do something
}
}
Spinning a Peer
Once all proton structs have been initialised and transport and callback functions have been defined, the proton_spin or proton_spin_once functions can be used to run the automatic transport and Bundle management of a peer.
The proton_spin_once function runs the peers state machine, utilising the transport functions to manage the transport connection, and handling the reception of Bundles from this peer. proton_spin is simply an infinite loop that repeatedly calls proton_spin_once, and so it is a blocking call intended to run in its own thread or task.
When the peers transport is in a connected state, the proton_spin_once function will call the transport read function to receive serialized Bundles from the peer, decode their IDs and data into the appropriate Bundle structs, and execute the Bundle's callback. If mutexes are implemented, it will ensure thread safety around the decode buffer.
proton_spin_once(&context.node, PROTON__PEER__NODE2);
Producing Bundles
While spinning a peer is used for handling Bundle reception, the production of Bundles occurs asynchronously. The proton_bundle_send function should be used to automatically encode and send a Bundle to each of its consumers.
The Bundle struct that was initialised should be filled out with the appropriate data, then proton_bundle_send should be called with the Bundles ID.
context.bundles.produced_bundle_bundle.integer = 12;
context.bundles.produced_bundle_bundle.floats[3] = -12.53f;
strncpy(context.bundles.produced_bundle_bundle.name, "NAME", PROTON__BUNDLE__PRODUCED_BUNDLE__SIGNAL__NAME__CAPACITY);
context.bundles.produced_bundle_bundle.hex_codes[4][0] = 0xFF;
proton_bundle_send(&context.node, PROTON__BUNDLE__PRODUCED_BUNDLE);
Heartbeats
Heartbeats can be enabled on a per-node basis in the Proton configuration file:
nodes:
- name: node1
heartbeat:
enabled: true
period: 1000
The period is the expected rate at which heartbeats will be produced by this node in milliseconds.
In protonc, a proton_bundle_heartbeat_t struct will be generated for each node that enables the heartbeat. They can be initialised the same way as any other Bundle. The heartbeat bundle is unique in that it uses the ID 0 (defined as PROTON_HEARTBEAT_ID), and has a single predefined heartbeat signal of type uint32. It is expected for the application to increment the heartbeat signal each time it is produced.
Heartbeats can be produced the same way as any other Bundle:
context.bundles.node1_heartbeat_bundle.heartbeat++;
proton_bundle_send(&context.node, PROTON_HEARTBEAT_ID);
and similarly received in a callback:
void proton_bundle_node2_heartbeat_callback(void * context)
{
if (context == NULL) return;
context_t * c = (context_t *)context;
printf("Received Node 2 heartbeat %ul\r\n", c->bundles.node2_heartbeat_bundle.heartbeat);
}