1#![no_std]
2
3use ri_esp_core::{EnvironmentReading, ReadingStatus};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PromptId {
16 HighHeatHumidity,
17 HotRoom,
18 HumidRoom,
19 MissingSensor,
20 NormalRoom,
21 SafeAction,
22 StaleData,
23 LocalFirstMeans,
24}
25
26impl PromptId {
27 pub const fn prompt_text(self) -> &'static str {
29 match self {
30 PromptId::HighHeatHumidity => "high heat and humidity. action is ",
31 PromptId::HotRoom => "hot room. action is ",
32 PromptId::HumidRoom => "humid room. action is ",
33 PromptId::MissingSensor => "missing sensor. action is ",
34 PromptId::NormalRoom => "normal room. action is ",
35 PromptId::SafeAction => "safe action is ",
36 PromptId::StaleData => "stale data. action is ",
37 PromptId::LocalFirstMeans => "local first means ",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum PolicyReason {
45 ProofPromptOverride,
46 SensorMissing,
47 StaleReading,
48 HotHumid,
49 HotRoom,
50 HumidRoom,
51 UnsupportedColdOrDry,
52 Normal,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct PolicyContext<'a> {
58 pub reading: EnvironmentReading,
59 pub uptime_ms: u64,
60 pub last_read_ms: u64,
61 pub proof_prompt_override: Option<&'a str>,
62}
63
64impl<'a> PolicyContext<'a> {
65 pub fn new(reading: EnvironmentReading, uptime_ms: u64) -> Self {
66 Self {
67 reading,
68 uptime_ms,
69 last_read_ms: 0,
70 proof_prompt_override: None,
71 }
72 }
73
74 pub fn with_last_read_ms(mut self, ms: u64) -> Self {
75 self.last_read_ms = ms;
76 self
77 }
78
79 pub fn with_proof_prompt_override(mut self, prompt: &'a str) -> Self {
80 self.proof_prompt_override = Some(prompt);
81 self
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct PolicyDecision {
88 pub prompt_id: PromptId,
89 pub reason: PolicyReason,
90 pub ai_route: bool,
91 pub confidence: f32,
92 prompt_text: &'static str,
93}
94
95impl PolicyDecision {
96 pub fn prompt(&self) -> &'static str {
98 self.prompt_text
99 }
100}
101
102pub struct SensorPolicy {
104 pub stale_ms: u64,
105 pub hot_f: f32,
106 pub cold_f: f32,
107 pub humid_pct: f32,
108 pub dry_pct: f32,
109}
110
111impl Default for SensorPolicy {
112 fn default() -> Self {
113 Self {
114 stale_ms: 120_000,
115 hot_f: 82.0,
116 cold_f: 60.0,
117 humid_pct: 65.0,
118 dry_pct: 25.0,
119 }
120 }
121}
122
123fn match_prompt_id(s: &str) -> PromptId {
124 match s {
125 "high heat and humidity. action is " => PromptId::HighHeatHumidity,
126 "hot room. action is " => PromptId::HotRoom,
127 "humid room. action is " => PromptId::HumidRoom,
128 "missing sensor. action is " => PromptId::MissingSensor,
129 "normal room. action is " => PromptId::NormalRoom,
130 "safe action is " => PromptId::SafeAction,
131 "stale data. action is " => PromptId::StaleData,
132 "local first means " => PromptId::LocalFirstMeans,
133 _ => PromptId::NormalRoom,
134 }
135}
136
137const fn ai_route_for(id: PromptId) -> bool {
138 match id {
139 PromptId::HighHeatHumidity => true,
140 PromptId::HotRoom => true,
141 PromptId::HumidRoom => true,
142 PromptId::MissingSensor => true,
143 PromptId::StaleData => true,
144 PromptId::NormalRoom => false,
145 PromptId::SafeAction => false,
146 PromptId::LocalFirstMeans => false,
147 }
148}
149
150fn decision(id: PromptId, reason: PolicyReason) -> PolicyDecision {
151 let conf = match id {
152 PromptId::HighHeatHumidity | PromptId::HotRoom | PromptId::HumidRoom => 0.9,
153 PromptId::MissingSensor => 0.0,
154 PromptId::StaleData => 0.3,
155 PromptId::NormalRoom => 1.0,
156 PromptId::SafeAction => 0.5,
157 PromptId::LocalFirstMeans => 0.8,
158 };
159 PolicyDecision {
160 prompt_id: id,
161 reason,
162 ai_route: ai_route_for(id),
163 confidence: conf,
164 prompt_text: id.prompt_text(),
165 }
166}
167
168impl SensorPolicy {
169 pub fn evaluate(&self, ctx: PolicyContext) -> PolicyDecision {
170 if let Some(override_prompt) = ctx.proof_prompt_override {
172 let id = match_prompt_id(override_prompt);
173 return PolicyDecision {
174 prompt_id: id,
175 reason: PolicyReason::ProofPromptOverride,
176 ai_route: ai_route_for(id),
177 confidence: 1.0,
178 prompt_text: id.prompt_text(),
179 };
180 }
181
182 let r = &ctx.reading;
183
184 if r.status == ReadingStatus::SensorReadFailed || r.status == ReadingStatus::NotConnected {
186 return decision(PromptId::MissingSensor, PolicyReason::SensorMissing);
187 }
188
189 if r.status == ReadingStatus::Stale {
191 return decision(PromptId::StaleData, PolicyReason::StaleReading);
192 }
193
194 if ctx.uptime_ms.saturating_sub(ctx.last_read_ms) > self.stale_ms {
196 return decision(PromptId::StaleData, PolicyReason::StaleReading);
197 }
198
199 let temp_c = r.temperature_c.unwrap_or(25.0);
201 let humidity = r.humidity_pct.unwrap_or(50.0);
202 let temp_f = temp_c * 9.0 / 5.0 + 32.0;
203
204 if temp_f >= self.hot_f && humidity >= self.humid_pct {
205 return decision(PromptId::HighHeatHumidity, PolicyReason::HotHumid);
206 }
207
208 if temp_f >= self.hot_f {
209 return decision(PromptId::HotRoom, PolicyReason::HotRoom);
210 }
211
212 if humidity >= self.humid_pct {
213 return decision(PromptId::HumidRoom, PolicyReason::HumidRoom);
214 }
215
216 if temp_f <= self.cold_f || humidity <= self.dry_pct {
217 return decision(PromptId::SafeAction, PolicyReason::UnsupportedColdOrDry);
218 }
219
220 decision(PromptId::NormalRoom, PolicyReason::Normal)
221 }
222}