1use std::collections::VecDeque;
4
5use umsh_core::{NodeHint, PacketBuilder};
6use umsh_crypto::{
7 CryptoEngine, NodeIdentity,
8 software::{SoftwareAes, SoftwareIdentity, SoftwareSha256},
9};
10use umsh_ulcp::battery::{BatteryChargeState, BatteryStatus};
11use umsh_ulcp::gnss::{FixKind, GnssSnapshot};
12use umsh_ulcp::{Status, hdlc};
13use umsh_ulcp_device::{
14 AlertConfig, BatteryFields, DutyLedger, Effect, GnssConfig, IdentitySource, RadioSettings,
15 SNAPSHOT_MAX, Session, SessionConfig, TimeConfig, TxOutcome,
16};
17
18const WIRE_CAPACITY: usize = umsh_ulcp::gatt::MAX_FRAME;
19
20type DeviceSession = Session<SoftwareAes, SoftwareSha256>;
21
22pub struct SimulatedDevice {
27 session: DeviceSession,
28 decoder: hdlc::Decoder<WIRE_CAPACITY>,
29 outbound: VecDeque<Vec<u8>>,
30 snapshot: Option<Vec<u8>>,
31 identity: Option<([u8; 32], [u8; 32])>,
32 identity_seed: u8,
33 pairing_pin: Option<u32>,
34 air: Vec<Vec<u8>>,
35 now_ms: u64,
36 epoch: Option<u32>,
40 fix_step: u32,
44}
45
46impl Default for SimulatedDevice {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl SimulatedDevice {
53 pub fn new() -> Self {
54 Self {
55 session: DeviceSession::new(
56 session_config(),
57 Status::RESET_POWER_ON,
58 CryptoEngine::new(SoftwareAes, SoftwareSha256),
59 ),
60 decoder: hdlc::Decoder::new(),
61 outbound: VecDeque::new(),
62 snapshot: None,
63 identity: None,
64 identity_seed: 0,
65 pairing_pin: None,
66 air: Vec::new(),
67 now_ms: 0,
68 epoch: None,
69 fix_step: 0,
70 }
71 }
72
73 pub fn attach(&mut self) {
75 self.decoder.reset();
76 self.outbound.clear();
77 self.session.attach(true);
78 }
79
80 pub fn detach(&mut self) {
81 self.decoder.reset();
82 self.outbound.clear();
83 self.session.detach();
84 }
85
86 pub fn ingest(&mut self, bytes: &[u8], now_ms: u64) -> Result<(), String> {
88 self.now_ms = now_ms;
89 for &byte in bytes {
90 let outcome = self
91 .decoder
92 .push(byte)
93 .map(|result| result.map(<[u8]>::to_vec));
94 if let Some(outcome) = outcome {
95 let frame = outcome.map_err(|error| format!("HDLC decode error: {error:?}"))?;
96 self.handle_frame(&frame);
97 }
98 }
99 Ok(())
100 }
101
102 pub fn take_outbound(&mut self) -> Option<Vec<u8>> {
103 self.outbound.pop_front()
104 }
105
106 pub fn inject_radio_rx(&mut self, bytes: &[u8], now_ms: u64) {
108 self.now_ms = now_ms;
109 let mut emitted = Vec::new();
110 let effect = self
111 .session
112 .on_radio_rx(bytes, -82, 35, None, now_ms, &mut |frame| {
113 emitted.push(frame.to_vec())
114 });
115 self.execute(effect, &mut emitted);
116 self.queue_emitted(emitted);
117 }
118
119 pub fn inject_demo_rx(&mut self, now_ms: u64) {
121 let mut bytes = [0; 64];
122 let packet = PacketBuilder::new(&mut bytes)
123 .broadcast()
124 .source_hint(NodeHint([0x11, 0x22, 0x33]))
125 .flood_hops(3)
126 .payload(b"hello from the simulated radio")
127 .build()
128 .expect("fixed demo packet fits")
129 .to_vec();
130 self.inject_radio_rx(&packet, now_ms);
131 }
132
133 #[cfg(test)]
134 fn transmitted_frames(&self) -> &[Vec<u8>] {
135 &self.air
136 }
137
138 fn handle_frame(&mut self, frame: &[u8]) {
139 let mut emitted = Vec::new();
140 let effect = self.session.handle_frame(frame, self.now_ms, &mut |bytes| {
141 emitted.push(bytes.to_vec())
142 });
143 self.execute(effect, &mut emitted);
144 self.queue_emitted(emitted);
145 }
146
147 fn execute(&mut self, effect: Option<Effect>, emitted: &mut Vec<Vec<u8>>) {
148 let mut emit = |frame: &[u8]| emitted.push(frame.to_vec());
149 match effect {
150 None
153 | Some(Effect::ApplyRadio(_))
154 | Some(Effect::DeviceNameChanged)
155 | Some(Effect::ApplyAlert(_)) => {}
156 Some(Effect::StartTransmit) => {
157 self.air.push(self.session.tx_data().to_vec());
158 self.session
159 .on_tx_result(TxOutcome::Sent, self.now_ms, &mut emit);
160 }
161 Some(Effect::SampleRssi { tid }) => {
162 self.session.respond_rssi(tid, Ok(-77), &mut emit);
163 }
164 Some(Effect::SampleBattery { tid }) => {
165 self.session.respond_battery(
168 tid,
169 Ok(BatteryStatus {
170 voltage_mv: Some(4111),
171 level_percent: Some(87),
172 charge_state: Some(BatteryChargeState::Charging),
173 }),
174 &mut emit,
175 );
176 }
177 Some(Effect::SampleIlluminance { tid }) => {
178 self.session
180 .respond_illuminance(tid, Some(320_000), &mut emit);
181 }
182 Some(Effect::SignIdentity { tid }) => {
185 self.session.respond_identity_blob(tid, Err(()), &mut emit);
186 }
187 Some(Effect::SetPairingPin { tid, pin }) => {
188 self.pairing_pin = pin;
189 self.session.respond_pin_set(tid, Ok(()), &mut emit);
190 }
191 Some(Effect::DrainQueue) => while self.session.drain_step(self.now_ms, &mut emit) {},
192 Some(Effect::SaveSnapshot { tid }) => {
193 let mut buf = [0u8; SNAPSHOT_MAX];
194 let result = match self.session.encode_snapshot(&mut buf) {
195 Some(len) => {
196 self.snapshot = Some(buf[..len].to_vec());
197 Ok(())
198 }
199 None => Err(()),
200 };
201 self.session.respond_save(tid, result, &mut emit);
202 }
203 Some(Effect::ClearSaved { tid }) => {
204 self.snapshot = None;
205 self.identity = None;
206 self.session.respond_clear(tid, Ok(()), &mut emit);
207 }
208 Some(Effect::FactoryReset) => {
209 self.snapshot = None;
214 self.identity = None;
215 self.identity_seed = 0;
216 self.pairing_pin = None;
217 self.session = DeviceSession::new(
218 session_config(),
219 Status::RESET_POWER_ON,
220 CryptoEngine::new(SoftwareAes, SoftwareSha256),
221 );
222 }
223 Some(Effect::ReadTime { tid }) => {
224 self.session.respond_time(tid, self.epoch, &mut emit);
225 }
226 Some(Effect::ApplyTime { epoch }) => {
227 self.epoch = epoch;
228 }
229 Some(Effect::SampleGnss { tid, key }) => {
230 let sample = self.gnss_sample();
231 self.session.respond_gnss(tid, key, Ok(sample), &mut emit);
232 }
233 Some(Effect::ProvisionIdentity { tid }) => {
234 let result = match self.session.identity_request() {
235 Some(source) => {
236 let secret = match source {
237 IdentitySource::Install(secret) => secret,
238 IdentitySource::Generate => {
239 self.identity_seed = self.identity_seed.wrapping_add(1).max(1);
240 [self.identity_seed; 32]
241 }
242 };
243 let public = SoftwareIdentity::from_secret_bytes(&secret).public_key().0;
244 self.identity = Some((secret, public));
245 Ok(public)
246 }
247 None => Err(()),
248 };
249 self.session.respond_identity(tid, result, &mut emit);
250 }
251 }
252 }
253
254 fn gnss_sample(&mut self) -> GnssSnapshot {
261 if !self.session.gnss_enabled() {
262 return GnssSnapshot::SEARCHING;
263 }
264 self.fix_step = self.fix_step.wrapping_add(1);
265 let mut snapshot = GnssSnapshot::SEARCHING;
266 snapshot.fix = FixKind::ThreeD;
267 snapshot.altitude_m = Some(64);
268 snapshot.accuracy_dm = Some(GnssSnapshot::accuracy_from_hdop_centi(120));
269 snapshot.sats_used = 9;
270 snapshot.sats_in_view = Some(14);
271 let step = (self.fix_step % 16) as u8;
272 snapshot.set_location(&[0x8a, 0x1f, 0x4c, 0x00, 0xd0 | step]);
273 snapshot
274 }
275
276 fn queue_emitted(&mut self, emitted: Vec<Vec<u8>>) {
277 for frame in emitted {
278 let mut encoded = vec![0; hdlc::max_encoded_len(frame.len())];
279 let len = hdlc::encode_frame(&frame, &mut encoded)
280 .expect("simulated device output buffer uses HDLC worst-case size");
281 encoded.truncate(len);
282 self.outbound.push_back(encoded);
283 }
284 }
285}
286
287fn session_config() -> SessionConfig {
288 SessionConfig {
289 dev_version: "umsh-web-sim/0.1",
290 default_device_name: "Browser simulated device",
291 mtu: 255,
292 sync_word: 0x1424,
293 min_tx_power_dbm: -9,
294 max_tx_power_dbm: 22,
295 freq_khz_min: 150_000,
296 freq_khz_max: 960_000,
297 defaults: RadioSettings {
298 enabled: false,
299 freq_khz: 910_525,
300 bw_hz: 62_500,
301 sf: 7,
302 cr_denom: 5,
303 tx_power_dbm: 14,
304 },
305 default_duty_limit: 0xFFFF,
306 duty: Box::leak(Box::new(DutyLedger::new())),
307 battery: Some(BatteryFields {
309 voltage: true,
310 level: true,
311 charge_state: true,
312 }),
313 alert: Some(AlertConfig::DEFAULT),
316 time: Some(TimeConfig),
322 gnss: Some(GnssConfig::DEFAULT),
323 illuminance: true,
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use umsh_ulcp::{Frame, PropPayload, frame, ids::prop};
331
332 fn exchange(sim: &mut SimulatedDevice, request: &[u8]) -> Vec<Vec<u8>> {
333 let mut wire = vec![0; hdlc::max_encoded_len(request.len())];
334 let len = hdlc::encode_frame(request, &mut wire).unwrap();
335 sim.ingest(&wire[..len], 100).unwrap();
336
337 let mut frames = Vec::new();
338 while let Some(wire) = sim.take_outbound() {
339 let mut decoder = hdlc::Decoder::<WIRE_CAPACITY>::new();
340 frames.push(
341 wire.into_iter()
342 .find_map(|byte| decoder.push(byte).map(|frame| frame.unwrap().to_vec()))
343 .unwrap(),
344 );
345 }
346 frames
347 }
348
349 #[test]
350 fn real_session_answers_attach_property_over_hdlc() {
351 let mut sim = SimulatedDevice::new();
352 sim.attach();
353 let mut request = [0; 16];
354 let len = frame::prop_get(&mut request, 1, prop::DEV_VERSION).unwrap();
355 let responses = exchange(&mut sim, &request[..len]);
356 assert_eq!(responses.len(), 1);
357 let response = Frame::parse(&responses[0]).unwrap();
358 let payload = PropPayload::parse(response.payload).unwrap();
359 assert_eq!(payload.key, prop::DEV_VERSION);
360 assert_eq!(payload.value, b"umsh-web-sim/0.1\0");
361 }
362
363 #[test]
364 fn real_session_executes_radio_transmit_effect() {
365 let mut sim = SimulatedDevice::new();
366 sim.attach();
367 let mut request = [0; 64];
368 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
369 exchange(&mut sim, &request[..len]);
370 let len = frame::str_send(
371 &mut request,
372 2,
373 umsh_ulcp::ids::stream::PHY_RAW,
374 b"demo",
375 &[],
376 )
377 .unwrap();
378 exchange(&mut sim, &request[..len]);
379 assert_eq!(sim.transmitted_frames(), &[b"demo".to_vec()]);
380 }
381
382 #[test]
383 fn demo_packet_uses_the_real_receive_path() {
384 let mut sim = SimulatedDevice::new();
385 sim.attach();
386 let mut request = [0; 16];
387 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
388 exchange(&mut sim, &request[..len]);
389
390 sim.inject_demo_rx(200);
391 let wire = sim.take_outbound().expect("demo receive is delivered");
392 let mut decoder = hdlc::Decoder::<WIRE_CAPACITY>::new();
393 let response = wire
394 .into_iter()
395 .find_map(|byte| decoder.push(byte).map(|frame| frame.unwrap().to_vec()))
396 .unwrap();
397 assert_eq!(
398 Frame::parse(&response).unwrap().command(),
399 Some(umsh_ulcp::Cmd::StrRecv)
400 );
401 }
402}