Skip to main content

Crate pamoja_profile

Crate pamoja_profile 

Source
Expand description

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

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 pamoja_profile::{Alert, Profile};

let mut control = Profile::vaccine_fridge_monitor().controller();

// A warm fridge: the cooler runs and a spoilage excursion is flagged.
let reaction = control.evaluate(9.0);
assert_eq!(reaction.actuator, Some(true));
assert!(matches!(reaction.alert, Some(Alert::OutOfRange { .. })));

§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 pamoja_profile::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 = Profile::from_json(manifest).expect("a valid manifest");
assert_eq!(profile.name, "rain-tank");
assert!(profile.to_json().unwrap().contains("rain-tank"));

§Control policies

Every profile names one ControlSpec, the rule applied to each reading:

  • Setpoint holds 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.
  • Level watches a falling level and warns before it reaches empty.
  • Surge warns when a reading changes faster than a safe rate (a flash flood).
  • Monitor only reports, with no output and no alert.

Every field is public, so a deployment can build or tune a policy in place:

use pamoja_profile::{ControlSpec, PowerSchedule, Profile};

// Hold soil moisture near 35% by opening a valve - a "heater" for moisture.
let profile = Profile {
    name: "drip-node".to_owned(),
    topic: "farm/soil-moisture".to_owned(),
    control: ControlSpec::Setpoint { setpoint: 35.0, hysteresis: 5.0, cooling: false, safe_band: 25.0 },
    power: PowerSchedule::new(300, 1800, 3600),
    presentation: None,
};
let mut control = profile.controller();
assert_eq!(control.evaluate(28.0).actuator, Some(true)); // 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 pamoja_profile::{ElementSpec, Presentation, Profile, Scope, Theme, Viz};

let profile = Profile::well_level().with_presentation(
    Presentation::new()
        // A turbidity probe drawn as a gauge. WHO drinking-water turbidity stays under 5 NTU.
        .with_element(
            ElementSpec::new("water_turbidity", "ntu", "Turbidity", Viz::Gauge)
                .with_band(0.0, 5.0)
                .with_locale_label("fr", "Turbidité"),
        )
        // A node stat (telemetry about the node itself), offered only on mesh links.
        .with_element(
            ElementSpec::new("packets_dropped", "count", "Packets dropped", Viz::Count)
                .as_stat()
                .on(Scope::Links(vec!["mesh".to_owned()])),
        )
        // Words for a custom state the profile emits, and a brand accent.
        .with_message("state.flushing", "Flushing")
        .with_theme(Theme { accent: Some("#3fb1c8".to_owned()), ..Theme::default() }),
);

let turbidity = &profile.presentation.as_ref().unwrap().elements[0];
assert_eq!(turbidity.viz.kind(), "radial"); // Gauge draws as the radial arch
assert_eq!(Viz::ALL.len(), 15);             // 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?

Structs§

Controller
The assembled, stateful decision logic of a profile.
ElementSpec
A custom sensor or node stat a profile contributes to the dashboard.
NoActuator
An actuator that accepts and ignores commands.
Node
A profile assembled around the components that make it run.
PowerSchedule
How often a node samples as its battery drains, in plain seconds.
Presentation
How a profile presents itself on the dashboard: its custom elements and theme.
Profile
A named, pre-wired bundle of control policy, publish topic, and power schedule.
Reaction
The outcome of evaluating one reading against a profile’s control policy.
Theme
A small set of theme tokens a profile can set on the dashboard.

Enums§

Alert
An alert raised when a reading crosses a profile’s safety threshold.
ControlSpec
How a profile turns each reading into control output and alerts.
LocalizedText
A piece of human-facing text a profile supplies, either one string for every locale or a per-locale map.
Scope
Which groups a declared element is offered on when a user adds a sensor.
Viz
The graphic a reading is drawn with on the dashboard.