1use std::collections::VecDeque;
18
19use umsh_core::{NodeHint, PacketBuilder};
20use umsh_crypto::{
21 CryptoEngine, NodeIdentity,
22 software::{SoftwareAes, SoftwareIdentity, SoftwareSha256},
23};
24use umsh_ulcp::battery::{BatteryChargeState, BatteryStatus};
25use umsh_ulcp::gnss::{FixKind, GnssSnapshot};
26use umsh_ulcp::{Status, hdlc};
27use umsh_ulcp_device::{Effect, IdentitySource, SNAPSHOT_MAX, Session, TxOutcome};
28
29pub use umsh_ulcp_device::{
30 AlertConfig, BatteryFields, DutyLedger, GnssConfig, RadioRxInfo, RadioSettings, SessionConfig,
31 TimeConfig,
32};
33
34const WIRE_CAPACITY: usize = umsh_ulcp::gatt::MAX_FRAME;
35
36const SIMULATED_BONDS: u8 = 2;
38
39type DeviceSession = Session<SoftwareAes, SoftwareSha256>;
40
41pub struct SimulatedDevice {
43 session: DeviceSession,
44 config: SessionConfig,
45 decoder: hdlc::Decoder<WIRE_CAPACITY>,
46 outbound: VecDeque<Vec<u8>>,
47 snapshot: Option<Vec<u8>>,
48 identity: Option<([u8; 32], [u8; 32])>,
49 identity_seed: u8,
50 pairing_pin: Option<u32>,
51 bond_count: u8,
55 air: Vec<Vec<u8>>,
56 now_ms: u64,
57 boot_ms: u64,
63 epoch: Option<u32>,
67 fix_step: u32,
71}
72
73impl SimulatedDevice {
74 pub fn new(config: SessionConfig) -> Self {
75 let mut session = DeviceSession::new(
76 config,
77 Status::RESET_POWER_ON,
78 CryptoEngine::new(SoftwareAes, SoftwareSha256),
79 );
80 session.set_ble_bond_count(SIMULATED_BONDS, &mut |_| {});
83 Self {
84 session,
85 config,
86 decoder: hdlc::Decoder::new(),
87 outbound: VecDeque::new(),
88 snapshot: None,
89 identity: None,
90 identity_seed: 0,
91 pairing_pin: None,
92 bond_count: SIMULATED_BONDS,
93 air: Vec::new(),
94 now_ms: 0,
95 boot_ms: 0,
96 epoch: None,
97 fix_step: 0,
98 }
99 }
100
101 fn device_ms(&self) -> u64 {
103 self.now_ms.saturating_sub(self.boot_ms)
104 }
105
106 pub fn attach(&mut self) {
108 self.decoder.reset();
109 self.outbound.clear();
110 self.session.attach(true);
111 }
112
113 pub fn detach(&mut self) {
114 self.decoder.reset();
115 self.outbound.clear();
116 self.session.detach();
117 }
118
119 pub fn ingest(&mut self, bytes: &[u8], now_ms: u64) -> Result<(), String> {
121 self.now_ms = now_ms;
122 for &byte in bytes {
123 let outcome = self
124 .decoder
125 .push(byte)
126 .map(|result| result.map(<[u8]>::to_vec));
127 if let Some(outcome) = outcome {
128 let frame = outcome.map_err(|error| format!("HDLC decode error: {error:?}"))?;
129 self.handle_frame(&frame);
130 }
131 }
132 Ok(())
133 }
134
135 pub fn take_outbound(&mut self) -> Option<Vec<u8>> {
136 self.outbound.pop_front()
137 }
138
139 pub fn take_transmitted(&mut self) -> Vec<Vec<u8>> {
143 std::mem::take(&mut self.air)
144 }
145
146 pub fn inject_radio_rx(&mut self, bytes: &[u8], now_ms: u64) {
148 self.inject_radio_rx_with_info(bytes, &RadioRxInfo::measured(-82, 35, None), now_ms);
149 }
150
151 pub fn inject_radio_rx_with_info(&mut self, bytes: &[u8], info: &RadioRxInfo, now_ms: u64) {
155 self.now_ms = now_ms;
156 let device_ms = self.device_ms();
157 let mut emitted = Vec::new();
158 let effect = self
159 .session
160 .on_radio_rx(bytes, info, device_ms, &mut |frame| {
161 emitted.push(frame.to_vec())
162 });
163 self.execute(effect, &mut emitted);
164 self.queue_emitted(emitted);
165 }
166
167 pub fn inject_demo_rx(&mut self, now_ms: u64) {
169 let mut bytes = [0; 64];
170 let packet = PacketBuilder::new(&mut bytes)
171 .broadcast()
172 .source_hint(NodeHint([0x11, 0x22, 0x33]))
173 .flood_hops(3)
174 .payload(b"hello from the simulated radio")
175 .build()
176 .expect("fixed demo packet fits")
177 .to_vec();
178 self.inject_radio_rx(&packet, now_ms);
179 }
180
181 fn handle_frame(&mut self, frame: &[u8]) {
182 let device_ms = self.device_ms();
183 let mut emitted = Vec::new();
184 let effect = self
185 .session
186 .handle_frame(frame, device_ms, &mut |bytes| emitted.push(bytes.to_vec()));
187 self.execute(effect, &mut emitted);
188 self.queue_emitted(emitted);
189 }
190
191 fn execute(&mut self, effect: Option<Effect>, emitted: &mut Vec<Vec<u8>>) {
192 let mut emit = |frame: &[u8]| emitted.push(frame.to_vec());
193 match effect {
194 None
197 | Some(Effect::ApplyRadio(_))
198 | Some(Effect::DeviceNameChanged)
199 | Some(Effect::ApplyBackhaul { .. })
202 | Some(Effect::ApplyAlert(_)) => {}
203 Some(Effect::StartTransmit) => {
204 let device_ms = self.device_ms();
205 self.air.push(self.session.tx_data().to_vec());
206 self.session
207 .on_tx_result(TxOutcome::Sent, device_ms, &mut emit);
208 }
209 Some(Effect::SampleRssi { tid }) => {
210 self.session.respond_rssi(tid, Ok(-77), &mut emit);
211 }
212 Some(Effect::SampleBattery { tid }) => {
213 self.session.respond_battery(
216 tid,
217 Ok(BatteryStatus {
218 voltage_mv: Some(4111),
219 level_percent: Some(87),
220 charge_state: Some(BatteryChargeState::Charging),
221 }),
222 &mut emit,
223 );
224 }
225 Some(Effect::SampleIlluminance { tid }) => {
226 self.session
228 .respond_illuminance(tid, Some(320_000), &mut emit);
229 }
230 Some(Effect::SignIdentity { tid }) => {
233 self.session.respond_identity_blob(tid, Err(()), &mut emit);
234 }
235 Some(Effect::SetPairingPin { tid, pin }) => {
236 self.pairing_pin = pin;
237 self.session.respond_pin_set(tid, Ok(()), &mut emit);
238 }
239 Some(Effect::BleClearBonds { tid }) => {
240 self.bond_count = 0;
244 self.pairing_pin = None;
245 self.session.set_ble_bond_count(0, &mut emit);
246 self.session.set_ble_pairing(true, &mut emit);
247 self.session.respond_ble_clear_bonds(tid, Ok(()), &mut emit);
248 }
249 Some(Effect::SetBlePairing { tid, open }) => {
250 self.session.respond_ble_pairing(tid, Ok(open), &mut emit);
256 }
257 Some(Effect::DrainQueue) => {
258 let device_ms = self.device_ms();
259 while self.session.drain_step(device_ms, &mut emit) {}
260 }
261 Some(Effect::SaveSnapshot { tid }) => {
262 let mut buf = [0u8; SNAPSHOT_MAX];
263 let result = match self.session.encode_snapshot(&mut buf) {
264 Some(len) => {
265 self.snapshot = Some(buf[..len].to_vec());
266 Ok(())
267 }
268 None => Err(()),
269 };
270 self.session.respond_save(tid, result, &mut emit);
271 }
272 Some(Effect::ClearSaved { tid }) => {
273 self.snapshot = None;
274 self.identity = None;
275 self.session.respond_clear(tid, Ok(()), &mut emit);
276 }
277 Some(Effect::FactoryReset) => {
278 self.snapshot = None;
283 self.identity = None;
284 self.identity_seed = 0;
285 self.pairing_pin = None;
286 self.bond_count = 0;
287 self.boot_ms = self.now_ms;
290 self.session = DeviceSession::new(
291 self.config,
292 Status::RESET_POWER_ON,
293 CryptoEngine::new(SoftwareAes, SoftwareSha256),
294 );
295 }
296 Some(Effect::Reboot) => {
297 self.boot_ms = self.now_ms;
302 self.session = DeviceSession::new(
303 self.config,
304 Status::RESET_POWER_ON,
305 CryptoEngine::new(SoftwareAes, SoftwareSha256),
306 );
307 if let Some(saved) = self.snapshot.clone() {
313 let _ = self.session.restore_at_boot(&saved);
314 }
315 self.session
319 .set_ble_bond_count(self.bond_count, &mut |_| {});
320 }
321 Some(Effect::ReadTime { tid }) => {
322 self.session.respond_time(tid, self.epoch, &mut emit);
323 }
324 Some(Effect::ApplyTime { epoch }) => {
325 self.epoch = epoch;
326 }
327 Some(Effect::SampleGnss { tid, key }) => {
328 let sample = self.gnss_sample();
329 self.session.respond_gnss(tid, key, Ok(sample), &mut emit);
330 }
331 Some(Effect::ProvisionIdentity { tid }) => {
332 let result = match self.session.identity_request() {
333 Some(source) => {
334 let secret = match source {
335 IdentitySource::Install(secret) => secret,
336 IdentitySource::Generate => {
337 self.identity_seed = self.identity_seed.wrapping_add(1).max(1);
338 [self.identity_seed; 32]
339 }
340 };
341 let public = SoftwareIdentity::from_secret_bytes(&secret).public_key().0;
342 self.identity = Some((secret, public));
343 Ok(public)
344 }
345 None => Err(()),
346 };
347 self.session.respond_identity(tid, result, &mut emit);
348 }
349 }
350 }
351
352 fn gnss_sample(&mut self) -> GnssSnapshot {
359 if !self.session.gnss_enabled() {
360 return GnssSnapshot::SEARCHING;
361 }
362 self.fix_step = self.fix_step.wrapping_add(1);
363 let mut snapshot = GnssSnapshot::SEARCHING;
364 snapshot.fix = FixKind::ThreeD;
365 snapshot.altitude_m = Some(64);
366 snapshot.accuracy_dm = Some(GnssSnapshot::accuracy_from_hdop_centi(120));
367 snapshot.sats_used = 9;
368 snapshot.sats_in_view = Some(14);
369 let step = (self.fix_step % 16) as u8;
370 snapshot.set_location(&[0x8a, 0x1f, 0x4c, 0x00, 0xd0 | step]);
371 snapshot
372 }
373
374 fn queue_emitted(&mut self, emitted: Vec<Vec<u8>>) {
375 for frame in emitted {
376 let mut encoded = vec![0; hdlc::max_encoded_len(frame.len())];
377 let len = hdlc::encode_frame(&frame, &mut encoded)
378 .expect("simulated device output buffer uses HDLC worst-case size");
379 encoded.truncate(len);
380 self.outbound.push_back(encoded);
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use umsh_ulcp::{Frame, PropPayload, frame, ids::prop};
389
390 fn test_config() -> SessionConfig {
391 SessionConfig {
392 dev_version: "umsh-simdev-test/0.1",
393 dev_model: None,
394 default_device_name: "Simulated test device",
395 mtu: 255,
396 sync_word: 0x1424,
397 min_tx_power_dbm: -9,
398 max_tx_power_dbm: 22,
399 freq_khz_min: 150_000,
400 freq_khz_max: 960_000,
401 defaults: RadioSettings {
402 enabled: false,
403 freq_khz: 910_525,
404 bw_hz: 62_500,
405 sf: 7,
406 cr_denom: 5,
407 tx_power_dbm: 14,
408 },
409 default_duty_limit: 0xFFFF,
410 duty: Box::leak(Box::new(DutyLedger::new())),
411 battery: Some(BatteryFields {
412 voltage: true,
413 level: true,
414 charge_state: true,
415 }),
416 alert: Some(AlertConfig::DEFAULT),
417 time: Some(TimeConfig),
418 gnss: Some(GnssConfig::DEFAULT),
419 illuminance: true,
420 ble: true,
421 ble_pairing: true,
422 reboot: true,
425 stats: None,
427 mac_node: false,
428 }
429 }
430
431 fn exchange(sim: &mut SimulatedDevice, request: &[u8]) -> Vec<Vec<u8>> {
432 exchange_at(sim, request, 100)
433 }
434
435 fn exchange_at(sim: &mut SimulatedDevice, request: &[u8], now_ms: u64) -> Vec<Vec<u8>> {
436 let mut wire = vec![0; hdlc::max_encoded_len(request.len())];
437 let len = hdlc::encode_frame(request, &mut wire).unwrap();
438 sim.ingest(&wire[..len], now_ms).unwrap();
439
440 let mut frames = Vec::new();
441 while let Some(wire) = sim.take_outbound() {
442 let mut decoder = hdlc::Decoder::<WIRE_CAPACITY>::new();
443 frames.push(
444 wire.into_iter()
445 .find_map(|byte| decoder.push(byte).map(|frame| frame.unwrap().to_vec()))
446 .unwrap(),
447 );
448 }
449 frames
450 }
451
452 #[test]
453 fn real_session_answers_attach_property_over_hdlc() {
454 let mut sim = SimulatedDevice::new(test_config());
455 sim.attach();
456 let mut request = [0; 16];
457 let len = frame::prop_get(&mut request, 1, prop::DEV_VERSION).unwrap();
458 let responses = exchange(&mut sim, &request[..len]);
459 assert_eq!(responses.len(), 1);
460 let response = Frame::parse(&responses[0]).unwrap();
461 let payload = PropPayload::parse(response.payload).unwrap();
462 assert_eq!(payload.key, prop::DEV_VERSION);
463 assert_eq!(payload.value, b"umsh-simdev-test/0.1\0");
464 }
465
466 #[test]
470 fn a_factory_reset_restarts_the_uptime_clock() {
471 let mut sim = SimulatedDevice::new(test_config());
472 sim.attach();
473
474 let read_uptime = |sim: &mut SimulatedDevice, now_ms: u64| -> u32 {
475 let mut request = [0; 16];
476 let len = frame::prop_get(&mut request, 1, prop::UPTIME).unwrap();
477 let responses = exchange_at(sim, &request[..len], now_ms);
478 let response = Frame::parse(&responses[0]).unwrap();
479 let payload = PropPayload::parse(response.payload).unwrap();
480 assert_eq!(payload.key, prop::UPTIME);
481 u32::from_le_bytes(payload.value.try_into().expect("UINT32"))
482 };
483
484 assert_eq!(read_uptime(&mut sim, 5_000), 5);
485
486 let mut request = [0; 16];
487 let len = frame::factory_reset(&mut request, 2).unwrap();
488 exchange_at(&mut sim, &request[..len], 5_000);
489
490 assert_eq!(read_uptime(&mut sim, 7_000), 2);
492 }
493
494 #[test]
495 fn real_session_executes_radio_transmit_effect() {
496 let mut sim = SimulatedDevice::new(test_config());
497 sim.attach();
498 let mut request = [0; 64];
499 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
500 exchange(&mut sim, &request[..len]);
501 let len = frame::str_send(
502 &mut request,
503 2,
504 umsh_ulcp::ids::stream::PHY_RAW,
505 b"demo",
506 &[],
507 )
508 .unwrap();
509 exchange(&mut sim, &request[..len]);
510 assert_eq!(sim.take_transmitted(), &[b"demo".to_vec()]);
511 assert!(
512 sim.take_transmitted().is_empty(),
513 "the drain leaves nothing behind"
514 );
515 }
516
517 #[test]
518 fn demo_packet_uses_the_real_receive_path() {
519 let mut sim = SimulatedDevice::new(test_config());
520 sim.attach();
521 let mut request = [0; 16];
522 let len = frame::prop_set(&mut request, 1, prop::PHY_ENABLED, &[1]).unwrap();
523 exchange(&mut sim, &request[..len]);
524
525 sim.inject_demo_rx(200);
526 let wire = sim.take_outbound().expect("demo receive is delivered");
527 let mut decoder = hdlc::Decoder::<WIRE_CAPACITY>::new();
528 let response = wire
529 .into_iter()
530 .find_map(|byte| decoder.push(byte).map(|frame| frame.unwrap().to_vec()))
531 .unwrap();
532 assert_eq!(
533 Frame::parse(&response).unwrap().command(),
534 Some(umsh_ulcp::Cmd::StrRecv)
535 );
536 }
537
538 #[test]
541 fn a_simulated_device_claims_no_node() {
542 let mut sim = SimulatedDevice::new(test_config());
543 sim.attach();
544 let mut request = [0; 16];
545 let len = frame::prop_get(&mut request, 1, umsh_ulcp::ids::prop::CAPS).unwrap();
546 let responses = exchange(&mut sim, &request[..len]);
547 let response = Frame::parse(&responses[0]).unwrap();
548 let payload = PropPayload::parse(response.payload).unwrap();
549 let mut caps = Vec::new();
550 let mut offset = 0;
551 while offset < payload.value.len() {
552 let (value, used) = umsh_ulcp::pui::decode(&payload.value[offset..]).unwrap();
553 caps.push(value);
554 offset += used;
555 }
556 assert!(!caps.contains(&umsh_ulcp::ids::cap::MAC_BACKHAUL));
557 assert!(!caps.contains(&umsh_ulcp::ids::cap::REPEATER));
558 assert!(caps.contains(&umsh_ulcp::ids::cap::WRITABLE_RAW_STREAM));
559 }
560}