Exchange structured data between Arduino boards.
- 💪 Cross-device strong data typing
- 📦 Send / receive either structs or scalar types
- 🔌 Use any Stream-based interface (HardwareSerial, SoftwareSerial, I2C, SPI...)
- 🔒 Internal checksum for data integrity verification
↔️ Receive and send different types on the same stream (example)- ⚡ Advanced command processing with Commander
Use the Arduino Library Manager to install the library.
- Define shared data (using a weather station as an example):
// libraries/weather-shared/src/sensor-data.h
#pragma once
struct SensorData
{
float temperature;
float humidity;
float latitude;
float longitude;
unsigned long time;
};
- Send it from one device:
#include <serde.h>
#include <sensor-data.h>
using SerdeTX = Serde<SensorData>;
void setup()
{
Serial1.begin(115200);
}
void loop()
{
SensorData data = getSensorData();
data.time = millis();
SerdeTX::send(data, Serial1);
}
- Receive it on another device:
#include <serde.h>
#include <sensor-data.h>
using SerdeRX = Serde<SensorData>;
// This is called when new data is available
void recordSensorData(const SensorData& data)
{
recordWeather(data.temperature, data.humidity);
recordPosition(data.latitude, data.longitude);
logTime(data.time);
}
void setup()
{
Serial1.begin(115200);
}
void loop()
{
// Pass it the serial port to read from and a callback:
SerdeRX::read(Serial1, recordSensorData);
// Or you can also check manually for incoming data:
// SensorData data;
// if (SerdeRX::receive(Serial1, data))
// {
// recordSensorData(data);
// }
}
This library requires both emitter and receiver to use the same data types.
Copying the definitions back and forth is cumbersome and will lead to errors, so sharing the type definitions between sketches is essential.
The best way to share files between sketches is to create a library.
- Go to the Arduino libraries directory:
~/Documents/Arduino/libraries
on macOS and Linux~\Documents\Arduino\libraries
on Windows
- Create a new directory, name it as you wish, for example:
weather-shared
- Create a subdirectory
src
inweather-shared
- Place your definitions in a header file in
src
, likeweather-shared/src/sensor-data.h
- Create a
library.properties
file inweather-shared
:
name=WeatherShared # This name is only used for published libraries
includes=sensor-data.h # The name of the file where you placed your definitions
# The rest is required by Arduino:
version=0.0.1
author=
maintainer=
sentence=
paragraph=
category=Uncategorized
architectures=*
You can now include your definitions as such:
#include <serde.h>
#include <sensor-data.h>
using SerdeTX = Serde<SensorData>;
- Both sending and receiving devices must have the same endianness.
- You can only exchange Plain Old Data (POD) objects, nothing allocated or which size is unknown at compile time.
Because strings are of an arbitrary, runtime-defined length, you will have
to send a char
buffer of a fixed length, capable of containing the
largest string you need (size it appropriately, as all messages will be
this big), plus one byte for
null-termination.
To send a string:
struct Message
{
// Can hold at maximum 31 characters + 1 null terminator
char text[32] = { 0 };
};
Message message;
memset(message.text, 0, sizeof(Message::text)); // clear
memcpy(message.text, "Hello, World !", 14); // copy
Serde<Message>::send(message, Serial);
There is nothing particular to do after reception, just use it:
Message message;
if (Serde<Message>::receive(Serial1, message))
{
Serial.println(message.text);
}
- When calling
receive
, you can be sure that the object passed will never have been changed ifreceive
returns false. - Since Arduino is single-threaded, there should be no race conditions
when
receive
returns true, however interrupts can be seen as threads, so if using the received value in an interrupt handler, be sure to know that it could be in the middle of an update.
Commander is a Remote Procedure Call layer built on top of Serde to handle multiple types of messages (commands) being sent and received, with different attributes:
#include <serde-commander.h>
// 1. Define a structure for each of
// the commands you want to receive:
struct SayHello
{
char name[32];
};
struct SetPinState
{
byte pinNumber;
bool state;
};
// 2. Create a CommanderRX interface and
// list the commands it will handle:
SERDE_COMMANDER_CREATE_RX(CommanderRX,
SayHello,
SetPinState
);
// 3. Create a function for each command
// with the following signature (required):
// void on{CommandName}Received(const CommandName&)
// It will be called automatically when the
// corresponding command is received.
void onSayHelloReceived(const SayHello& data)
{
Serial.print("Hello, ");
Serial.println(data.name);
}
void onSetPinStateReceived(const SetPinState& data)
{
digitalWrite(data.pinNumber, data.state ? HIGH : LOW);
}
// --
void setup()
{
Serial.begin(115200);
Serial1.begin(115200);
}
void loop()
{
// Just pass it where to read from:
CommanderRX::read(Serial1);
}
To send commands:
#include <serde-commander.h>
// Those structs would be defined in a shared file
struct SayHello
{
char name[32];
};
struct SetPinState
{
byte pinNumber;
bool state;
};
// 2. Create a CommanderTX interface and
// list the commands it may send:
SERDE_COMMANDER_CREATE_TX(CommanderTX,
SayHello,
SetPinState
);
// No need to define callbacks for Commander TX
// They are only required if you want to receive
// commands (RX-only or bidirectional).
// --
void setup()
{
Serial1.begin(115200);
}
void loop()
{
// Create commands and send them:
SayHello hello;
memset(hello.name, 0, sizeof(SayHello::name));
memcpy(hello.name, "Commander", 9);
CommanderTX::send(hello, Serial1);
SetPinState pinState;
pinState.pinNumber = 13;
pinState.state = true;
CommanderTX::send(pinState, Serial1);
delay(100);
pinState.state = false;
CommanderTX::send(pinState, Serial1);
delay(100);
}
There are 3 Commander creation macros:
SERDE_COMMANDER_CREATE_TX
: Send-only (no need to implement the callbacks for this one).SERDE_COMMANDER_CREATE_RX
: Receive-only (callbacks required)SERDE_COMMANDER_CREATE
: Both send and receive the same commands (callbacks required)
Checkout the Commander examples for more details:
Commander currently supports up to 8 commands. If you need more, please
open a PR on
src/serde-macros.h
.
Also please note that a Commander message will not be compatible with a plain Serde message (and vice-versa).
You can have one side of the communication handled by Commander, and the "reply" handled by Serde (as done in the CommanderCalculator example), as long as both devices use the right types:
Device A Device B
----------- -----------
CommanderTX -> CommanderRX
SerdeRX <- SerdeTX
But these connections won't work:
CommanderTX -> SerdeRX
or
SerdeTX -> CommanderRX
Inspired from Rust's awesome crate serde. 🦀
MIT - Made with ❤️ by François Best.