Skip to main content

Profile

Struct Profile 

Source
pub struct Profile {
    pub name: String,
    pub description: Option<String>,
    pub reads: Option<Reads>,
    pub topic: String,
    pub control: ControlSpec,
    pub power: PowerSchedule,
    pub presentation: Option<Presentation>,
}
Expand description

A named, pre-wired bundle of control policy, publish topic, and power schedule.

A profile is the unit a builder instantiates instead of wiring pins and tuning constants, and it is plain data: it serializes to and from a manifest a community can write, store in a file, and share. Pick a preset such as vaccine_fridge_monitor or load one with from_json, hand it a sensor, an actuator, a transport, and a codec, and the resulting Node reads, decides, drives the output, and publishes on its own. Every field is public, so a deployment can adjust the policy, topic, or power schedule in place.

A manifest is written in the format FORMAT names, and may say so with a $schema naming Profile::SCHEMA, which an editor reads to check the file as it is typed. A field the format does not have is refused, with the nearest one it does, so a misspelling never leaves a default in place without a word.

§Examples

use pamoja_profile::{ControlSpec, Profile};

let profile = Profile::vaccine_fridge_monitor();
assert_eq!(profile.name, "vaccine-fridge-monitor");
assert!(matches!(profile.control, ControlSpec::Setpoint { .. }));

Fields§

§name: String

A stable, human-readable name, such as "vaccine-fridge-monitor".

§description: Option<String>

What the profile is for, in a sentence or two: what it watches or holds, and what it does when a reading crosses a line. A manifest shared in a catalog explains itself with it; a profile built in code may leave it None.

§reads: Option<Reads>

What the profile reads, the quantity and the unit its numbers are in. A profile whose node reads a whole measurement through a policy of its own may leave it None.

§topic: String

The topic each reading is published to.

§control: ControlSpec

The control policy applied to each reading.

§power: PowerSchedule

The power schedule that sets how often the node samples as the battery drains.

§presentation: Option<Presentation>

How this profile presents itself on the dashboard - its custom sensors, node stats, and theme. A profile that introduces no element beyond the dashboard’s built-in set leaves this None.

Implementations§

Source§

impl Profile

Source

pub const SCHEMA: &'static str = "https://pamoja.molex.cloud/schema/profile-1.json"

The address of the published JSON Schema for the manifest format this build writes, which to_json names as the manifest’s $schema.

Source

pub fn new( name: impl Into<String>, topic: impl Into<String>, control: ControlSpec, power: PowerSchedule, ) -> Self

Creates a profile of the caller’s own from its parts, with no description, no declaration of what it reads, and no presentation.

§Arguments
  • name - a stable, human-readable name, such as "raised-bed-drip".
  • topic - the topic each reading is published to.
  • control - the control policy applied to each reading.
  • power - how often the node samples as the battery drains.
§Returns

The profile. with_description, with_reads, and with_presentation add the rest.

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

let drip = Profile::new(
    "raised-bed-drip",
    "garden/bed-1/moisture",
    ControlSpec::Setpoint { setpoint: 37.5, hysteresis: 7.5, cooling: false, safe_band: 15.0 },
    PowerSchedule::new(300, 1800, 3600),
)
.with_reads("soil_moisture", "percent");
assert_eq!(drip.name, "raised-bed-drip");
assert_eq!(Profile::from_json(&drip.to_json()?)?, drip);
Source

pub fn with_reads( self, quantity: impl Into<String>, unit: impl Into<String>, ) -> Self

Declares what the profile reads.

§Arguments
  • quantity - the quantity its control decides on, such as "temperature".
  • unit - the unit its numbers are in, such as "celsius".
§Returns

The profile, for chaining.

§Examples
use pamoja_profile::Profile;

let fridge = Profile::vaccine_fridge_monitor();
let reads = fridge.reads.as_ref().expect("every preset says what it reads");
assert_eq!((reads.quantity.as_str(), reads.unit.as_str()), ("temperature", "celsius"));
Source

pub fn vaccine_fridge_monitor() -> Self

A cold-chain fridge monitor: hold 5 C and alert on a spoilage excursion.

Switches a cooler to hold the contents near 5 C and raises an Alert::OutOfRange the moment the temperature leaves the 2-8 C safe range. Data integrity outweighs power here, so it keeps sampling often even as the battery drains.

§Returns

The cold-chain monitoring profile.

Source

pub fn irrigation_node() -> Self

An irrigation node: hold soil moisture near a target by opening a valve.

Treats the valve as a “heater” for soil moisture, opening it when the soil dries below the band and closing it once it is wet enough, and alerts if the soil falls critically dry. Samples less often than the fridge, since soil changes slowly and battery life matters more.

§Returns

The irrigation profile.

Source

pub fn well_level() -> Self

A well-level monitor: report depth and warn before the well runs dry.

Observes the water level without driving an output and raises an Alert::RunningOut once the level is on course to reach the dry mark within a few more samples.

§Returns

The well-level monitoring profile.

Source

pub fn flood_sensor() -> Self

A flash-flood sensor: warn when a river level rises dangerously fast.

Watches a river or stream gauge and raises an Alert::ChangingFast when the level rises more than 0.3 m in a single sample, the signature of a flash flood. It samples often, because a flood gives little warning.

§Returns

The flash-flood monitoring profile.

§Examples
use pamoja_profile::{Alert, Profile};

let mut control = Profile::flood_sensor().controller()?;
control.evaluate(1.0); // first fix establishes the level
let reaction = control.evaluate(1.5); // the river jumped 0.5 m
assert!(matches!(reaction.alert, Some(Alert::ChangingFast { .. })));
Source

pub fn controller(&self) -> Result<Controller>

Assembles this profile’s ControlSpec into a live Controller.

Each call builds a new controller, so keep the one it returns for the life of the node. One built again for each reading forgets whether its output was on and what the reading before was, so a heater never holds through its deadband and a level or a surge never has a previous reading to measure against.

§Returns

A fresh controller implementing the profile’s policy, with its control state reset.

§Errors

Returns Error::Codec when the profile names a custom kind, which no built-in controller decides: a PolicyRegistry that registers the kind resolves it to the code that does. The reason names the kind, and the built-in kind it is probably a misspelling of when there is one.

§Examples
use pamoja_profile::{ControlSpec, Params, PowerSchedule, Profile};

let typo = Profile::new(
    "cellar-heater",
    "home/cellar/temperature",
    ControlSpec::custom("setpiont", Params::new())?,
    PowerSchedule::new(300, 900, 1800),
);
let refused = typo.controller().unwrap_err().to_string();
assert!(refused.contains("did you mean `setpoint`?"), "{refused}");
Source

pub fn with_presentation(self, presentation: Presentation) -> Self

Attaches a dashboard Presentation declaring this profile’s custom elements.

A profile that measures something the dashboard does not draw out of the box - a turbidity probe, a custom node stat - carries the graphic, band, and label for it here, so the dashboard offers and renders it with no code.

§Arguments
  • presentation - how this profile presents itself on the dashboard.
§Returns

The profile, for chaining.

§Examples
use pamoja_profile::{ElementSpec, Presentation, Profile, Viz};

// `with_presentation` sets the whole presentation, so on a shipped preset it
// replaces the elements the preset came with: a well-level profile drawn this
// way shows only the turbidity gauge. `with_element` adds one and keeps the rest.
let replaced = Profile::well_level().with_presentation(
    Presentation::new().with_element(
        ElementSpec::new("water_turbidity", "ntu", "Turbidity", Viz::Gauge)
            .with_band(0.0, 5.0),
    ),
);
let elements = &replaced.presentation.unwrap().elements;
assert_eq!(elements.len(), 1);
assert_eq!(elements[0].viz.kind(), "radial");
Source

pub fn with_element(self, element: ElementSpec) -> Self

Adds one element to how this profile presents itself, keeping the rest.

with_presentation replaces a profile’s whole presentation, which drops what a preset already declared. This adds to it, so a deployment can hang its own gauge off a shipped profile and keep the profile’s own elements, labels, and theme.

§Arguments
  • element - the custom sensor or node stat to add.
§Returns

The profile, for chaining.

§Examples
use pamoja_profile::{ElementSpec, Profile, Viz};

// The shipped irrigation profile, plus the turbidity probe this farm also fitted.
let profile = Profile::irrigation_node().with_element(
    ElementSpec::new("water_turbidity", "ntu", "Turbidity", Viz::Gauge).with_band(0.0, 5.0),
);
let keys: Vec<&str> = profile
    .presentation
    .as_ref()
    .unwrap()
    .elements
    .iter()
    .map(|element| element.key.as_str())
    .collect();
assert_eq!(keys, ["soil_moisture", "drip_valve", "water_turbidity"]);
Source

pub fn with_message( self, code: impl Into<String>, text: impl Into<LocalizedText>, ) -> Self

Adds the wording for one state or event code this profile emits.

The dashboard ships no translation for a code it never knew, so a profile that raises its own supplies the words here. Like with_element this keeps whatever the profile already declared.

§Arguments
  • code - the message key, a state. or event. code.
  • text - one text for every locale, or per-locale text.
§Returns

The profile, for chaining.

§Examples
use pamoja_profile::Profile;

let profile = Profile::irrigation_node()
    .with_message("state.flushing", [("en", "Flushing"), ("sw", "Inasafishwa")]);
assert!(profile.presentation.unwrap().messages.contains_key("state.flushing"));
Source

pub fn with_description(self, description: impl Into<String>) -> Self

Sets what this profile is for, in a sentence or two.

A manifest shared in a catalog carries its purpose with it, so a reader knows what the profile watches or holds and what it does about it before opening the file.

§Arguments
  • description - the purpose, in plain words.
§Returns

The profile, for chaining.

§Examples
use pamoja_profile::Profile;

let profile = Profile::well_level()
    .with_description("Warns before the village borehole runs dry.");
assert!(profile.to_json().unwrap().contains("borehole"));
Source

pub fn check(&self) -> Result<()>

Checks the profile for what a manifest can say that a node or a dashboard could not make sense of.

A manifest is written by hand and shared, so from_json runs this on every one it loads, and each language’s constructor runs it on the parts it is given. A profile built in Rust can run it too.

§Returns

Nothing when the profile is usable.

§Errors

Returns Error::Codec naming the first problem:

  • an empty name, or a topic that is empty or holds + or #, which make it a filter rather than a place to publish;
  • a reads whose quantity or unit is not lowercase words joined by underscores;
  • a control value that is not a finite number, a hysteresis of zero or less, a safe band narrower than the hysteresis, a level that warns within no samples, a surge limit of zero or less, or a custom kind with no name;
  • a sampling interval of zero, intervals that shorten as the battery drains, a saver threshold outside 0 to 1, or a critical threshold that is not between 0 and the saver threshold;
  • anything Presentation::check refuses in the presentation.
§Examples
use pamoja_profile::{ControlSpec, PowerSchedule, Profile};

// A deadband of zero switches the output on and off on every reading near the
// setpoint, which wears out a relay.
let chattering = Profile::new(
    "cellar-heater",
    "home/cellar/temperature",
    ControlSpec::Setpoint { setpoint: 8.0, hysteresis: 0.0, cooling: false, safe_band: 5.0 },
    PowerSchedule::new(300, 900, 1800),
);
let refused = chattering.check().unwrap_err().to_string();
assert!(refused.contains("the output chatters at the setpoint"));
Source§

impl Profile

Source

pub fn from_json(manifest: &str) -> Result<Self>

Loads a profile from a JSON manifest.

This is how a shared profile reaches a device: a community publishes a manifest file, and the runtime loads it into a profile to assemble a node from.

§Arguments
  • manifest - the JSON text of the profile.
§Returns

The profile described by manifest.

§Errors

Returns Error::Codec if manifest is not valid JSON, does not describe a profile, is written in a format this build does not read, carries a field the format does not have, or describes a profile check refuses. A misspelled field or value is reported with the name it was probably meant to be.

§Examples
use pamoja_profile::Profile;

// A well-level monitor, shared as a manifest. The power thresholds are
// optional and default when omitted.
let manifest = r#"{
    "$schema": "https://pamoja.molex.cloud/schema/profile-1.json",
    "name": "tank-level",
    "reads": { "quantity": "water_level", "unit": "meter" },
    "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("valid manifest");
assert_eq!(profile.name, "tank-level");

let mut control = profile.controller().expect("a built-in kind");
control.evaluate(10.0); // first reading establishes a level
assert!(control.evaluate(2.0).alert.is_some()); // falling fast toward empty

// A misspelled field is refused with the one it was meant to be, rather than
// leaving the default in its place.
let typo = manifest.replace("\"warn_within\"", "\"warn_withn\"");
let refused = Profile::from_json(&typo).unwrap_err().to_string();
assert!(refused.contains("did you mean `warn_within`?"), "{refused}");
Source

pub fn to_json(&self) -> Result<String>

Serializes this profile to a JSON manifest a community can share.

§Returns

The pretty-printed JSON text of the profile.

§Errors

Returns Error::Codec if the profile cannot be serialized.

Trait Implementations§

Source§

impl Clone for Profile

Source§

fn clone(&self) -> Profile

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Profile

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Profile

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for Profile

Source§

fn eq(&self, other: &Profile) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Profile

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Profile

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.