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.
examples/batched_telemetry.rsMetered-link encoding: pack a batch of readings into a fraction of the bytes.
cargo run -p pamoja-examples --example batched_telemetryexamples/conformance.rsThe whole SDK in one run: a cold-chain node from sensor to gateway over loopback.
cargo run -p pamoja-examples --example conformanceexamples/conformance_vectors.rsRegenerates the cross-language conformance vectors.
cargo run -p pamoja-examples --example conformance_vectorsexamples/degraded_link.rsOffline-first survives a flaky link: buffer, retry over a degraded link, lose nothing.
cargo run -p pamoja-examples --example degraded_linkexamples/device_profile.rsA device profile assembled into a ready-to-run cold-chain node, over loopback.
cargo run -p pamoja-examples --example device_profileexamples/lora_budget.rsLoRa airtime and duty cycle: what it costs to send a batch over a long-range link.
cargo run -p pamoja-examples --example lora_budgetcargo run -p pamoja-examples --example mavlink_sitlexamples/robot_waypoint.rsDrive a rover safely, dead-reckon where it is, steer to a waypoint, and speak ROS 2.
cargo run -p pamoja-examples --example robot_waypointexamples/signed_audit.rsA tamper-evident cold-chain log: signed, hash-chained fridge readings.
cargo run -p pamoja-examples --example signed_auditexamples/signed_telemetry.rsTamper-evident telemetry: a device signs each reading, a gateway verifies it.
cargo run -p pamoja-examples --example signed_telemetryexamples/signed_update.rsUpdating a device in the field, including the update that goes wrong.
cargo run -p pamoja-examples --example signed_updateexamples/store_and_forward.rsOffline-first store-and-forward, end to end, with no hardware and no broker.
cargo run -p pamoja-examples --example store_and_forwardcargo run -p pamoja-examples --example telemetryGuide 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#
- 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#
- 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 of100sets, and that error is what the packing trades for the bytes.
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#
- 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.
- 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.
- 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.
- A device at
0x76is written to as0xECand read from as0xED, one byte either way, which is why a datasheet and a bus capture rarely print the same number. 0x76is a device address and0x78is not, because0x78opens 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#
- 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.
- 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,
1000then1100then0100, 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#
- 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.
- A device holding nothing but the root key verifies the accept and reads the address
0x26012E43out 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.
- 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.
- 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.
MAVLink#
- Fed noise and a copy whose checksum fails, the parser still recovers the frame behind them, and it decodes back to the vehicle's heartbeat: Rust compares every field, the other three the type it reports.
- The recovered frame carries the message id the dialect gives
HEARTBEAT, so the header agrees with the payload it wraps. - The first arm request goes out with confirmation
0, and a timeout hands back1for the resend, so the vehicle can tell a retry from a second, deliberate command. - An acknowledgement for
NAV_TAKEOFFcomes back unrelated, so another command's answer leaves this exchange still waiting. - The acknowledgement naming
COMPONENT_ARM_DISARMends the exchange and hands back the result the vehicle sent.
Trust and operation#
- 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.
- 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.
- 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.
- 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_fractiondivides the period it is given, so a quarter-duty second is 250ms awake and 750ms asleep.
- A link cost sets the bar:
Meteredputs it atInfoandExpensiveraises it toWarn, so how much a node says follows what the link costs and not the level it was built with. - The same
reading.okis 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.lowat0.18, so a transport has the number that triggered it. - A held-back event is still counted: two readings were recorded at
Infoeven 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.
Offlineis the last rung, holding back everything belowError, so a node with no link keeps its failures and nothing else.
Transports and testing#
- 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 thesensors/+/temperaturefilter 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.
- 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.
- 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/rawreading 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.
- 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.1then20.4then20.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.
- 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.
- One publish reaches every subscriber, and each reads its own copy of
battery.lowoff its own queue. - A subscriber taken later starts at the next event, so its first read is
link.upand what went out before it existed is gone for good. - The logger still has
battery.lowwaiting 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.
- A fault injector sits in the ladder where a plain link would, and the send it refuses comes back as
Bufferedrather 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.1and then20.4, so the far end sees the readings in the order they were taken, not the order the link became willing to carry them.
- 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#
- A manifest parses into the name, topic, setpoint policy and sampling schedule the node runs on, with
coolingset false marking the output a heater. saver_belownever appears in the manifest and still reads0.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_beloweven though the manifest never did.
- A token may hold letters, digits and underscores but may not begin with a digit, so
/2foois rejected where/robot1/camera_left/image_rawpasses. - A leading slash is what makes a name fully qualified, and
chatterwithout one is relative. - A topic goes out under
rt, a service request underrqand its response underrr, so a request and its response keep one ROS name and still never collide in a DDS partition. std_msgs/msg/Stringbecomesstd_msgs::msg::dds_::String_, thedds_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.
*stands for exactly one chunk, sofleet/*/batterycoversfleet/n7/batteryand notfleet/n7/rack/battery.**stands for any number of chunks, sofleet/**coversfleet/n7/rack/battery, andfleet/**/batterycoversfleet/battery, where it stands for none at all.- A repeated wildcard is not canonical.
fleet/**/**/batterycanonizes tofleet/**/battery, so a router compares subscriptions in that form rather than as written. - An empty chunk makes
fleet//batteryinvalid, and canonizing it yields nothing rather than a repaired expression.