pamoja

An SDK for IoT, robotics, and drones

pamojaRev 0.2.0MIT licensepamoja.molex.cloud

One core.
Every language.
For the devices that
change lives.

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.

Opening state
Soil moisture34%
Well level60%
Drip valveOpen
Battery4.06V
LinkLoRa -87dBm

Scripted illustration

Figure 1. Typical application: a farm node. The readings are scripted, not measured. Open the dashboard demo

Table 1. Ordering information. The language pin sets every listing on this site.

LanguageInstall
cargo add pamoja
npm install pamoja
pip install pamoja
dotnet add package Pamoja

Get startedAPI reference

1Features

  • 32 capabilities, each a crate in Rust and a package in TypeScript, Python, and C#
  • 36 crates over one core; the 25 that are no_std are cross-compiled for a Cortex-M4F in CI
  • 38 guides, each showing the same example in four languages, spliced from the tests that run it
  • Offline first: store and forward, compact codecs, LoRa, LoRaWAN, and mesh as first-class links
  • Device identity, a secured session, signed updates with rollback, and a tamper-evident log
  • Builds and tests with nothing plugged in: a loopback link and simulated hardware stand in
  • MIT licensed, published in lockstep on crates.io, npm, PyPI, and NuGet

2Specifications

LanguagesRust, TypeScript, Python, C#
Capabilities32
Crates in the workspace36
Guides38, each in four languages
Smallest targetCortex-M4F, no_std
Third-party code1 of the 5 crates a one-capability Rust build compiles
Registriescrates.io, npm, PyPI, NuGet
LicenseMIT

3Quickstart

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.

Rust
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.

TypeScript
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.

Python
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.

C#
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");
}

4Capabilities

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

4.1

Engine

Core engine for the pamoja device SDK: device model, transport, event bus, and error types.

pamoja-core

4.2

Identity

Signing a payload and checking it, the way a gateway verifies a reading.

security

4.3

Codecs

Moving a document to the compact form a metered link should carry, and back.

codec

4.4

Helpers

The helper math a field node runs between reading a sensor and acting on it.

kit

4.5

Field I/O

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.

serial, modbus, can, gpio, hal

4.8

MAVLink

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.

mavlink

4.12

Dashboard

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.

pamoja-dashboard

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.

5Typical applications

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.

Opening state
Soil moisture34%
Well level60%
Drip valveOpen
Battery4.06V
LinkLoRa -87dBm
Figure 5-1. Water, only when the soil asks for it.

Water, only when the soil asks for it.

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

6Direction

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.

TrackShipsNextLater
Messaging and radioA cost-aware ladder that tries the cheapest link first and buffers when there is none.11 shipping, 4 next, 4 later
  • ESP-NOW link
  • nRF24 link
  • Meshtastic bridge
  • cellular uplink (NB-IoT, LTE-M)
  • BLE and the phone as gateway
  • Matter and Thread
  • sub-GHz radios (CC1101, HC-12)
  • LoRa-to-satellite
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
  • ultrasonic tank level (JSN-SR04T)
  • GPS (NEO-M8N)
  • DC motor drivers
  • OLED and e-ink displays
  • particulates (PMS5003)
  • NDIR CO2 (MH-Z19)
  • pulse oximetry (MAX30102)
  • IR temperature (MLX90614)
  • load cells (HX711)
  • 9-axis IMU (MPU9250)
  • humidity (DHT22)
  • gas and VOC (MQ series, SGP40, BME680)
  • AC energy metering (PZEM-004T)
  • battery fuel gauge (MAX17043)
  • compass (QMC5883)
  • presence (PIR, microwave radar)
  • time-of-flight distance (VL53L1X)
  • ECG (AD8232)
  • RFID, NFC, and fingerprint
  • flow and leak sensing
  • water quality (pH, TDS, turbidity)
  • submersible pressure level
  • weather station (rain, wind, light)
  • thermocouples (MAX31855)
  • LCD and TFT displays
  • buzzers and RGB alerts
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
  • fleet and swarm orchestration
  • mission planning
  • numeric IK for longer arms
  • micro-ROS on microcontrollers
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
  • data-mule sync
  • field timekeeping (RTC, GPS time)
  • telemetry export to common backends
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
  • DTLS for CoAP
  • X.509 device identity
  • secure-element key storage
  • attestation and secure boot
  • encrypted updates
ReachOne engine, idiomatic in every language a device developer actually uses, plus plain-language helpers.1 shipping, 2 next, 8 later
  • Lua
  • WebAssembly
  • Kotlin and Java
  • Swift
  • Go
  • Ruby
  • Sema, a language written in sentences
  • visual block editor
  • in-browser simulator
  • offline cookbook

7BackingNot open

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

KitsNot open
Uplink sponsorshipNot open
Partner onboardingNot open

Each opens once the first pilot has run and the numbers are real.

Table 7-2. How it opens, in order

StateMilestoneDetail
nowDesign the kit with the people who will run itThe bill of materials, the salvage-friendly boards, the radios, and the pre-flashed profiles, worked out with field partners rather than guessed at.
nextA first pilot in one placeTen kits in one district for one season, with the dashboard on local phones, before anything is offered to anyone.
laterBacking opensThe 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.

  1. FreeNeighbor meshA hop to the node next door, on its own battery
  2. SponsoredCommunity gatewayOne uplink carrying a whole area's traffic
  3. Per byteCellularMoney for every byte, and a chunk of the battery
  4. DearestSatelliteThe last rung, reached only when the rest are gone
Figure 7-1. The ladder a message climbs, cheapest rung first. A rung is reached only when every rung under it is gone, which is what keeps the dear ones rare.