1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! 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?](#which-pieces-do-i-need) for a one-line map.
//!
//! # The shape of a profile
//!
//! - [`Profile`] is the manifest: plain data a community can publish and share, carrying
//! a [`ControlSpec`], a [`PowerSchedule`], and an optional [`Presentation`]. It
//! serializes to and from JSON with [`Profile::to_json`] and [`Profile::from_json`], so
//! a profile ships as a file and loads onto a device. The presets
//! [`Profile::vaccine_fridge_monitor`], [`Profile::irrigation_node`],
//! [`Profile::well_level`], and [`Profile::flood_sensor`] are convenience constructors
//! for the same data.
//! - [`Node`] is what the runtime assembles from a profile and real components: a
//! [`Sensor`](pamoja_core::Sensor), an [`Actuator`](pamoja_core::Actuator), a
//! [`Transport`](pamoja_core::Transport), and a [`Codec`](pamoja_codec::Codec). Each
//! [`tick`](Node::tick) reads, 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 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`](Node::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`](Presentation::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
//! [`Spark`](Viz::Spark)line, a 270-degree [`Gauge`](Viz::Gauge), a needle
//! [`Dial`](Viz::Dial), a [`Bar`](Viz::Bar), a [`Thermometer`](Viz::Thermometer), a
//! [`Droplet`](Viz::Droplet), a [`Battery`](Viz::Battery), a [`Wind`](Viz::Wind) rotor, a
//! [`Sun`](Viz::Sun), an acoustic [`Wave`](Viz::Wave), a [`Switch`](Viz::Switch) chip, a
//! [`Valve`](Viz::Valve), a hash [`Chain`](Viz::Chain), a [`Mesh`](Viz::Mesh) map, and a
//! [`Count`](Viz::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):
//!
//! ```text
//! 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`](Profile::controller).
//! - **Sharing a recipe?** Write a JSON manifest and load it with [`Profile::from_json`].
//! - **A sensor we do not draw?** Add an [`ElementSpec`] with the [`Viz`] you want.
//! - **Your own look and words?** Add a [`Theme`] and
//! [`with_message`](Presentation::with_message) for custom states and events.
// The public traits this crate composes use `async fn`, matching the core SDK.
pub use ;
pub use ;
pub use ;
pub use ;