BeeBus

BeeBus

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

BeeBus

BeeBus

A peer-to-peer protocol for devices that need to talk to each other.

No master. No broker. No polling. You bind a variable, somebody else asks for
it, and it arrives — on the terms they asked for.

Runs on Arduino and as a plain C++ library on Linux, macOS and Windows. Over
RS485, over radio, over UDP.


If you know MQTT and Modbus, you already know BeeBus

BeeBus carries two data models over the same wire, and you can use both at
once on the same node — even on the same variable.

Topics — MQTT, without the broker

The preferred way to move data, and the one to reach for first.

beebus.addInputTopic("temperature", 1, &temperature);   // I publish this
beebus.subscribe(1, "temperature", 0.5f, 60000);        // Tell me when it moves

If you have used MQTT, this is the same idea: named values, publish and
subscribe, per-subscriber terms. What is different:

MQTT BeeBus topics
A broker every message passes through No broker. Nodes talk to each other directly
Subscribe to a topic, get whatever arrives Subscribe on your terms — a deadband and a maximum interval you choose, per subscriber
Payload is bytes; both ends must agree what they mean Values carry their type, checked at the far end
A value that stops arriving looks the same as one that has not changed topicQuality() tells you Good / Uncertain / Bad against the interval you asked for
QoS 1/2 need a session and a broker to track them Gap detection is built in — every publish is sequenced, so a loss is reported
Offline messages need a persistent session bufferTopic() stores and forwards with the true age of every sample

Registers — stock Modbus, for everything else

For the things that are not BeeBus: a SCADA package, a PLC, an HMI, a Modbus
TCP gateway.

beebus.addHreg(40, &setpoint);          // Peers may set it
beebus.writeHreg(1, 40, 750);           // Write another node's register

Stock function codes, stock addressing. Point a Modbus tool at a gateway and it
sees an ordinary register map. Unlike classic Modbus there is no master —
any node reads or writes any other.

Use topics for your own devices, registers where something else needs to
read you.
The same C++ variable can be both: your nodes see a typed value
with quality, the SCADA sees register 40, and nothing translates between them.


It does the hard part for you

You call writeHreg() or publishAll() and get on with your application. Between
your call and the wire, and with no configuration at all, the node:

  • Stacks the work. Operations queue up as you make them. Nothing blocks and
    nothing is sent immediately — it waits for this node's turn with the token.
  • Discards what is redundant. Queue the same write twice and the second is
    dropped. Ask for the same read twice and so is that.
  • Sends only the latest. Write a setpoint faster than the token comes round
    and the queued value is replaced, not stacked behind. A control loop moving
    a setpoint every scan sends the value it settled on, not a history of every
    step. Whoever was waiting on the replaced write is told, rather than left
    hanging.
  • Packs one frame. When the token arrives, everything outstanding —
    register writes, reads, topic publishes, replies to other nodes, for several
    different destinations
    — goes into a single frame. One turn on the bus, not
    one per operation.
  • Times itself. Every timeout is a number of token rotations, and a rotation
    is measured. The same code suits a 115200 wired segment and a long-range radio
    that takes two seconds to come round.
  • Retries and reports. A confirmed write is retried until it lands or gives
    up, and either way you are told which.

There is nothing to tune. BeeBusConfig exists so you can pin something on a
segment that demonstrably needs it, and a node started with no config at all is
a fully working node.


The problem

Put four devices on one RS485 pair and you immediately have three problems that
have nothing to do with what you were building.

Who is allowed to talk? The usual answer is a master that polls everybody.
Now the master is a single point of failure, every reading is as old as the poll
cycle, and two field devices cannot talk to each other at all.

How fast is fast enough? Pick timeouts for a wired segment and they expire
constantly over a radio. Pick them for the radio and a wired fault takes half a
minute to notice. And the right answer changes when you add a node.

What do these bytes mean? A Modbus register is sixteen anonymous bits.
Nothing on the wire says whether it is an integer, half a float, or two
characters of a fault message. That lives in a spreadsheet, and the spreadsheet
is always slightly wrong.

What BeeBus does about it

Nobody is in charge. Nodes find each other and form a ring, sorted by node
ID. A token goes round it. Any node can leave or arrive at any time and the ring
closes over the change by itself — including the lowest-numbered one, because
there is nothing special about it.

The network times itself. Every timeout is a number of token rotations, and
a rotation is measured. Add a node, move to a slower link, and everything
adjusts on its own.

Values carry their own type. You bind a float and a float is what travels.
The other end checks it against what it bound and refuses a mismatch — so the
spreadsheet stops being the thing that defines the wire.


Quick start: topics

What it solves: getting a value from one node to another, continuously,
without polling — and knowing whether you should still believe it.

The node with the value:

#include <BeeBusGlobal.h>

BeeBusAPI beebus;
float temperature = 0.0f;

void setup() {
    Serial1.begin(115200);
    beebus.begin(&Serial1, 1, BeeBusConfig(), 115200);      // I am node 1

    beebus.addInputTopic("temperature", 1, &temperature);   // Here is my data

    beebus.connect();
}

void loop() {
    beebus.loop();
    temperature = readSensor();                             // Your own code. That is all
}

The node that wants it:

#include <BeeBusGlobal.h>

BeeBusAPI beebus;
float remoteTemp = 0.0f;

void setup() {
    Serial1.begin(115200);
    beebus.begin(&Serial1, 2, BeeBusConfig(), 115200);      // I am node 2

    beebus.registerTopic(1, "temperature", 1, &remoteTemp); // Node 1's data, here

    beebus.onMemberJoined([](uint8_t node) {
        if (node == 1) {
            // Tell me when it moves 0.5°, and in any case once a minute
            beebus.subscribe(1, "temperature", 0.5f, 60000);
        }
    });

    beebus.connect();
}

void loop() {
    beebus.loop();

    // remoteTemp now tracks node 1. Nothing polls it.
    if (beebus.topicQuality(1, "temperature") == BeeBusQuality::Good) {
        control(remoteTemp);                                // Safe to act on
    }
}

That is the whole thing. No polling loop, no message building, no timeout
constants, no master.

You also get, for free:

  • topicQuality() — Good, Uncertain, Bad, judged against the interval
    you asked for. A variable holding 21.5 tells you nothing about whether 21.5
    is still true.

  • Gap reporting — every update carries a sequence number, so a lost one is
    reported, not merely absent.

  • Store-and-forward — beebus.bufferTopic("temperature", 60) and a node that
    goes offline delivers its history later, in order, with the true age of every
    sample attached.

  • Text, not just numbers — bind a char buffer and a topic carries a fault
    message. The length travels with it, so nothing at the far end has to know how
    long it is:

    char fault[32] = "";
    beebus.addInputTopic("fault", 20, fault, sizeof(fault));
    strcpy(fault, "pump 2 overload");
    

Quick start: Modbus registers

What it solves: letting something that is not BeeBus see your data — a SCADA
package, a PLC, an HMI, a Modbus TCP gateway.

#include <BeeBusGlobal.h>

BeeBusAPI beebus;

bool    pumpRun  = false;
int16_t setpoint = 0;
int16_t level    = 0;

void setup() {
    Serial1.begin(115200);

    BeeBusConfig config;
    config.enableFailSafe = true;
    beebus.begin(&Serial1, 1, config, 115200);

    beebus.addCoil(0,  &pumpRun);      // Peers may switch it
    beebus.addHreg(40, &setpoint);     // Peers may set it
    beebus.addIreg(30, &level);        // Peers may only read it

    // If the node commanding this pump goes away, stop it.
    beebus.setCoilFailsafe(0, BeeBusFailsafe::ResetToValue, false);

    beebus.connect();
}

void loop() {
    beebus.loop();

    level = readLevel();
    digitalWrite(PUMP_PIN, pumpRun);
}

From another node:

beebus.writeHreg(1, 40, 750, BeeBusDelivery::Confirmed);   // And be told it landed
beebus.readIreg(1, 30, myLevel);                           // Arrives on a later token

Stock function codes, stock addressing. A Modbus tool pointed at a gateway sees
an ordinary register map.

And you can have both. The same C++ variable can be a topic and a holding
register — your nodes read it as a typed value with quality, the SCADA reads
register 40, and nothing translates between them.


Features

Peer to peer No master, no broker, no coordinator. Any node talks to any node.
Self-organising Nodes discover each other and build the ring. Joining and leaving are ordinary events.
Self-timing Every timeout is measured, not configured. One config suits wire and radio.
Typed data Values carry their type — including text, length-prefixed. Mismatches are refused, not misread.
Publish / subscribe Per-subscriber deadband and maximum interval. Two subscribers can ask for different things.
Broadcast publishAll() reports a value to the whole ring in one section, however many are listening.
Data quality Good / Uncertain / Bad, judged against the contract you set.
Gap detection Sequence numbers on every publish. A loss is reported, not silent.
Store and forward Buffered topics deliver history with true ages after an outage.
Modbus compatible Coils, discrete inputs, holding and input registers, plus strings.
Failsafe Per-address safe states applied when the commanding node disappears.
Efficient framing One frame carries operations for several destinations in one token turn.
Automatic queuing Operations stack, duplicates are dropped, and a repeated write sends only the latest value.
Network health Every node times every hop, so you can walk the ring and see which link is slow or going bad.
Transport agnostic RS485, UDP multicast, radio, or a transport of your own in six methods.

Supported platforms

Arduino ESP32, ESP8266, SAMD, STM32, Arduino's mbed-based boards and the Uno R4 (renesas_uno)
Host (CMake, C++17) Linux, macOS and Windows, with serial and UDP multicast transports

Arduino boards need 32 KB of RAM or more. A node needs roughly 13 KB for its
fixed tables and buffers, plus its operation queues, before any of your own
data, so the 8-bit AVR boards (Uno, Nano, Mega) are not supported. See
Memory, and trimming it.


Install

Arduino — copy this repository into your libraries folder, or add it as a
ZIP library. Then:

#include <BeeBusGlobal.h>

A serial node needs nothing else: begin(&Serial1, ...) wraps the port for you.
Other transports have their own header under transport/, for example
#include <transport/BeeBusArduinoUdpTransport.h>.

CMake — add the repository to your project and link the BeeBus::beebus
target:

add_subdirectory(modbee-beebus)
target_link_libraries(my_app PRIVATE BeeBus::beebus)

The library builds as the static target beebus (alias BeeBus::beebus).
BEEBUS_BUILD_EXAMPLES builds the host examples beebus_serial_node,
beebus_udp_node, beebus_topic_node and beebus_gateway_node; it defaults on
when BeeBus is the top-level project and off under add_subdirectory().
BEEBUS_SANITIZE and BEEBUS_SANITIZE_THREAD build with the
address/undefined-behaviour or thread sanitizers.

Try it with no hardware — two processes on one machine over UDP multicast is
a real BeeBus network:

cmake -S . -B build && cmake --build build -j
./build/beebus_topic_node 1 2      # in one terminal
./build/beebus_topic_node 2 1      # in another

Documentation

Full documentation is in docs/.

Overview What it is, what it solves, when not to use it
Getting started A first network, one step at a time, with the reason for each step
How the ring works Token passing, membership, healing, measured timing
Frame format Every byte
The topic PDU Typed values, subscriptions, quality, buffering
The Modbus PDU Registers, strings, failsafe
Transports RS485, UDP, radio, writing your own
API reference Every call
Operations and framing Queuing, batching, acknowledgement, retries
Configuration Settings and build options
Gateways Joining segments, and bridging to MQTT
Best practices What to do, and what catches people
Applications Six example networks, laid out and explained

Examples

examples/arduino/Topics_Node1 / Node2 Two nodes exchanging typed values
examples/arduino/Topics_Gateway RS485 to UDP, on one ESP32
examples/arduino/Topics_MQTT_Gateway BeeBus to an MQTT broker, both directions
examples/arduino/Node1_Remote_IO / Node2 Transparent I/O over registers
examples/arduino/BeeBusProtocol A single node with a full register map
examples/arduino/Node_UDP A node over WiFi UDP multicast, on an ESP32
examples/cpp/topic_node Topics over UDP, host build
examples/cpp/gateway_node RS485 to UDP, host build
examples/cpp/serial_node, udp_node Registers over serial and UDP

Not for

Hard real-time motion control. Networks beyond 254 nodes. Untrusted networks —
there is no authentication or encryption, so keep it on a segment you own. Bulk
transfer; a frame is 512 bytes.

32-bit targets only. A node needs roughly 13 KB of RAM for its fixed tables
and buffers, plus its operation queues, before your own data, so the 8-bit AVR
boards are out of reach. The largest fixed item is the per-peer hop table, which
covers the whole address space by default and can be
sized down to what your ring
actually uses — -DBEEBUS_HOP_TABLE_NODES=32 buys back 2.6 KB. The queues can
be trimmed with maxPendingOps and maxPendingResponses.


Licence

BeeBus is licensed under the GNU Affero General Public License v3.0
(AGPL-3.0). See LICENSE.


You focus on the data. Not the protocol itself.

Last updated 1 hour ago
Was this helpful?
Thanks!