Skip to main content

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:

FilePurpose
protonc_generator.pyMain code generation logic
config.pyYAML parsing and configuration representation
source_writer.pyHelper 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)
note

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 ... / #endif prevent 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.h which defines the runtime proton_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 default proton_peer_t initializer.
  • The peer’s default transport vtable uses function prototypes that you must implement (proton_node_node2_transport_*).

Bundle ID enum

  • PROTON__BUNDLE_e assigns each bundle a numeric ID from the YAML (non-zero). Heartbeats use PROTON_HEARTBEAT_ID from 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 struct per bundle that reflects the signal layout and types (e.g., float[LEN], uint8_t[L][C]).
  • Constant signals become const fields in the struct (read-only to user code).

Bundles aggregate struct

  • A proton_bundles_<target>_t that groups all bundles (including heartbeat bundles) for the target node.

Bundle init prototypes

  • Functions like proton_bundle_<name>_init initialize a bundle struct and bind signal handles.
  • proton_bundles_<target>_init initializes the full bundle set for the target.

Encode/decode and print prototypes

  • proton_bundle_encode serializes a bundle into a proton_buffer_t.
  • proton_bundle_decode deserializes and validates producers/IDs.
  • proton_bundle_print is a helper to print a bundle for debug.

Peer enum

  • PROTON__PEER_e assigns an index for each peer in the peers[] array used by the runtime node.

Receive/send prototypes

  • proton_node_<peer>_receive is called after transport read to decode and dispatch callbacks for that peer.
  • proton_bundle_send encodes 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>_init initializes peer state with its ID and defaults.
  • proton_node_<target>_init configures 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_handle are runtime handles that bind bundle IDs, signal handles, and producer/consumer masks.

Per-bundle init functions

  • Functions like proton_bundle_produced_bundle_init call proton_init_signal for each signal in order, then proton_init_bundle to assemble the bundle handle.

Aggregate init function

  • proton_bundles_<target>_init initializes all bundles (including heartbeat bundles) for the target node.

Bundle decode

  • proton_bundle_decode reads the bundle ID, selects the correct handle (including heartbeat by producer), validates producer permissions, then calls proton_decode.

Bundle encode

  • proton_bundle_encode chooses the appropriate handle by bundle ID and calls proton_encode to serialize into a buffer.

Bundle print helper

  • proton_bundle_print selects the handle and forwards to proton_print_bundle for debugging.

Peer receive function

  • proton_node_<peer>_receive decodes a buffer received from a peer, chooses the correct callback, and invokes it.

Bundle send function

  • proton_bundle_send encodes 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>_init fills a peer struct with IDs, heartbeat defaults, transport vtable, callbacks, and buffer.

Node init function

  • proton_node_<target>_init configures 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);
note

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);
note

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);
}