pamoja-profile
Named, ready-to-run device profiles for pamoja: assemble a sensor, actuator, transport, codec, and power schedule into a working node.
The same capability in every language
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-profile |
reference, docs.rs, install |
| TypeScript | @pamoja/profile |
reference, install |
| Python | pamoja-profile |
reference, install |
| C# | Pamoja.Profile |
reference, install |
Device profiles: named, ready-to-run nodes assembled from pamoja capabilities.
Most people who can put a sensor to good use are not electrical engineers, and the gap between "I can read a sensor" and "I built something that works and warns me when it fails" is wiring, tuning, and glue code. A device profile closes that gap. It is a named, pre-wired bundle - a control policy, a publish topic, and a power schedule - that a builder instantiates instead of choosing algorithms and constants by hand.
This guide runs from the simplest use to a fully themed dashboard. Skip to Which pieces do I need? for a one-line map.
The shape of a profile
Profileis the manifest: plain data a community can publish and share, carrying aControlSpec, aPowerSchedule, and an optionalPresentation. It serializes to and from JSON withProfile::to_jsonandProfile::from_json, so a profile ships as a file and loads onto a device. The presetsProfile::vaccine_fridge_monitor,Profile::irrigation_node,Profile::well_level, andProfile::flood_sensorare convenience constructors for the same data.Nodeis what the runtime assembles from a profile and real components: aSensor, anActuator, aTransport, and aCodec. Eachtickreads, decides, drives the output, and publishes.
The decision logic is a Controller that composes the pamoja-kit helpers, so a
profile is glue over field-tested math rather than new behavior. Its I/O is async; its
decisions are synchronous and hardware-free, so a whole control policy is unit-testable
with no devices and no network.
Start simple: pick a preset
The quickest path is a named preset. Hand its controller a reading and it decides:
use ;
let mut control = vaccine_fridge_monitor.controller;
// A warm fridge: the cooler runs and a spoilage excursion is flagged.
let reaction = control.evaluate;
assert_eq!;
assert!;
The manifest: write it, share it, load it
A profile is just data, so a community can write one as JSON, store it in a file, and
share it - no code. Profile::from_json loads it and Profile::to_json writes it
back; the power thresholds are optional and default when omitted.
use Profile;
let manifest = r#"{
"name": "rain-tank",
"topic": "water/tank/level",
"control": { "kind": "level", "empty": 0.0, "warn_within": 5 },
"power": { "active_secs": 600, "saver_secs": 1800, "critical_secs": 3600 }
}"#;
let profile = from_json.expect;
assert_eq!;
assert!;
Control policies
Every profile names one ControlSpec, the rule applied to each reading:
Setpointholds a value by switching an output on and off (a fridge's cooler, an irrigation valve) and alerts when the reading leaves a safe band.Levelwatches a falling level and warns before it reaches empty.Surgewarns when a reading changes faster than a safe rate (a flash flood).Monitoronly reports, with no output and no alert.
Every field is public, so a deployment can build or tune a policy in place:
use ;
// Hold soil moisture near 35% by opening a valve - a "heater" for moisture.
let profile = Profile ;
let mut control = profile.controller;
assert_eq!; // dry: the valve opens
Power: sampling that follows the battery
A PowerSchedule sets how often a node samples as its battery drains - often when
healthy, sparingly when low - and eases back toward the active cadence while charging.
Node::schedule turns it into the power mode and the interval to wait before the
next tick.
Custom dashboard elements
The local-first dashboard (the pamoja-dashboard crate) draws a built-in set of sensor
types. When a deployment measures something beyond it, the profile declares the extra
as a Presentation, and the dashboard renders it with no page change. Each
ElementSpec names a stable key and unit, the graphic to draw it with (Viz), an
optional safe band, a label (with optional per-locale labels), whether it is a node
stat, and which groups it is offered on (Scope). A Theme tints the console, and
with_message localizes any custom state or event code
the profile emits.
The full set of graphics is Viz::ALL, fifteen hand-drawn instruments: a
Sparkline, a 270-degree Gauge, a needle
Dial, a Bar, a Thermometer, a
Droplet, a Battery, a Wind rotor, a
Sun, an acoustic Wave, a Switch chip, a
Valve, a hash Chain, a Mesh map, and a
Count. Each renders to a stable kind (Viz::kind) the page draws.
use ;
let profile = well_level.with_presentation;
let turbidity = &profile.presentation.as_ref.unwrap.elements;
assert_eq!; // Gauge draws as the radial arch
assert_eq!; // fifteen graphics to choose from
Show it on a dashboard, wire it to your project
The dashboard side lives in the pamoja-dashboard crate: build a catalog from your
profiles and serve it, gate which sensors a client may add, and feed live readings into
the graphic a profile chose. This is the whole loop (its examples/gateway.rs is a
runnable version):
use pamoja_dashboard::{Assets, Catalog, Fleet, LinkKind, Reading, Sensor, Server, Viz};
let fleet = Fleet::builder()
.org("farm", "Pamoja farm")
.group("farm", "field", "Field node", LinkKind::Lora)
.sensor("field", Sensor::new("turbidity",
Reading::new("water_turbidity", 2.4, "ntu").with_band(0.0, 5.0).with_viz(Viz::Gauge)))
.build();
// A real device only accepts the sensors it can bind; anything else is refused.
fleet.allow_sensors(["water_turbidity", "drip_valve"]);
Server::new(fleet, Assets::Embedded)
.with_catalog(Catalog::from_profiles(&[&profile])) // served at GET /catalog
.run("0.0.0.0:80")
.unwrap();
From your own sampling loop you push each real reading in with report_reading, and the
dashboard reads it; control actions queue back for you to apply. See the
pamoja-dashboard crate for the full push model, pairing, and the served catalog.
Which pieces do I need?
- Just want it to work? Pick a preset and call
controller. - Sharing a recipe? Write a JSON manifest and load it with
Profile::from_json. - A sensor we do not draw? Add an
ElementSpecwith theVizyou want. - Your own look and words? Add a
Themeandwith_messagefor custom states and events.
License
MIT - part of the pamoja workspace: one memory-safe Rust core with bindings for every language.