pamoja_ffi/power.rs
1//! The C ABI for power-aware scheduling.
2//!
3//! These functions wrap [`pamoja_power`] for callers that reach the SDK through
4//! the flat C boundary: the split between working and sleeping that a duty cycle
5//! describes, and the plan that stretches a work interval as a battery falls.
6//!
7//! Everything here is arithmetic over scalars, so both types cross by value and
8//! nothing allocates. Durations cross as microseconds, which covers intervals
9//! from a radio burst to weeks of deep sleep in a 64-bit unsigned integer.
10
11use core::time::Duration;
12
13use pamoja_power::{DutyCycle, PowerMode, PowerPlan};
14
15/// The split between the time a node works and the time it sleeps.
16#[repr(C)]
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct PamojaDutyCycle {
19 /// How long the node stays awake each period, in microseconds.
20 pub active_us: u64,
21 /// How long it sleeps each period, in microseconds.
22 pub sleep_us: u64,
23}
24
25/// What a node should be doing at the current state of charge.
26#[repr(C)]
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum PamojaPowerMode {
29 /// Full duty, because the charge is healthy.
30 Active = 0,
31 /// Reduced duty, to conserve charge.
32 Saver = 1,
33 /// Minimum duty, to stay alive as long as possible.
34 Critical = 2,
35}
36
37/// The work intervals a node uses in each mode, and where the modes change.
38///
39/// Build one with [`pamoja_power_plan_new`], which applies the default
40/// thresholds, then move them with [`pamoja_power_plan_with_thresholds`].
41#[repr(C)]
42#[derive(Clone, Copy, Debug, PartialEq)]
43pub struct PamojaPowerPlan {
44 /// The interval between work at a healthy charge, in microseconds.
45 pub active_us: u64,
46 /// The interval used to conserve charge, in microseconds.
47 pub saver_us: u64,
48 /// The interval used at a critically low charge, in microseconds.
49 pub critical_us: u64,
50 /// Enter [`PamojaPowerMode::Saver`] below this state of charge.
51 pub saver_below: f32,
52 /// Enter [`PamojaPowerMode::Critical`] below this state of charge.
53 pub critical_below: f32,
54}
55
56/// Creates a duty cycle from the time awake and the time asleep.
57///
58/// # Arguments
59///
60/// * `active_us` - how long the node works each period, in microseconds.
61/// * `sleep_us` - how long it sleeps each period, in microseconds.
62///
63/// # Returns
64///
65/// The duty cycle.
66#[no_mangle]
67pub extern "C" fn pamoja_duty_cycle_new(active_us: u64, sleep_us: u64) -> PamojaDutyCycle {
68 let duty = DutyCycle::new(
69 Duration::from_micros(active_us),
70 Duration::from_micros(sleep_us),
71 );
72 PamojaDutyCycle {
73 active_us: micros(duty.active()),
74 sleep_us: micros(duty.sleep()),
75 }
76}
77
78/// Creates a duty cycle that spends a fraction of each period awake.
79///
80/// # Arguments
81///
82/// * `period_us` - the whole period, in microseconds.
83/// * `fraction` - the share of the period spent awake, clamped to 0.0 through 1.0.
84///
85/// # Returns
86///
87/// The duty cycle.
88#[no_mangle]
89pub extern "C" fn pamoja_duty_cycle_from_fraction(
90 period_us: u64,
91 fraction: f32,
92) -> PamojaDutyCycle {
93 let duty = DutyCycle::from_fraction(Duration::from_micros(period_us), fraction);
94 PamojaDutyCycle {
95 active_us: micros(duty.active()),
96 sleep_us: micros(duty.sleep()),
97 }
98}
99
100/// Returns the whole period of a duty cycle, awake plus asleep.
101///
102/// # Arguments
103///
104/// * `duty` - the duty cycle.
105///
106/// # Returns
107///
108/// The period in microseconds.
109#[no_mangle]
110pub extern "C" fn pamoja_duty_cycle_period_us(duty: PamojaDutyCycle) -> u64 {
111 micros(cycle(duty).period())
112}
113
114/// Returns the share of a period a duty cycle spends awake.
115///
116/// # Arguments
117///
118/// * `duty` - the duty cycle.
119///
120/// # Returns
121///
122/// The fraction from 0.0 through 1.0, or `0.0` if the period is zero.
123#[no_mangle]
124pub extern "C" fn pamoja_duty_cycle_fraction(duty: PamojaDutyCycle) -> f32 {
125 cycle(duty).fraction()
126}
127
128/// Creates a power plan from its three work intervals, with default thresholds.
129///
130/// The defaults enter [`PamojaPowerMode::Saver`] below 50% charge and
131/// [`PamojaPowerMode::Critical`] below 20%.
132///
133/// # Arguments
134///
135/// * `active_us` - the interval at a healthy charge, in microseconds.
136/// * `saver_us` - the longer interval used to conserve, in microseconds.
137/// * `critical_us` - the longest interval, in microseconds.
138///
139/// # Returns
140///
141/// The power plan.
142#[no_mangle]
143pub extern "C" fn pamoja_power_plan_new(
144 active_us: u64,
145 saver_us: u64,
146 critical_us: u64,
147) -> PamojaPowerPlan {
148 PamojaPowerPlan {
149 active_us,
150 saver_us,
151 critical_us,
152 saver_below: 0.5,
153 critical_below: 0.2,
154 }
155}
156
157/// Returns a plan with the state-of-charge thresholds moved.
158///
159/// # Arguments
160///
161/// * `plan` - the plan to adjust.
162/// * `saver_below` - enter [`PamojaPowerMode::Saver`] below this charge.
163/// * `critical_below` - enter [`PamojaPowerMode::Critical`] below this charge.
164///
165/// # Returns
166///
167/// The adjusted plan.
168#[no_mangle]
169pub extern "C" fn pamoja_power_plan_with_thresholds(
170 plan: PamojaPowerPlan,
171 saver_below: f32,
172 critical_below: f32,
173) -> PamojaPowerPlan {
174 PamojaPowerPlan {
175 saver_below,
176 critical_below,
177 ..plan
178 }
179}
180
181/// Returns the mode a plan calls for at a state of charge.
182///
183/// # Arguments
184///
185/// * `plan` - the power plan.
186/// * `soc` - the battery state of charge, from 0.0 through 1.0.
187///
188/// # Returns
189///
190/// The mode the node should run in.
191#[no_mangle]
192pub extern "C" fn pamoja_power_plan_mode(plan: PamojaPowerPlan, soc: f32) -> PamojaPowerMode {
193 mode(rust_plan(plan).mode(soc))
194}
195
196/// Returns the mode a plan calls for, easing off one step while charging.
197///
198/// A node taking charge is heading the right way, so it moves one step toward
199/// full duty rather than holding at what the charge alone would call for.
200///
201/// # Arguments
202///
203/// * `plan` - the power plan.
204/// * `soc` - the battery state of charge, from 0.0 through 1.0.
205/// * `charging` - `1` if the node is charging, `0` if it is not.
206///
207/// # Returns
208///
209/// The mode the node should run in.
210#[no_mangle]
211pub extern "C" fn pamoja_power_plan_mode_while_charging(
212 plan: PamojaPowerPlan,
213 soc: f32,
214 charging: u8,
215) -> PamojaPowerMode {
216 mode(rust_plan(plan).mode_while_charging(soc, charging != 0))
217}
218
219/// Returns the work interval a plan uses in a mode.
220///
221/// # Arguments
222///
223/// * `plan` - the power plan.
224/// * `mode` - the mode to look up.
225///
226/// # Returns
227///
228/// The interval in microseconds.
229#[no_mangle]
230pub extern "C" fn pamoja_power_plan_interval_for_us(
231 plan: PamojaPowerPlan,
232 mode: PamojaPowerMode,
233) -> u64 {
234 micros(rust_plan(plan).interval_for(rust_mode(mode)))
235}
236
237/// Returns the work interval a plan calls for at a state of charge.
238///
239/// # Arguments
240///
241/// * `plan` - the power plan.
242/// * `soc` - the battery state of charge, from 0.0 through 1.0.
243///
244/// # Returns
245///
246/// The interval in microseconds.
247#[no_mangle]
248pub extern "C" fn pamoja_power_plan_interval_us(plan: PamojaPowerPlan, soc: f32) -> u64 {
249 micros(rust_plan(plan).interval(soc))
250}
251
252/// Rebuilds the Rust duty cycle from the fields that crossed the boundary.
253fn cycle(duty: PamojaDutyCycle) -> DutyCycle {
254 DutyCycle::new(
255 Duration::from_micros(duty.active_us),
256 Duration::from_micros(duty.sleep_us),
257 )
258}
259
260/// Rebuilds the Rust power plan from the fields that crossed the boundary.
261fn rust_plan(plan: PamojaPowerPlan) -> PowerPlan {
262 PowerPlan::new(
263 Duration::from_micros(plan.active_us),
264 Duration::from_micros(plan.saver_us),
265 Duration::from_micros(plan.critical_us),
266 )
267 .thresholds(plan.saver_below, plan.critical_below)
268}
269
270/// Narrows a duration to the microseconds the boundary carries.
271fn micros(duration: Duration) -> u64 {
272 duration.as_micros().min(u128::from(u64::MAX)) as u64
273}
274
275/// Maps a Rust power mode onto the value that crosses the boundary.
276fn mode(mode: PowerMode) -> PamojaPowerMode {
277 match mode {
278 PowerMode::Active => PamojaPowerMode::Active,
279 PowerMode::Saver => PamojaPowerMode::Saver,
280 PowerMode::Critical => PamojaPowerMode::Critical,
281 }
282}
283
284/// Maps a boundary power mode back onto the Rust one.
285fn rust_mode(mode: PamojaPowerMode) -> PowerMode {
286 match mode {
287 PamojaPowerMode::Active => PowerMode::Active,
288 PamojaPowerMode::Saver => PowerMode::Saver,
289 PamojaPowerMode::Critical => PowerMode::Critical,
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn a_fraction_splits_the_period() {
299 let duty = pamoja_duty_cycle_from_fraction(1_000_000, 0.25);
300 assert_eq!(duty.active_us, 250_000);
301 assert_eq!(duty.sleep_us, 750_000);
302 assert_eq!(pamoja_duty_cycle_period_us(duty), 1_000_000);
303 assert!((pamoja_duty_cycle_fraction(duty) - 0.25).abs() < 1e-6);
304 }
305
306 #[test]
307 fn a_falling_charge_stretches_the_interval() {
308 let plan = pamoja_power_plan_new(60_000_000, 300_000_000, 3_600_000_000);
309
310 assert_eq!(pamoja_power_plan_mode(plan, 0.9), PamojaPowerMode::Active);
311 assert_eq!(pamoja_power_plan_mode(plan, 0.3), PamojaPowerMode::Saver);
312 assert_eq!(pamoja_power_plan_mode(plan, 0.1), PamojaPowerMode::Critical);
313 assert_eq!(pamoja_power_plan_interval_us(plan, 0.1), 3_600_000_000);
314 }
315
316 #[test]
317 fn charging_eases_a_low_node_up_one_step() {
318 let plan = pamoja_power_plan_new(60_000_000, 300_000_000, 3_600_000_000);
319
320 assert_eq!(
321 pamoja_power_plan_mode_while_charging(plan, 0.1, 1),
322 PamojaPowerMode::Saver
323 );
324 assert_eq!(
325 pamoja_power_plan_mode_while_charging(plan, 0.3, 1),
326 PamojaPowerMode::Active
327 );
328 assert_eq!(
329 pamoja_power_plan_mode_while_charging(plan, 0.1, 0),
330 PamojaPowerMode::Critical
331 );
332 }
333
334 #[test]
335 fn moved_thresholds_change_where_the_modes_meet() {
336 let plan = pamoja_power_plan_with_thresholds(
337 pamoja_power_plan_new(60_000_000, 300_000_000, 3_600_000_000),
338 0.8,
339 0.4,
340 );
341
342 assert_eq!(pamoja_power_plan_mode(plan, 0.7), PamojaPowerMode::Saver);
343 assert_eq!(pamoja_power_plan_mode(plan, 0.3), PamojaPowerMode::Critical);
344 }
345}