pamoja

Documentation

Examples#

Everything on this page runs in CI on every change, so an example here is never one that worked once. The programs are complete: each has a main, reads top to bottom, and runs with nothing plugged in, since a simulator or a loopback stands in for the hardware. The guide examples are the same program in four languages, spliced into each guide from the test file that runs it, so what a guide shows is exactly what ran.

Programs#

Each one is a complete program with a main, written to be read top to bottom and run with nothing plugged in. The line beside it runs it.

batched_telemetryexamples/batched_telemetry.rs

Metered-link encoding: pack a batch of readings into a fraction of the bytes.

cargo run -p pamoja-examples --example batched_telemetry
conformanceexamples/conformance.rs

The whole SDK in one run: a cold-chain node from sensor to gateway over loopback.

cargo run -p pamoja-examples --example conformance
conformance_vectorsexamples/conformance_vectors.rs

Regenerates the cross-language conformance vectors.

cargo run -p pamoja-examples --example conformance_vectors
device_profileexamples/device_profile.rs

A device profile assembled into a ready-to-run cold-chain node, over loopback.

cargo run -p pamoja-examples --example device_profile
lora_budgetexamples/lora_budget.rs

LoRa airtime and duty cycle: what it costs to send a batch over a long-range link.

cargo run -p pamoja-examples --example lora_budget
robot_waypointexamples/robot_waypoint.rs

Drive a rover safely, dead-reckon where it is, steer to a waypoint, and speak ROS 2.

cargo run -p pamoja-examples --example robot_waypoint
signed_auditexamples/signed_audit.rs

A tamper-evident cold-chain log: signed, hash-chained fridge readings.

cargo run -p pamoja-examples --example signed_audit
signed_telemetryexamples/signed_telemetry.rs

Tamper-evident telemetry: a device signs each reading, a gateway verifies it.

cargo run -p pamoja-examples --example signed_telemetry
signed_updateexamples/signed_update.rs

Updating a device in the field, including the update that goes wrong.

cargo run -p pamoja-examples --example signed_update
store_and_forwardexamples/store_and_forward.rs

Offline-first store-and-forward, end to end, with no hardware and no broker.

cargo run -p pamoja-examples --example store_and_forward
telemetryexamples/telemetry.rs

Observability that degrades gracefully on a metered link.

cargo run -p pamoja-examples --example telemetry

Guide examples#

Every guide carries the same example in Rust, TypeScript, Python, and C#, spliced from the file that runs it in CI. The buttons open those files; the guide explains them.

Identity#

Device identity
  • A public key taken as 32 bytes verifies the reading the device signed, so a gateway needs nothing else from a device to check what it sends.
  • Signing is deterministic: the same reading signed twice gives the identical signature, so signing needs no entropy.
  • A reading altered after signing does not verify, which is what catches a value edited between the meter and the bill.
  • The same reading and signature offered under a second device's key do not verify either, so a signature does not carry over to another identity.

Codecs#

Codecs
  • A reading transcoded to CBOR comes back unchanged, and the CBOR is shorter than the JSON it came from.
  • Five samples that rise, fall, then jump to 900 pack into fewer bytes than the forty the raw values cost, so a negative difference and a large one both stay small.
  • The packed batch unpacks to the same five numbers, the jump included.
  • Quantized readings decode to within 0.01, the precision a scale of 100 sets, and that error is what the packing trades for the bytes.

Helpers#

Helpers
  • 12 mA reads 50% and 4 mA reads 0%, because the span starts at 4 mA and not at zero; a map scaled from zero would put mid-scale at 60% and still be self-consistent.
  • 0 mA reads -25%, off the bottom of the scale, which is what separates a broken loop from an empty tank.
  • One dropout among five samples leaves the filtered level at 50%, where a mean over the same window would be dragged down by it.
  • At the setpoint the pump stays off, and it starts only once the level falls below the deadband.
  • Once running it keeps running at a level back inside the deadband, and stops only above the top of it.

Field I/O#

Serial framing
  • A payload carrying the end byte or the escape byte is stuffed rather than taken for a frame boundary, and it decodes back byte for byte with both values still in it.
  • Both framings cost bytes: each frame comes out longer than the payload that went into it.
  • A frame that ends inside an escape pair is discarded on its own, and the whole frames before and after it come out of the same chunk intact.
  • The dropped frame is counted, and the count stays at one for the rest of the chunk rather than climbing as the good bytes after it arrive, so a read loop can measure how noisy a link is.
Modbus RTU
  • A request for three holding registers is eight bytes on the wire: the unit address, the function code, the two 16-bit fields and the checksum.
  • A reply validates its own checksum before any value is read out of it.
  • In TypeScript, Python and C# the reply reports the unit address it was sent to and no exception, so a served request is not read as a refused one.
  • The three 16-bit registers come back in the order the meter reported them.
  • A corrupted byte is caught rather than passed on as a plausible reading.
CAN and J1939
  • A priority, a parameter group and a source address compose an identifier and decode back out of it unchanged.
  • The broadcast carries no destination, while a parameter group below the PDU1 limit is addressed, so those eight bits name a node instead of extending the group number.
  • A standard 11-bit identifier decodes to nothing, because J1939 does not use one.
  • Engine speed sits in bytes 4 and 5 of that group at 0.125 rpm per bit, so the eight-byte payload reads back as a thousand rpm.
  • The CAN-FD length encoding puts 32 bytes at data length code 13, while a classic frame still refuses a ninth byte.
I2C, SPI, and GPIO
  • A device at 0x76 is written to as 0xEC and read from as 0xED, one byte either way, which is why a datasheet and a bus capture rarely print the same number.
  • 0x76 is a device address and 0x78 is not, because 0x78 opens the block the specification keeps back for itself.
  • A 10-bit address takes two bytes on the wire where a 7-bit one takes a single byte, so a bus driver sends a different number of bytes depending on the address it holds.
  • Mode 3 is CPOL 1 with CPHA 1, and the pair maps back the other way: CPOL 1 with CPHA 0 is mode 2, not mode 3 again.
  • An active-low relay is energised by a low level, which that polarity reads back as asserted, and releasing it is a rising edge that a falling-edge trigger ignores.

Sensing and actuation#

Sensor drivers
  • 25.0625 degrees Celsius at 12-bit resolution builds register 0x0191, the row the DS18B20 temperature table publishes, and that register decodes back to the same temperature, exact in integer micro-degrees.
  • The same nine bytes report the resolution the configuration byte selects and both alarm thresholds, 75 and -10 degrees, written into them.
  • One flipped bit fails the CRC, so a read corrupted on a long 1-Wire run is repeated instead of logged as a temperature a couple of degrees off.
  • The 1-Wire checksum is CRC-8/MAXIM-DOW, which over the ASCII digits 1 to 9 produces the published check value 0xA1.
  • 1 mA per count across a 2 milliohm shunt calibrates to 0x5000, the number the INA219 datasheet's design example works out, and the registers a monitor across that load reports decode back to 11.98 V, 10 A, and 119.8 W.
Actuator drivers
  • 50 Hz off the 25 MHz internal oscillator is prescale 121 (0x79), the value the datasheet's formula gives, so a divider that is wrong but round-trips consistently still fails.
  • Channel 3's registers begin at 0x12, four along from each channel before it.
  • A centred 1500 microsecond pulse at 50 Hz goes low at count 307 of the 4096 counts in a period.
  • Fully off is its own encoding rather than a zero duty, which would still hold the output high for the first count of every period.
  • Half-step drive alternates one energised coil with two, 1000 then 1100 then 0100, and eight steps wrap back to the pattern it started on.
  • A quarter turn of a 1.8-degree motor is 50 whole steps.

Radio and reach#

LoRa airtime
  • Data rate 0 in EU863-870 selects SF12, the slowest rate the band defines and the one that reaches furthest.
  • A ten-byte frame at those settings takes 991,232 microseconds on air, the published time on air for SF12 at 125 kHz, so a plan carrying the wrong bandwidth fails here rather than passing a round-trip against itself.
  • 868.1 MHz sits in a sub-band capped at 1% of the time and 16 dBm, both read from the plan by frequency.
  • One percent of the time buys ninety-nine times the frame's own length in silence after it, which leaves thirty-six readings an hour.
  • A frequency inside no sub-band the plan describes reports no duty cycle rather than an unlimited one, because the limit on it is published elsewhere.
LoRaWAN
  • A device holding nothing but the root key verifies the accept and reads the address 0x26012E43 out of it, decrypted from the frame rather than configured on the device.
  • Neither side transmits a session key. The device derives its pair from the accept it decrypts, the network derives its pair from the grant, and a frame the device encrypts reads back at the network as level=high.
  • That uplink exercises both derived keys: the message integrity code verifies under the network session key and the payload decrypts under the application key, because the frame goes to a port above zero.
  • One byte flipped inside the accept fails the integrity check, so a device does not activate on a join it cannot attribute to its own network.
Mesh frames
  • A broadcast frame parses back out of the bytes that go on the air with its payload intact and its destination equal to BROADCAST.
  • A packet is identified as it floods by its source and sequence id, so the second copy to arrive is dropped instead of relayed again.
  • Relaying spends exactly one hop, and the forwarded bytes still parse and carry the same payload, because the checksum covers every byte except the hop limit.
  • A packet whose hops have run out is not relayed, which is what keeps a flood finite.
  • An inverted payload byte fails the checksum instead of arriving as a plausible reading.
Routing
  • One packet heard from the pump through the north relay teaches the way back to it, with no routing messages exchanged.
  • A cost-1 report through the east relay takes that route over and a cost-4 report through the south relay is refused, so the table holds the cheapest way it has heard and each observation says whether it changed anything.
  • Four observations of two nodes leave two routes, not four.
  • A packet for the gateway is delivered, one for the pump relays to the east relay, and one for the silo floods.
  • Forgetting the pump drops its route and leaves the tank's; packets for the pump flood again.

Trust and operation#

Audit log
  • The two records verify in order against nothing but the public half of the device's key.
  • The second record's link is the digest of the first, so the chain fixes the order as well as the contents.
  • A record edited in storage still parses and still carries the device's signature, but the digest recomputed from its fields no longer matches it, so verification fails.
  • A log missing its first record is rejected as well: the survivor's index and its link both say a record came before it.
Secured session
  • The gateway opens what the node sealed, so both ends reached the same key from opposite roles without either of them sending it.
  • What leaves the node is not the reading: the ciphertext differs from flow=41.2.
  • Those nine bytes come back exactly, in Rust out of the same buffer that held the ciphertext a moment earlier.
  • A frame the gateway has already accepted is refused when it arrives again, so a message captured off the air cannot be delivered twice.
Signed updates
  • Verifying the envelope hands back the manifest, so the device learns which slot the release is for from the signature rather than from whoever sent it.
  • The digest in the manifest is the one the library computes over the image, so a publisher that hashed the wrong bytes cannot produce a release that stages.
  • Staging completes only because every byte the manifest declared arrived and hashed to that digest, which the device recomputes as the pieces come in.
  • The release lands in the slot the device is not running from, so the working image is never overwritten.
  • The first boot into the staged image is a trial, and confirming it is what leaves the slot confirmed rather than reverting on the next boot.
  • A release signed by a key the device is not anchored to is refused, even though the manifest inside it is the one that was just accepted, because the signature is checked before anything in the manifest is read.
Power
  • With the default thresholds, 80% charge is active, 35% is saver and 12% is critical.
  • The interval follows the mode, so a battery at 12% is asked for one reading an hour where a healthy one gives sixty.
  • A delivering panel eases the governor off by one mode and no further, so the flat battery reports on the saver cadence rather than the active one.
  • Two seconds of work is one part in thirty at the minute cadence and one part in 1800 at the hourly one, the sixtyfold cut in average draw the stretch buys.
  • The fraction is the awake share of the whole period, so two seconds awake and 58 asleep is one in thirty, not one in twenty-nine.
  • from_fraction divides the period it is given, so a quarter-duty second is 250ms awake and 750ms asleep.
Telemetry
  • A link cost sets the bar: Metered puts it at Info and Expensive raises it to Warn, so how much a node says follows what the link costs and not the level it was built with.
  • The same reading.ok is handed back on the metered link and held back on the satellite one, so each event is judged against the bar in force when it is recorded.
  • A shipped event comes back with its code and its measurement, battery.low at 0.18, so a transport has the number that triggered it.
  • A held-back event is still counted: two readings were recorded at Info even though only the first one went out.
  • Five events recorded reconcile as three shipped and two dropped, so thinning the stream loses nothing from the totals.
  • Offline is the last rung, holding back everything below Error, so a node with no link keeps its failures and nothing else.

Transports and testing#

MQTT
  • A subscription with a + in it takes a reading published under a concrete name, so a gateway follows every node's temperature without naming one.
  • What arrives is the topic the node published to, sensors/1/temperature, rather than the sensors/+/temperature filter that matched it, and the payload is the bytes the node sent.
  • Both clients default to at least once, and the one setting covers the subscription and the publish alike, so a reading travels under a guarantee the broker acknowledges rather than fire and forget.
  • A client that has disconnected reports itself disconnected, so code deciding whether to reconnect is not reading a stale flag.
  • A broker that is not there fails the connect and leaves the client not connected, which is what a retry loop tests.
CoAP
  • Connecting a CoAP endpoint binds a local socket and nothing else: it reports itself connected with nothing on the far side.
  • A non-confirmable send succeeds without an acknowledgement, which is the mode for a reading whose loss costs nothing.
  • A confirmable send to that same address fails once its retransmissions run out. Both endpoints point at the same dead port, so the delivery guarantee, not the destination, decides the outcome.
  • The failure arrives as an error the caller handles rather than a silent success: each example catches it and prints why the command gave up, so a command is never assumed to have landed.
  • Disconnecting releases the socket and the endpoint reports itself closed.
Loopback
  • A payload published on one link arrives on another carrying the topic it was sent to, with no port bound and no broker process running.
  • + matches exactly one level, so the filter takes the temperature topic and leaves the /raw reading a level below it, even though that one went out first.
  • # matches the levels that remain, so the second filter takes the deeper topic the single-level one passed over.
  • A link can join a broker that has already routed traffic, take a filter of its own, and receive a reading published after it connects.
  • A disconnected link fails the send rather than accepting a reading it has no way to deliver.
Store and forward
  • Peek returns the oldest record, 20.1, and leaves all three readings queued, so a send that fails part-way loses nothing.
  • The queue drains oldest first, 20.1 then 20.4 then 20.2, the order the readings were taken.
  • Popping until it returns nothing leaves the queue empty.
  • A full store refuses the third append and still holds two records, so the caller is told to back off rather than have the oldest reading dropped to make room.
Transport ladder
  • Rungs are tried in the order they were added, and a refusing rung falls through to the next.
  • The first reading arrives on the backhaul's subscriber carrying 21.5, so which link was used is observable rather than assumed.
  • With every rung down, a send is buffered rather than lost, and the ladder reports the one record it is holding.
  • A flush while both links are down forwards nothing and leaves that record waiting in the queue.
  • The next flush forwards one, the gateway receives 21.6, and the queue drops to zero, so the backlog went out exactly once.
Event bus
  • One publish reaches every subscriber, and each reads its own copy of battery.low off its own queue.
  • A subscriber taken later starts at the next event, so its first read is link.up and what went out before it existed is gone for good.
  • The logger still has battery.low waiting after control has read it, so an event is not consumed by whoever reads first.
  • Five events into a buffer of two leave the reader at 3, so a reader that falls behind loses the oldest events, not the newest.
  • All five publishes return with nothing draining the buffer, so a slow subscriber costs itself rather than the publisher.
Engine surface
  • A fault injector sits in the ladder where a plain link would, and the send it refuses comes back as Buffered rather than an error, so the reading is held instead of lost.
  • The reading taken next is buffered too, even though the link would carry it now, and the ladder counts both as queued.
  • A flush forwards the whole backlog and leaves nothing queued behind it.
  • The subscriber reads 20.1 and then 20.4, so the far end sees the readings in the order they were taken, not the order the link became willing to carry them.
Simulators
  • The replay hands back exactly the series it was given: 4 m, 3 m, 1.5 m and 0.5 m, in that order.
  • The recording actuator keeps every command the loop issued: three at one metre per second, then a zero once the 0.5 m reading falls under the metre of clearance the rule drives on.
  • Those three half-second commands dead-reckon to 1.5 m along x and nothing along y, so a straight run stays straight.
  • The turn on the spot puts the heading at 0.5 rad and leaves x at 1.5 m; an integrator that translated on a pure rotation would carry the rover past that and still look self-consistent.

Profiles and robotics#

Device profiles
  • A manifest parses into the name, topic, setpoint policy and sampling schedule the node runs on, with cooling set false marking the output a heater.
  • saver_below never appears in the manifest and still reads 0.5, the documented default, rather than nothing.
  • A reading below the deadband switches the lamp on and raises OutOfRange, so the excursion is reported as well as acted on.
  • A reading inside the safe band raises nothing, so an alert tracks the band rather than firing on every sample.
  • Serializing writes the defaulted threshold out by name, so the shared text names saver_below even though the manifest never did.
ROS 2 rules
  • A token may hold letters, digits and underscores but may not begin with a digit, so /2foo is rejected where /robot1/camera_left/image_raw passes.
  • A leading slash is what makes a name fully qualified, and chatter without one is relative.
  • A topic goes out under rt, a service request under rq and its response under rr, so a request and its response keep one ROS name and still never collide in a DDS partition.
  • std_msgs/msg/String becomes std_msgs::msg::dds_::String_, the dds_ namespace and the trailing underscore included, since a peer matches the whole string.
  • A malformed type name maps to nothing rather than to something that looks plausible.
Zenoh keys
  • * stands for exactly one chunk, so fleet/*/battery covers fleet/n7/battery and not fleet/n7/rack/battery.
  • ** stands for any number of chunks, so fleet/** covers fleet/n7/rack/battery, and fleet/**/battery covers fleet/battery, where it stands for none at all.
  • A repeated wildcard is not canonical. fleet/**/**/battery canonizes to fleet/**/battery, so a router compares subscriptions in that form rather than as written.
  • An empty chunk makes fleet//battery invalid, and canonizing it yields nothing rather than a repaired expression.

Edit this page on GitHub