4.1
Engine
Core engine for the pamoja device SDK: device model, transport, event bus, and error types.
pamojaRev 0.2.0MIT licensepamoja.molex.cloud
An open SDK for IoT, robotics, and drones. pamoja runs on a two-dollar microcontroller, on a solar panel, over an intermittent radio link, so connected devices can reach the farms, clinics, and disaster zones that every other stack quietly leaves out.
| Soil moisture | 34% |
|---|---|
| Well level | 60% |
| Drip valve | Open |
| Battery | 4.06V |
| Link | LoRa -87dBm |
Scripted illustration
Table 1. Ordering information. The language pin sets every listing on this site.
| Language | Install |
|---|---|
cargo add pamoja | |
npm install pamoja | |
pip install pamoja | |
dotnet add package Pamoja |
no_std are cross-compiled for a Cortex-M4F in CI| Languages | Rust, TypeScript, Python, C# |
|---|---|
| Capabilities | 32 |
| Crates in the workspace | 36 |
| Guides | 38, each in four languages |
| Smallest target | Cortex-M4F, no_std |
| Third-party code | 1 of the 5 crates a one-capability Rust build compiles |
| Registries | crates.io, npm, PyPI, NuGet |
| License | MIT |
The same program in the language you already work in: a reading taken off a wire on a field node, sent over a link, and checked on the gateway that receives it, with nothing plugged in and nothing running.
Listing 3-1, Rust. From examples/guides/quickstart.rs, which runs in CI.
use pamoja_codec::{decode_deltas, encode_deltas};
use pamoja_core::{Receive, Transport};
use pamoja_kit::Smoother;
use pamoja_loopback::{LoopbackBroker, LoopbackTransport};
use pamoja_security::{DeviceIdentity, PublicIdentity};
use pamoja_sensors::ds18b20::{temperature_from_celsius, Resolution, Scratchpad};
// The link. A loopback broker stands in for MQTT or CoAP, so this runs with no network
// and nothing listening. Point the node at a real transport and nothing below changes.
let broker = LoopbackBroker::new();
let mut node = LoopbackTransport::new(broker.clone());
let mut gateway = LoopbackTransport::new(broker);
node.connect().await?;
gateway.connect().await?;
let topic = "sensors/1/temperature";
gateway.subscribe(topic).await?;
// The device's identity is provisioned once and never leaves it. The gateway is told
// only the public half, which is how it recognizes this device later.
let device = DeviceIdentity::from_seed(&[7u8; 32]);
let known = PublicIdentity::from_bytes(&device.public().to_bytes()).expect("a valid key");
println!("gateway trusts device {}", known.fingerprint());
// A stand-in for the thermometer. On a running node these nine bytes arrive from the
// 1-Wire bus; here the library builds what a part at 25.0625 C would send.
let off_the_bus = Scratchpad::new(
temperature_from_celsius(25.0625, Resolution::Bits12),
Resolution::Bits12,
75,
-10,
)
.to_bytes();
// On the node. The part checksums every read, so a value mangled on a long run is an
// error rather than a plausible temperature a couple of degrees off.
let celsius = Scratchpad::parse(&off_the_bus)
.expect("the thermometer's checksum matches")
.temperature_celsius();
println!("read {celsius:.4} C");
// Readings jitter, so smooth them, and send a batch rather than one at a time.
// Successive readings differ by very little, so the differences cost a fraction of
// what the readings would on a link that charges by the byte.
let mut smoother = Smoother::new(0.5);
let batch: Vec<i64> = [celsius, celsius + 0.5, celsius + 0.4]
.into_iter()
.map(|sample| (smoother.update(sample) * 100.0).round() as i64)
.collect();
let packed = encode_deltas(&batch);
let (readings, bytes) = (batch.len(), packed.len());
println!("packed {readings} readings into {bytes} bytes");
// Sign the batch and send it. The signature travels with the payload as one message,
// so there is nothing to keep together and split correctly at the far end.
let message = device.sign_message(&packed);
node.send(topic, &message)
.await
.expect("the node publishes");
// On the gateway. Verifying returns the payload, so a reading that was altered on the
// way, or signed by some other device, never reaches the code that unpacks it.
let received = gateway.recv().await?.expect("a message");
match known.verify_message(&received.payload) {
Ok(payload) => {
let readings = decode_deltas(payload).expect("a valid batch");
println!("gateway accepted {readings:?} in hundredths of a degree");
}
Err(error) => println!("gateway rejected the reading: {error}"),
}Listing 3-1, TypeScript. From bindings/node/guides/quickstart.ts, which runs in CI.
import { packSamples, unpackSamples } from '@pamoja/codec'
import { Smoother } from '@pamoja/kit'
import { LoopbackBroker } from '@pamoja/loopback'
import { DeviceIdentity, fingerprint, verifyMessage } from '@pamoja/security'
import { ds18b20 } from '@pamoja/sensors'
// The device's identity is provisioned once and never leaves it. The gateway is told only
// the public half, which is how it recognizes this device later.
const SEED = Buffer.alloc(32, 7)
const TOPIC = 'sensors/1/temperature'
async function main(): Promise<Buffer> {
// The link. A loopback broker stands in for MQTT or CoAP, so this runs with no network
// and nothing listening. Point the node at a real transport and nothing below changes.
const broker = new LoopbackBroker()
const node = broker.link()
const gateway = broker.link()
await node.connect()
await gateway.connect()
await gateway.subscribe(TOPIC)
const device = DeviceIdentity.fromSeed(SEED)
const known = device.publicKey()
console.log(`gateway trusts device ${fingerprint(known)}`)
// A stand-in for the thermometer. On a running node these nine bytes arrive from the
// 1-Wire bus; here the library builds what a part at 25.0625 C would send.
const offTheBus = ds18b20.buildScratchpad(25.0625, 12, 75, -10)
// On the node. The part checksums every read, so a value mangled on a long run is an
// error rather than a plausible temperature a couple of degrees off.
const celsius = ds18b20.parseScratchpad(offTheBus).microCelsius / 1e6
console.log(`read ${celsius.toFixed(4)} C`)
// Readings jitter, so smooth them, and send a batch rather than one at a time.
// Successive readings differ by very little, so the differences cost a fraction of what
// the readings would on a link that charges by the byte.
const smoother = new Smoother(0.5)
const batch = [celsius, celsius + 0.5, celsius + 0.4].map((sample) =>
Math.round(smoother.update(sample) * 100),
)
const packed = packSamples(batch)
console.log(`packed ${batch.length} readings into ${packed.length} bytes`)
// Sign the batch and send it. The signature travels with the payload as one message, so
// there is nothing to keep together and split correctly at the far end.
await node.send(TOPIC, device.signMessage(packed))
// On the gateway. Verifying returns the payload, so a reading that was altered on the
// way, or signed by some other device, never reaches the code that unpacks it.
const received = await gateway.recv()
const payload = verifyMessage(known, received!.payload)
if (payload === null) {
console.log('gateway rejected the reading')
} else {
console.log(`gateway accepted ${unpackSamples(payload).join(', ')} in hundredths of a degree`)
}
return received!.payload
}
main()Listing 3-1, Python. From bindings/python/guides/quickstart.py, which runs in CI.
import asyncio
from pamoja import sensors
from pamoja.codec import pack_samples, unpack_samples
from pamoja.kit import Smoother
from pamoja.loopback import LoopbackBroker
from pamoja.security import DeviceIdentity, fingerprint, verify_message
# The device's identity is provisioned once and never leaves it. The gateway is told only
# the public half, which is how it recognizes this device later.
SEED = bytes([7]) * 32
TOPIC = "sensors/1/temperature"
async def main() -> bytes:
# The link. A loopback broker stands in for MQTT or CoAP, so this runs with no network
# and nothing listening. Point the node at a real transport and nothing below changes.
broker = LoopbackBroker()
node = broker.link()
gateway = broker.link()
await node.connect()
await gateway.connect()
await gateway.subscribe(TOPIC)
device = DeviceIdentity.from_seed(SEED)
known = device.public_key
print(f"gateway trusts device {fingerprint(known)}")
# A stand-in for the thermometer. On a running node these nine bytes arrive from the
# 1-Wire bus; here the library builds what a part at 25.0625 C would send.
off_the_bus = sensors.ds18b20.build_scratchpad(25.0625, 12, 75, -10)
# On the node. The part checksums every read, so a value mangled on a long run is an
# error rather than a plausible temperature a couple of degrees off.
celsius = sensors.ds18b20.parse_scratchpad(off_the_bus).micro_celsius / 1e6
print(f"read {celsius:.4f} C")
# Readings jitter, so smooth them, and send a batch rather than one at a time.
# Successive readings differ by very little, so the differences cost a fraction of
# what the readings would on a link that charges by the byte.
smoother = Smoother(0.5)
batch = [
round(smoother.update(sample) * 100)
for sample in (celsius, celsius + 0.5, celsius + 0.4)
]
packed = pack_samples(batch)
print(f"packed {len(batch)} readings into {len(packed)} bytes")
# Sign the batch and send it. The signature travels with the payload as one message,
# so there is nothing to keep together and split correctly at the far end.
await node.send(TOPIC, device.sign_message(packed))
# On the gateway. Verifying returns the payload, so a reading that was altered on the
# way, or signed by some other device, never reaches the code that unpacks it.
received = await gateway.recv()
payload = verify_message(known, received.payload)
if payload is None:
print("gateway rejected the reading")
else:
print(f"gateway accepted {unpack_samples(payload)} in hundredths of a degree")
return received.payload
message = asyncio.run(main())Listing 3-1, C#. From bindings/dotnet/samples/Pamoja.Guides/Quickstart.cs, which runs in CI.
byte[] seed = new byte[DeviceIdentity.KeyLength];
Array.Fill(seed, (byte)7);
// The link. A loopback broker stands in for MQTT or CoAP, so this runs with no
// network and nothing listening. Point the node at a real transport and nothing
// below changes.
using var broker = new LoopbackBroker();
using LoopbackTransport node = broker.Link();
using LoopbackTransport gateway = broker.Link();
await node.ConnectAsync();
await gateway.ConnectAsync();
await gateway.SubscribeAsync(Topic);
using var device = new DeviceIdentity(seed);
byte[] known = device.PublicKey;
Console.WriteLine($"gateway trusts device {DeviceIdentity.FingerprintOf(known)}");
// A stand-in for the thermometer. On a running node these nine bytes arrive from
// the 1-Wire bus; here the library builds what a part at 25.0625 C would send.
byte[] offTheBus = Ds18b20.BuildScratchpad(25.0625f, 12, 75, -10);
// On the node. The part checksums every read, so a value mangled on a long run is
// an error rather than a plausible temperature a couple of degrees off.
float celsius = Ds18b20.ParseScratchpad(offTheBus).MicroCelsius / 1e6f;
Console.WriteLine($"read {celsius:F4} C");
// Readings jitter, so smooth them, and send a batch rather than one at a time.
// Successive readings differ by very little, so the differences cost a fraction of
// what the readings would on a link that charges by the byte.
using var smoother = new Smoother(0.5f);
long[] batch =
[
.. new[] { celsius, celsius + 0.5f, celsius + 0.4f }
.Select(sample => (long)Math.Round(smoother.Update(sample) * 100)),
];
byte[] packed = Codec.PackSamples(batch);
Console.WriteLine($"packed {batch.Length} readings into {packed.Length} bytes");
// Sign the batch and send it. The signature travels with the payload as one
// message, so there is nothing to keep together and split correctly at the far end.
await node.SendAsync(Topic, device.SignMessage(packed));
// On the gateway. Verifying returns the payload, so a reading that was altered on
// the way, or signed by some other device, never reaches the code that unpacks it.
TransportMessage? received = await gateway.ReceiveAsync();
byte[]? payload = DeviceIdentity.VerifyMessage(known, received!.Payload);
if (payload is null)
{
Console.WriteLine("gateway rejected the reading");
}
else
{
Console.WriteLine(
$"gateway accepted {string.Join(", ", Codec.UnpackSamples(payload))}"
+ " in hundredths of a degree");
}Every capability is a crate in Rust and a package in each binding, behind the traits in pamoja-core. On a microcontroller you bring in two crates and nothing else.
Table 4-1. The four bindings over the engine, 37 capabilities under 10 headings, and the dashboard. A heading that holds more than one is also one thing to install.
Bindings
pamoja-ffi4.1
Core engine for the pamoja device SDK: device model, transport, event bus, and error types.
4.2
Signing a payload and checking it, the way a gateway verifies a reading.
4.3
Moving a document to the compact form a metered link should carry, and back.
4.4
The helper math a field node runs between reading a sensor and acting on it.
4.5
The wires a gateway actually has: framed serial packets, an RS485 request and the reply it draws, a CAN frame, the address a chip answers on, and the bus that carries a driver to the part.
4.6
The parts wired to a board: a thermometer that checks its own bytes, a servo pulse, a stepper walking its coils, and a part of your own.
4.7
Budgeting airtime, framing a mesh packet, routing it, and securing a LoRaWAN uplink: everything a node needs to reach a network it cannot see.
4.8
Talking to an autopilot: framing a message, reading it back off a link that splits and garbles it, proving a signed frame came from who it claims, and moving a plan across one frame at a time.
4.9
Proving what a node did, saying it in confidence, fixing it in the field, and deciding how often it can afford to do any of that.
4.10
Reaching the network when no single link always works, and testing all of it with nothing plugged in.
mqtt, coap, loopback, sync, ladder, bus, transport, link, sim
4.11
A node written down as a JSON file, rules between nodes as another, and the naming and encoding rules a robot's topics follow, with no ROS 2 or Zenoh installed.
4.12
Local-first device dashboard for pamoja: a node serves a hand-built, localized web UI on its local network from a language-neutral state snapshot, fully offline.
Every capability, with its package on crates.io, npm, PyPI, and NuGet and its API pages in all four languages, is on the reference. How a call reaches a crate, from a binding down through the engine, is drawn on the architecture page.
Each figure is a scripted illustration of a node doing its job: the readings are drawn in the browser, not measured, and none of the nine is a deployment. Six are field nodes; three are robots. What the crates behind them actually do is in the guides, where every example runs in CI.
| Soil moisture | 34% |
|---|---|
| Well level | 60% |
| Drip valve | Open |
| Battery | 4.06V |
| Link | LoRa -87dBm |
Drip controller · RS485 probes · LoRa uplink
A drip controller reads soil moisture from RS485 probes down a long cable, watches a well's level, and opens the valve only when it should, then duty-cycles back to sleep on a small solar panel until the next cycle. One node, a whole season, unattended.
Crates: pamoja-modbus, pamoja-lora, pamoja-kit, pamoja-power, pamoja-sync, pamoja-profile
| Fridge temp | 4.2°C |
|---|---|
| Ward power | 90% |
| Oxygen stock | 74% |
| Uplink | Synced |
| Link | NB-IoT -102dBm |
Cold chain monitor · NB-IoT · signed log
A solar-run clinic tracks the things it cannot afford to lose, the power to the ward, the oxygen on the shelf, the cold chain for medicines, and writes each reading into a tamper-evident log. Drop the link for days and nothing is lost; when it returns, the record uploads intact.
Crates: pamoja-telemetry, pamoja-security, pamoja-audit, pamoja-sync, pamoja-power, pamoja-profile
| Flow rate | 0L/min |
|---|---|
| Well level | 60% |
| Storage tank | 40% |
| Pump health | Nominal |
| Link | LoRa -91dBm |
Handpump meter · LoRa · store and forward
A sensor on the well watches the water level and the flow through the pump, logging every draw, so a community, and the people who fund the repair, can see a pump weakening before it fails, not weeks after the queue has already given up on it.
Crates: pamoja-modbus, pamoja-kit, pamoja-audit, pamoja-sync, pamoja-lora, pamoja-profile
| Ambient sound | 41dB |
|---|---|
| River level | 38% |
| Battery | 92% |
| Relay | Idle |
| Link | Mesh -95dBm |
Acoustic relay · mesh · solar
Low-power nodes across a reserve relay the sound of a chainsaw or a shot, the level of a drying river, the movement at a waterhole, hop by hop to a ranger post, on batteries that last a season, where pulling a wire was never an option.
Crates: pamoja-mesh, pamoja-routing, pamoja-lora, pamoja-telemetry, pamoja-power, pamoja-codec
| Neighbors | 5 |
|---|---|
| Hops to gateway | 3 |
| Routing | Optimized |
| Messages relayed | 318 |
| Link | Mesh -83dBm |
Mesh node · no tower · learned routes
Nodes form a mesh, each relaying for the next, flooding a message across the valley exactly once, then learning cheaper routes from the traffic they overhear, so the airtime and battery that blind flooding wastes is saved for the messages that matter.
Crates: pamoja-mesh, pamoja-routing, pamoja-lora, pamoja-sync, pamoja-codec
| Cell grid | Down |
|---|---|
| pamoja mesh | Up |
| Uplink queue | 11 |
| Reports carried | 12 |
| Link | Sat -118dBm |
Coast relay · satellite backhaul · queued
A typhoon makes landfall and the cell network goes with it. Along the coast, battery nodes relay hop by hop to a gateway that still holds a satellite uplink, carrying the one thing that matters in the first hours: where people are, and who needs help.
Crates: pamoja-mesh, pamoja-routing, pamoja-lorawan, pamoja-telemetry, pamoja-power
| Linear speed | 0.00m/s |
|---|---|
| Angular rate | 0.00rad/s |
| Odometry | 0.0m |
| Safety | Armed |
| Waypoints | 5 |
Mobile rover · waypoint patrol
The rover chases a carrot along its route while odometry turns wheel motion into a live pose, with no GPS. A watchdog and an obstacle stop cut motion in milliseconds, and every cmd_vel leaves as a ROS 2 Twist on a Zenoh key any robot on the fleet already understands.
Crates: pamoja-kit, pamoja-ros2, pamoja-zenoh, pamoja-sim
| Joint 1 | 0° |
|---|---|
| Joint 2 | 0° |
| Reach | 0% |
| Gripper | Open |
| Trajectory | 4 points |
Robot arm · pick and place
A two-link arm solves its own inverse kinematics to land its tip on a moving target, elbow up or down, while forward kinematics draw the linkage and confirm the reach. The same arm takes a ROS 2 trajectory action from any controller on the bus.
Crates: pamoja-kit, pamoja-ros2
| Peers | 3 |
|---|---|
| Command rate | 0/s |
| Sim twins | 1/ 3 |
| Coordination | In sync |
| Link | Zenoh -57dBm |
Fleet · one bus, many robots
A single Zenoh key expression subscribes to every robot's cmd_vel at once, routerless, and one service flips them all into a mode together. Each robot can be the real thing or a pure-software twin, so the whole fleet is testable with no hardware.
Crates: pamoja-zenoh, pamoja-ros2, pamoja-sim, pamoja-kit
Not a sensor library: a platform for physical things. Each track runs from what ships today, across the line at today, to what is committed next and what comes after it.
Table 6-1. Each track from what ships to what is committed. A filled mark ships today, an open mark is committed next, and a light mark comes later.
| Track | Ships | Next | Later |
|---|---|---|---|
| Messaging and radioA cost-aware ladder that tries the cheapest link first and buffers when there is none.11 shipping, 4 next, 4 later |
|
|
|
| Hardware and sensorsTalk to the cheap, salvageable parts the field already runs on, by name instead of by pin.14 shipping, 10 next, 16 later |
|
|
|
| Robotics and dronesDrive it, dead-reckon where it is, follow a path, and keep it safe: a robot as an ordinary pamoja device, bridged to ROS 2 over Zenoh, and an autopilot driven over MAVLink.9 shipping, 1 next, 3 later |
|
|
|
| Resilience and powerOffline-first by default, awake only when it must be, so a node lives on sun and a battery.6 shipping, 1 next, 2 later |
|
| |
| Security and trustMemory-safe by construction, with identity, a secured channel, and signed updates that survive a hostile link.6 shipping, 5 next, 0 later |
| ||
| ReachOne engine, idiomatic in every language a device developer actually uses, plus plain-language helpers.1 shipping, 2 next, 8 later |
|
|
The software is free and MIT-licensed forever. What costs money is the hardware: cheap, salvageable boards, radios, and a USB or SD card with the SDK preloaded, put into the hands of the people who need it. Donors fund kits; vendors and partners help build and ship them. A kit ships pre-flashed with a local dashboard it serves over its own WiFi, so anyone nearby opens it on an ordinary phone while the radio mesh carries the data behind it.
Table 7-1. What backing will open
| Kits | Not open |
|---|---|
| Uplink sponsorship | Not open |
| Partner onboarding | Not open |
Each opens once the first pilot has run and the numbers are real.
Table 7-2. How it opens, in order
| State | Milestone | Detail |
|---|---|---|
| now | Design the kit with the people who will run it | The bill of materials, the salvage-friendly boards, the radios, and the pre-flashed profiles, worked out with field partners rather than guessed at. |
| next | A first pilot in one place | Ten kits in one district for one season, with the dashboard on local phones, before anything is offered to anyone. |
| later | Backing opens | The tiers, the uplink sponsorship, and partner onboarding go live once the pilot has run and the numbers below are real. |
pamoja is free, MIT-licensed software. It does not run satellites, and satellite data is never free. What pamoja does is make connectivity cheap enough to be free at the point of use. Satellite and cellular are the last, most expensive rung of the ladder, reached only when local mesh and community gateways are gone. One sponsored gateway carries a whole area's traffic, and every message is kept to tens of bytes, so the cost is cents, and it is borne upstream by an NGO, an agency, or a donated kit, never by the family on the ground.