1use std::ffi::c_char;
25use std::ptr;
26
27use pamoja_profile::{Alert, ControlSpec, Controller, PowerSchedule, Profile};
28
29use crate::power::PamojaPowerPlan;
30use crate::{read_str, set_last_error, PamojaStatus, PamojaString};
31
32#[repr(C)]
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum PamojaControlKind {
36 Setpoint = 0,
38 Level = 1,
40 Surge = 2,
42 Monitor = 3,
44}
45
46#[repr(C)]
50#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct PamojaControlSpec {
52 pub kind: PamojaControlKind,
54 pub setpoint: f32,
56 pub hysteresis: f32,
58 pub cooling: bool,
61 pub safe_band: f32,
64 pub empty: f32,
66 pub warn_within: u32,
68 pub rising: bool,
71 pub limit: f32,
73}
74
75#[repr(C)]
77#[derive(Clone, Copy, Debug, PartialEq)]
78pub struct PamojaPowerSchedule {
79 pub active_secs: u64,
81 pub saver_secs: u64,
83 pub critical_secs: u64,
85 pub saver_below: f32,
87 pub critical_below: f32,
89}
90
91#[repr(C)]
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub enum PamojaAlertKind {
95 None = 0,
97 OutOfRange = 1,
99 RunningOut = 2,
101 ChangingFast = 3,
103}
104
105#[repr(C)]
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub struct PamojaReaction {
111 pub has_actuator: bool,
116 pub actuator: bool,
118 pub alert: PamojaAlertKind,
120 pub reading: f32,
122 pub samples: u32,
124 pub rate: f32,
127}
128
129impl From<ControlSpec> for PamojaControlSpec {
130 fn from(spec: ControlSpec) -> Self {
131 let mut flat = Self {
132 kind: PamojaControlKind::Monitor,
133 setpoint: 0.0,
134 hysteresis: 0.0,
135 cooling: false,
136 safe_band: 0.0,
137 empty: 0.0,
138 warn_within: 0,
139 rising: false,
140 limit: 0.0,
141 };
142 match spec {
143 ControlSpec::Setpoint {
144 setpoint,
145 hysteresis,
146 cooling,
147 safe_band,
148 } => {
149 flat.kind = PamojaControlKind::Setpoint;
150 flat.setpoint = setpoint;
151 flat.hysteresis = hysteresis;
152 flat.cooling = cooling;
153 flat.safe_band = safe_band;
154 }
155 ControlSpec::Level { empty, warn_within } => {
156 flat.kind = PamojaControlKind::Level;
157 flat.empty = empty;
158 flat.warn_within = warn_within;
159 }
160 ControlSpec::Surge { rising, limit } => {
161 flat.kind = PamojaControlKind::Surge;
162 flat.rising = rising;
163 flat.limit = limit;
164 }
165 ControlSpec::Monitor => {}
166 }
167 flat
168 }
169}
170
171impl From<PowerSchedule> for PamojaPowerSchedule {
172 fn from(schedule: PowerSchedule) -> Self {
173 Self {
174 active_secs: schedule.active_secs,
175 saver_secs: schedule.saver_secs,
176 critical_secs: schedule.critical_secs,
177 saver_below: schedule.saver_below,
178 critical_below: schedule.critical_below,
179 }
180 }
181}
182
183impl PamojaReaction {
184 fn flatten(reaction: pamoja_profile::Reaction) -> Self {
186 let mut flat = Self {
187 has_actuator: reaction.actuator.is_some(),
188 actuator: reaction.actuator.unwrap_or(false),
189 alert: PamojaAlertKind::None,
190 reading: 0.0,
191 samples: 0,
192 rate: 0.0,
193 };
194 match reaction.alert {
195 None => {}
196 Some(Alert::OutOfRange { reading }) => {
197 flat.alert = PamojaAlertKind::OutOfRange;
198 flat.reading = reading;
199 }
200 Some(Alert::RunningOut { samples }) => {
201 flat.alert = PamojaAlertKind::RunningOut;
202 flat.samples = samples;
203 }
204 Some(Alert::ChangingFast { rate }) => {
205 flat.alert = PamojaAlertKind::ChangingFast;
206 flat.rate = rate;
207 }
208 }
209 flat
210 }
211}
212
213pub struct PamojaProfile {
215 inner: Profile,
216}
217
218impl PamojaProfile {
219 fn into_raw(inner: Profile) -> *mut Self {
221 Box::into_raw(Box::new(Self { inner }))
222 }
223}
224
225#[no_mangle]
231pub extern "C" fn pamoja_profile_vaccine_fridge_monitor() -> *mut PamojaProfile {
232 PamojaProfile::into_raw(Profile::vaccine_fridge_monitor())
233}
234
235#[no_mangle]
241pub extern "C" fn pamoja_profile_irrigation_node() -> *mut PamojaProfile {
242 PamojaProfile::into_raw(Profile::irrigation_node())
243}
244
245#[no_mangle]
251pub extern "C" fn pamoja_profile_well_level() -> *mut PamojaProfile {
252 PamojaProfile::into_raw(Profile::well_level())
253}
254
255#[no_mangle]
261pub extern "C" fn pamoja_profile_flood_sensor() -> *mut PamojaProfile {
262 PamojaProfile::into_raw(Profile::flood_sensor())
263}
264
265#[no_mangle]
282pub unsafe extern "C" fn pamoja_profile_from_json(manifest: *const c_char) -> *mut PamojaProfile {
283 let Some(manifest) = read_str(manifest, "manifest") else {
284 return ptr::null_mut();
285 };
286 match Profile::from_json(manifest) {
287 Ok(profile) => PamojaProfile::into_raw(profile),
288 Err(error) => {
289 set_last_error(error.to_string());
290 ptr::null_mut()
291 }
292 }
293}
294
295#[no_mangle]
311pub unsafe extern "C" fn pamoja_profile_to_json(
312 profile: *const PamojaProfile,
313) -> *mut PamojaString {
314 let Some(profile) = profile_handle(profile) else {
315 return ptr::null_mut();
316 };
317 match profile.inner.to_json() {
318 Ok(manifest) => PamojaString::into_raw(manifest),
319 Err(error) => {
320 set_last_error(error.to_string());
321 ptr::null_mut()
322 }
323 }
324}
325
326#[no_mangle]
342pub unsafe extern "C" fn pamoja_profile_name(profile: *const PamojaProfile) -> *mut PamojaString {
343 match profile_handle(profile) {
344 Some(profile) => PamojaString::into_raw(profile.inner.name.clone()),
345 None => ptr::null_mut(),
346 }
347}
348
349#[no_mangle]
365pub unsafe extern "C" fn pamoja_profile_topic(profile: *const PamojaProfile) -> *mut PamojaString {
366 match profile_handle(profile) {
367 Some(profile) => PamojaString::into_raw(profile.inner.topic.clone()),
368 None => ptr::null_mut(),
369 }
370}
371
372#[no_mangle]
389pub unsafe extern "C" fn pamoja_profile_control(
390 profile: *const PamojaProfile,
391 out_control: *mut PamojaControlSpec,
392) -> PamojaStatus {
393 if out_control.is_null() {
394 set_last_error("out_control must not be null".to_owned());
395 return PamojaStatus::InvalidArgument;
396 }
397 let Some(profile) = profile_handle(profile) else {
398 return PamojaStatus::InvalidArgument;
399 };
400 *out_control = profile.inner.control.into();
401 PamojaStatus::Ok
402}
403
404#[no_mangle]
421pub unsafe extern "C" fn pamoja_profile_power(
422 profile: *const PamojaProfile,
423 out_schedule: *mut PamojaPowerSchedule,
424) -> PamojaStatus {
425 if out_schedule.is_null() {
426 set_last_error("out_schedule must not be null".to_owned());
427 return PamojaStatus::InvalidArgument;
428 }
429 let Some(profile) = profile_handle(profile) else {
430 return PamojaStatus::InvalidArgument;
431 };
432 *out_schedule = profile.inner.power.into();
433 PamojaStatus::Ok
434}
435
436#[no_mangle]
456pub unsafe extern "C" fn pamoja_profile_power_plan(
457 profile: *const PamojaProfile,
458 out_plan: *mut PamojaPowerPlan,
459) -> PamojaStatus {
460 if out_plan.is_null() {
461 set_last_error("out_plan must not be null".to_owned());
462 return PamojaStatus::InvalidArgument;
463 }
464 let Some(profile) = profile_handle(profile) else {
465 return PamojaStatus::InvalidArgument;
466 };
467 let schedule = profile.inner.power;
468 *out_plan = PamojaPowerPlan {
469 active_us: schedule.active_secs.saturating_mul(1_000_000),
470 saver_us: schedule.saver_secs.saturating_mul(1_000_000),
471 critical_us: schedule.critical_secs.saturating_mul(1_000_000),
472 saver_below: schedule.saver_below,
473 critical_below: schedule.critical_below,
474 };
475 PamojaStatus::Ok
476}
477
478#[no_mangle]
493pub unsafe extern "C" fn pamoja_profile_controller(
494 profile: *const PamojaProfile,
495) -> *mut PamojaController {
496 match profile_handle(profile) {
497 Some(profile) => PamojaController::into_raw(profile.inner.controller()),
498 None => ptr::null_mut(),
499 }
500}
501
502#[no_mangle]
511pub unsafe extern "C" fn pamoja_profile_free(profile: *mut PamojaProfile) {
512 if !profile.is_null() {
513 drop(Box::from_raw(profile));
514 }
515}
516
517pub struct PamojaController {
523 inner: Controller,
524}
525
526impl PamojaController {
527 fn into_raw(inner: Controller) -> *mut Self {
529 Box::into_raw(Box::new(Self { inner }))
530 }
531}
532
533#[no_mangle]
546pub extern "C" fn pamoja_controller_setpoint(
547 setpoint: f32,
548 hysteresis: f32,
549 cooling: bool,
550 safe_band: f32,
551) -> *mut PamojaController {
552 PamojaController::into_raw(Controller::setpoint(
553 setpoint, hysteresis, cooling, safe_band,
554 ))
555}
556
557#[no_mangle]
568pub extern "C" fn pamoja_controller_level(empty: f32, warn_within: u32) -> *mut PamojaController {
569 PamojaController::into_raw(Controller::level(empty, warn_within))
570}
571
572#[no_mangle]
583pub extern "C" fn pamoja_controller_surge(rising: bool, limit: f32) -> *mut PamojaController {
584 PamojaController::into_raw(Controller::surge(rising, limit))
585}
586
587#[no_mangle]
593pub extern "C" fn pamoja_controller_monitor() -> *mut PamojaController {
594 PamojaController::into_raw(Controller::monitor())
595}
596
597#[no_mangle]
615pub unsafe extern "C" fn pamoja_controller_evaluate(
616 controller: *mut PamojaController,
617 reading: f32,
618 out_reaction: *mut PamojaReaction,
619) -> PamojaStatus {
620 if controller.is_null() {
621 set_last_error("controller must not be null".to_owned());
622 return PamojaStatus::InvalidArgument;
623 }
624 if out_reaction.is_null() {
625 set_last_error("out_reaction must not be null".to_owned());
626 return PamojaStatus::InvalidArgument;
627 }
628 let controller = &mut *controller;
629 *out_reaction = PamojaReaction::flatten(controller.inner.evaluate(reading));
630 PamojaStatus::Ok
631}
632
633#[no_mangle]
642pub unsafe extern "C" fn pamoja_controller_free(controller: *mut PamojaController) {
643 if !controller.is_null() {
644 drop(Box::from_raw(controller));
645 }
646}
647
648unsafe fn profile_handle<'a>(profile: *const PamojaProfile) -> Option<&'a PamojaProfile> {
654 if profile.is_null() {
655 set_last_error("profile must not be null".to_owned());
656 return None;
657 }
658 Some(&*profile)
659}
660
661#[cfg(test)]
662mod tests {
663 use std::ffi::{CStr, CString};
664
665 use super::*;
666
667 fn text_of(string: *mut PamojaString) -> String {
668 assert!(!string.is_null(), "the call produced no string");
669 let text = unsafe { CStr::from_ptr(crate::pamoja_string_data(string)) }
670 .to_str()
671 .expect("utf-8")
672 .to_owned();
673 unsafe { crate::pamoja_string_free(string) };
674 text
675 }
676
677 fn reaction_for(controller: *mut PamojaController, reading: f32) -> PamojaReaction {
678 let mut reaction = PamojaReaction {
679 has_actuator: false,
680 actuator: false,
681 alert: PamojaAlertKind::None,
682 reading: 0.0,
683 samples: 0,
684 rate: 0.0,
685 };
686 assert_eq!(
687 unsafe { pamoja_controller_evaluate(controller, reading, &mut reaction) },
688 PamojaStatus::Ok
689 );
690 reaction
691 }
692
693 #[test]
694 fn a_warm_fridge_runs_the_cooler_and_flags_the_excursion() {
695 let profile = pamoja_profile_vaccine_fridge_monitor();
696 let controller = unsafe { pamoja_profile_controller(profile) };
697 let reaction = reaction_for(controller, 9.0);
698
699 assert!(reaction.has_actuator, "the profile drives a cooler");
700 assert!(reaction.actuator, "which runs when the fridge is warm");
701 assert_eq!(
702 reaction.alert,
703 PamojaAlertKind::OutOfRange,
704 "and 9 C is outside the safe band"
705 );
706 assert_eq!(reaction.reading, 9.0);
707
708 unsafe {
709 pamoja_controller_free(controller);
710 pamoja_profile_free(profile);
711 }
712 }
713
714 #[test]
715 fn a_monitoring_profile_drives_no_output() {
716 let controller = pamoja_controller_monitor();
717 let reaction = reaction_for(controller, 21.5);
718 assert!(
719 !reaction.has_actuator,
720 "a monitor observes rather than acts"
721 );
722 assert_eq!(reaction.alert, PamojaAlertKind::None);
723 unsafe { pamoja_controller_free(controller) };
724 }
725
726 #[test]
727 fn a_manifest_survives_a_round_trip() {
728 let profile = pamoja_profile_well_level();
729 let manifest = text_of(unsafe { pamoja_profile_to_json(profile) });
730
731 let text = CString::new(manifest).expect("no interior null");
732 let loaded = unsafe { pamoja_profile_from_json(text.as_ptr()) };
733 assert!(!loaded.is_null());
734
735 assert_eq!(
736 text_of(unsafe { pamoja_profile_name(loaded) }),
737 text_of(unsafe { pamoja_profile_name(profile) })
738 );
739
740 let mut original = PamojaControlSpec::from(ControlSpec::Monitor);
741 let mut restored = original;
742 unsafe {
743 assert_eq!(
744 pamoja_profile_control(profile, &mut original),
745 PamojaStatus::Ok
746 );
747 assert_eq!(
748 pamoja_profile_control(loaded, &mut restored),
749 PamojaStatus::Ok
750 );
751 pamoja_profile_free(loaded);
752 pamoja_profile_free(profile);
753 }
754 assert_eq!(restored.kind, PamojaControlKind::Level);
755 assert_eq!(restored, original, "the policy came back unchanged");
756 }
757
758 #[test]
759 fn a_schedule_becomes_a_governor_in_microseconds() {
760 let profile = pamoja_profile_flood_sensor();
761 let mut schedule = PamojaPowerSchedule {
762 active_secs: 0,
763 saver_secs: 0,
764 critical_secs: 0,
765 saver_below: 0.0,
766 critical_below: 0.0,
767 };
768 let mut plan = PamojaPowerPlan {
769 active_us: 0,
770 saver_us: 0,
771 critical_us: 0,
772 saver_below: 0.0,
773 critical_below: 0.0,
774 };
775 unsafe {
776 assert_eq!(
777 pamoja_profile_power(profile, &mut schedule),
778 PamojaStatus::Ok
779 );
780 assert_eq!(
781 pamoja_profile_power_plan(profile, &mut plan),
782 PamojaStatus::Ok
783 );
784 pamoja_profile_free(profile);
785 }
786 assert_eq!(plan.active_us, schedule.active_secs * 1_000_000);
787 assert_eq!(plan.saver_below, schedule.saver_below);
788 }
789
790 #[test]
791 fn a_null_argument_is_rejected_rather_than_dereferenced() {
792 assert!(unsafe { pamoja_profile_from_json(ptr::null()) }.is_null());
793 assert!(unsafe { pamoja_profile_to_json(ptr::null()) }.is_null());
794 assert!(unsafe { pamoja_profile_name(ptr::null()) }.is_null());
795 assert!(unsafe { pamoja_profile_topic(ptr::null()) }.is_null());
796 assert!(unsafe { pamoja_profile_controller(ptr::null()) }.is_null());
797 assert_eq!(
798 unsafe { pamoja_profile_control(ptr::null(), ptr::null_mut()) },
799 PamojaStatus::InvalidArgument
800 );
801 assert_eq!(
802 unsafe { pamoja_controller_evaluate(ptr::null_mut(), 0.0, ptr::null_mut()) },
803 PamojaStatus::InvalidArgument
804 );
805 }
806}