1use std::collections::VecDeque;
17use std::io;
18use std::time::Duration;
19
20use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf};
21use tokio::time::Instant;
22
23use umsh_core::{ChannelKey, RegionCode};
24use umsh_crypto::CryptoEngine;
25use umsh_crypto::software::{SoftwareAes, SoftwareSha256};
26use umsh_hal::{CadPolicy, Radio, RxInfo, RxOrigin, Snr, TxError, TxOptions};
27use umsh_ulcp::Status;
28use umsh_ulcp::airtime::lora_airtime_ms;
29use umsh_ulcp::alert::AlertState;
30use umsh_ulcp::battery::BatteryStatus;
31use umsh_ulcp::frame::{self, Cmd, Frame, MultiEntries, StreamPayload, TID_UNSOLICITED};
32use umsh_ulcp::gnss::GnssSnapshot;
33use umsh_ulcp::hdlc;
34use umsh_ulcp::host::{PropertyNotification, PropertyNotificationKind, TidAllocator};
35use umsh_ulcp::ids::{self, cap, prop, stream};
36use umsh_ulcp::items;
37use umsh_ulcp::meta::{BufferedRxMeta, RX_FLAG_SELF_TX, RxMeta, TX_FLAG_NOCCA, TxMeta};
38use umsh_ulcp::pui;
39
40const WIRE_BUF: usize = 1024;
42const READ_CHUNK: usize = 256;
44const RX_QUEUE_DEPTH: usize = 8;
48const RESPONSE_QUEUE_DEPTH: usize = 8;
50const PROP_EVENT_DEPTH: usize = 16;
53const CCA_RETRY_DELAY: Duration = Duration::from_millis(10);
55
56#[derive(Debug)]
57pub enum UlcpError {
58 Io(io::Error),
59 Disconnected,
61 Protocol(&'static str),
63 Status(Status),
65 UnexpectedReset(Status),
68 FrameTooLarge(usize),
70 Timeout,
72 Transport(String),
74 AdministrativeAttach,
78}
79
80impl core::fmt::Display for UlcpError {
81 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
82 match self {
83 Self::Io(error) => write!(formatter, "io error: {error}"),
84 Self::Disconnected => write!(formatter, "ULCP link disconnected"),
85 Self::Protocol(message) => write!(formatter, "protocol error: {message}"),
86 Self::Status(status) => write!(formatter, "device reported {status:?}"),
87 Self::UnexpectedReset(status) => {
88 write!(formatter, "device reset unexpectedly ({status:?})")
89 }
90 Self::FrameTooLarge(len) => write!(formatter, "frame too large: {len} bytes"),
91 Self::Timeout => write!(formatter, "timed out waiting for device response"),
92 Self::Transport(message) => write!(formatter, "transport error: {message}"),
93 Self::AdministrativeAttach => write!(
94 formatter,
95 "host-domain writes need a tethered attach, not an administrative one"
96 ),
97 }
98 }
99}
100
101impl std::error::Error for UlcpError {}
102
103impl From<io::Error> for UlcpError {
104 fn from(error: io::Error) -> Self {
105 Self::Io(error)
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct UlcpDeviceConfig {
112 pub freq_khz: u32,
114 pub bandwidth_hz: u32,
116 pub spreading_factor: u8,
118 pub coding_rate_denom: u8,
121 pub tx_power_dbm: i8,
123 pub sync_word: u16,
125 pub response_timeout: Duration,
129}
130
131impl UlcpDeviceConfig {
132 pub fn new(
136 freq_khz: u32,
137 bandwidth_hz: u32,
138 spreading_factor: u8,
139 coding_rate_denom: u8,
140 ) -> Self {
141 Self {
142 freq_khz,
143 bandwidth_hz,
144 spreading_factor,
145 coding_rate_denom,
146 tx_power_dbm: 0,
147 sync_word: umsh_ulcp::profiles::DEFAULT_SYNC_WORD,
148 response_timeout: Duration::from_secs(2),
149 }
150 }
151}
152
153struct RxPacket {
154 data: Vec<u8>,
155 meta: RxMeta,
156 raw_meta: Vec<u8>,
159}
160
161type ResponseKind = PropertyNotificationKind;
163
164pub type MultiValue = Result<(u32, Vec<u8>), Status>;
167
168struct Response {
171 tid: u8,
172 kind: ResponseKind,
173 key: u32,
174 value: Vec<u8>,
175}
176
177#[derive(Clone, Copy)]
178enum PropResponsePolicy {
179 Value,
180 StatusOnly,
181}
182
183#[derive(Clone, Debug, PartialEq, Eq)]
189pub enum PropEvent {
190 Is { key: u32, value: Vec<u8> },
192 Inserted { key: u32, digest: Vec<u8> },
194 Removed { key: u32, digest: Vec<u8> },
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum TraceDirection {
202 HostToDevice,
203 DeviceToHost,
204}
205
206impl core::fmt::Display for TraceDirection {
207 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208 formatter.write_str(match self {
209 Self::HostToDevice => "host→device",
210 Self::DeviceToHost => "device→host",
211 })
212 }
213}
214
215pub type FrameTrace = Box<dyn FnMut(TraceDirection, &str) + Send>;
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub enum RestoreCompletion {
224 Updated,
227 Reset,
231}
232
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub enum HostOwnership {
237 Ours,
240 Unclaimed,
242 OtherHost([u8; 32]),
247 Unsupported,
250 Unreachable,
254}
255
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub enum SavedSnapshot {
264 None,
266 Current,
268 Fallback,
272 Unreadable,
275}
276
277impl SavedSnapshot {
278 fn from_octet(value: &[u8]) -> Result<Self, UlcpError> {
279 match value {
280 [ids::saved::NONE] => Ok(Self::None),
281 [ids::saved::CURRENT] => Ok(Self::Current),
282 [ids::saved::FALLBACK] => Ok(Self::Fallback),
283 [ids::saved::UNREADABLE] => Ok(Self::Unreadable),
284 _ => Err(UlcpError::Protocol("malformed PROP_SAVED")),
285 }
286 }
287
288 pub fn is_saved(self) -> bool {
291 matches!(self, Self::Current | Self::Fallback)
292 }
293}
294
295#[derive(Clone, Debug)]
300pub struct DeviceSync {
301 pub last_status: Status,
303 pub reset_since_last_contact: bool,
307 pub capabilities: Vec<u32>,
309 pub ownership: HostOwnership,
311 pub host_key: Option<[u8; 32]>,
313 pub phy_enabled: bool,
316 pub freq_khz: u32,
318 pub device_name: String,
320 pub saved: Option<SavedSnapshot>,
322 pub queue_count: Option<u16>,
324 pub queue_dropped: Option<u32>,
326 pub filters: Option<Vec<items::Filter>>,
328 pub host_channel_ids: Option<Vec<[u8; items::CHANNEL_ID_LEN]>>,
331 pub host_peer_keys: Option<Vec<[u8; items::PUBLIC_KEY_LEN]>>,
334 pub auto_ack: Option<bool>,
336 pub dev_key: Option<[u8; 32]>,
339}
340
341impl DeviceSync {
342 pub fn has_capability(&self, capability: u32) -> bool {
344 self.capabilities.contains(&capability)
345 }
346}
347
348#[derive(Clone, Debug, Default, PartialEq, Eq)]
354pub struct RepeaterPolicy {
355 pub enabled: bool,
358 pub regions: Vec<String>,
364 pub default_region: Option<RegionCode>,
367 pub min_rssi: Option<i16>,
370 pub min_snr: Option<i8>,
372}
373
374#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376pub struct DeviceTime {
377 pub epoch: Option<u32>,
380 pub tz_offset_min: i16,
383}
384
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
392pub struct GnssStatus {
393 pub enabled: bool,
396 pub fix: GnssSnapshot,
398 pub ident_update: bool,
401 pub ident_precision: u8,
404 pub time_trust: bool,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub struct AdvertPolicy {
412 pub advert_interval_s: u32,
415 pub beacon_interval_s: u32,
417 pub startup_beacon: bool,
419}
420
421#[derive(Clone, Debug)]
424pub struct HostProvisioning {
425 pub host_key: [u8; 32],
429 pub filters: Vec<items::Filter>,
431 pub channel_keys: Vec<[u8; items::CHANNEL_KEY_LEN]>,
433 pub peer_keys: Vec<items::PeerKeyEntry>,
439 pub auto_ack: bool,
441}
442
443#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
450pub struct ProvisionReport {
451 pub host_replaced: bool,
454 pub filters_replaced: bool,
456 pub channels_replaced: bool,
460 pub channels_inserted: usize,
462 pub peers_inserted: usize,
464 pub peers_removed: usize,
466 pub auto_ack_changed: bool,
468}
469
470#[allow(async_fn_in_trait)]
472pub trait FrameLink {
473 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError>;
475
476 fn poll_recv_frame(
481 &mut self,
482 cx: &mut core::task::Context<'_>,
483 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>>;
484
485 async fn recv_frame(&mut self) -> Result<Vec<u8>, UlcpError> {
487 core::future::poll_fn(|cx| self.poll_recv_frame(cx)).await
488 }
489}
490
491pub struct SerialFrameLink<IO> {
493 io: IO,
494 decoder: hdlc::Decoder<WIRE_BUF>,
495 read_buf: [u8; READ_CHUNK],
496 read_pos: usize,
497 read_len: usize,
498}
499
500impl<IO> SerialFrameLink<IO> {
501 pub fn new(io: IO) -> Self {
503 Self {
504 io,
505 decoder: hdlc::Decoder::new(),
506 read_buf: [0; READ_CHUNK],
507 read_pos: 0,
508 read_len: 0,
509 }
510 }
511
512 pub fn into_inner(self) -> IO {
514 self.io
515 }
516}
517
518impl<IO> FrameLink for SerialFrameLink<IO>
519where
520 IO: AsyncRead + AsyncWrite + Unpin,
521{
522 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
523 let mut wire = vec![0u8; hdlc::max_encoded_len(frame.len())];
524 let len = hdlc::encode_frame(frame, &mut wire).expect("buffer sized with max_encoded_len");
525 self.io.write_all(&wire[..len]).await?;
526 self.io.flush().await?;
527 Ok(())
528 }
529
530 fn poll_recv_frame(
531 &mut self,
532 cx: &mut core::task::Context<'_>,
533 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
534 loop {
535 while self.read_pos < self.read_len {
536 let byte = self.read_buf[self.read_pos];
537 self.read_pos += 1;
538 if let Some(Ok(frame)) = self.decoder.push(byte) {
539 return core::task::Poll::Ready(Ok(frame.to_vec()));
540 }
541 }
542
543 self.read_pos = 0;
544 self.read_len = 0;
545 let mut read_buf = ReadBuf::new(&mut self.read_buf);
546 match core::pin::Pin::new(&mut self.io).poll_read(cx, &mut read_buf) {
547 core::task::Poll::Ready(Ok(())) => {
548 self.read_len = read_buf.filled().len();
549 if self.read_len == 0 {
550 return core::task::Poll::Ready(Err(UlcpError::Disconnected));
551 }
552 }
553 core::task::Poll::Ready(Err(error)) => {
554 return core::task::Poll::Ready(Err(UlcpError::Io(error)));
555 }
556 core::task::Poll::Pending => return core::task::Poll::Pending,
557 }
558 }
559 }
560}
561
562#[cfg(feature = "ble-radio")]
564#[derive(Clone, Copy, Debug)]
565pub struct BleFrameLinkConfig {
566 pub segment_payload: usize,
568 pub discovery_timeout: Duration,
570 pub operation_timeout: Duration,
572 pub pairing_timeout: Duration,
576}
577
578#[cfg(feature = "ble-radio")]
579impl Default for BleFrameLinkConfig {
580 fn default() -> Self {
581 Self {
582 segment_payload: 19,
584 discovery_timeout: Duration::from_secs(10),
585 operation_timeout: Duration::from_secs(10),
586 pairing_timeout: Duration::from_secs(90),
587 }
588 }
589}
590
591#[cfg(feature = "ble-radio")]
592impl BleFrameLinkConfig {
593 fn validate(&self) -> Result<(), UlcpError> {
594 if !(1..=511).contains(&self.segment_payload) {
595 return Err(UlcpError::Protocol(
596 "BLE segment payload must be in 1..=511",
597 ));
598 }
599 if self.discovery_timeout.is_zero()
600 || self.operation_timeout.is_zero()
601 || self.pairing_timeout.is_zero()
602 {
603 return Err(UlcpError::Protocol(
604 "BLE discovery, operation, and pairing timeouts must be nonzero",
605 ));
606 }
607 Ok(())
608 }
609}
610
611#[cfg(feature = "ble-radio")]
612struct BleNotificationReceiver {
613 notifications: tokio::sync::mpsc::Receiver<Vec<u8>>,
614 reassembler: umsh_ulcp::gatt::Reassembler<{ umsh_ulcp::gatt::MAX_FRAME }>,
615}
616
617#[cfg(feature = "ble-radio")]
618impl BleNotificationReceiver {
619 fn new(notifications: tokio::sync::mpsc::Receiver<Vec<u8>>) -> Self {
620 Self {
621 notifications,
622 reassembler: umsh_ulcp::gatt::Reassembler::new(),
623 }
624 }
625
626 fn poll_recv_frame(
627 &mut self,
628 cx: &mut core::task::Context<'_>,
629 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
630 loop {
631 match self.notifications.poll_recv(cx) {
632 core::task::Poll::Ready(Some(segment)) => {
633 if let Some(Ok(frame)) = self.reassembler.push(&segment) {
634 return core::task::Poll::Ready(Ok(frame.to_vec()));
635 }
636 }
638 core::task::Poll::Ready(None) => {
639 self.reassembler.reset();
640 return core::task::Poll::Ready(Err(UlcpError::Disconnected));
641 }
642 core::task::Poll::Pending => return core::task::Poll::Pending,
643 }
644 }
645 }
646}
647
648#[cfg(feature = "ble-radio")]
651#[derive(Clone, Debug)]
652pub struct BleScanResult {
653 pub id: String,
655 pub name: Option<String>,
657 pub rssi: Option<i16>,
659}
660
661#[cfg(feature = "ble-radio")]
663pub struct BleFrameLink {
664 peripheral: btleplug::platform::Peripheral,
665 frame_in: btleplug::api::Characteristic,
666 receiver: BleNotificationReceiver,
667 segment_payload: usize,
668 operation_timeout: Duration,
669}
670
671#[cfg(feature = "ble-radio")]
672impl BleFrameLink {
673 pub async fn scan(timeout: Duration) -> Result<Vec<BleScanResult>, UlcpError> {
680 use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
681
682 let manager = btleplug::platform::Manager::new()
683 .await
684 .map_err(ble_error)?;
685 let adapters = manager.adapters().await.map_err(ble_error)?;
686 let service = uuid::Uuid::from_u128(umsh_ulcp::gatt::SERVICE_UUID);
687 let deadline = Instant::now() + timeout;
688 let mut results: Vec<BleScanResult> = Vec::new();
689
690 for adapter in adapters {
691 adapter
692 .start_scan(ScanFilter {
693 services: vec![service],
694 })
695 .await
696 .map_err(ble_error)?;
697 while Instant::now() < deadline {
698 tokio::time::sleep(Duration::from_millis(250)).await;
699 let peripherals =
700 match tokio::time::timeout_at(deadline, adapter.peripherals()).await {
701 Ok(result) => result.map_err(ble_error)?,
702 Err(_) => break,
703 };
704 for peripheral in peripherals {
705 let properties =
706 match tokio::time::timeout_at(deadline, peripheral.properties()).await {
707 Ok(result) => result.map_err(ble_error)?,
708 Err(_) => break,
709 };
710 let advertises_service = properties
711 .as_ref()
712 .is_some_and(|properties| properties.services.contains(&service));
713 if !advertises_service {
714 continue;
715 }
716 let id = peripheral.id().to_string();
717 let name = properties
718 .as_ref()
719 .and_then(|properties| properties.local_name.clone());
720 let rssi = properties.as_ref().and_then(|properties| properties.rssi);
721 match results.iter_mut().find(|result| result.id == id) {
722 Some(existing) => {
723 existing.name = name.or(existing.name.take());
724 existing.rssi = rssi.or(existing.rssi);
725 }
726 None => results.push(BleScanResult { id, name, rssi }),
727 }
728 }
729 }
730 let _ = tokio::time::timeout(Duration::from_secs(1), adapter.stop_scan()).await;
733 }
734 Ok(results)
735 }
736
737 pub async fn connect(
742 selector: Option<&str>,
743 config: BleFrameLinkConfig,
744 ) -> Result<Self, UlcpError> {
745 use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
746 use futures_util::StreamExt;
747
748 config.validate()?;
749
750 let manager = btleplug::platform::Manager::new()
751 .await
752 .map_err(ble_error)?;
753 let adapters = manager.adapters().await.map_err(ble_error)?;
754 let service = uuid::Uuid::from_u128(umsh_ulcp::gatt::SERVICE_UUID);
755 let deadline = Instant::now() + config.discovery_timeout;
756 let mut matches = Vec::new();
757
758 for adapter in adapters {
759 adapter
760 .start_scan(ScanFilter {
761 services: vec![service],
762 })
763 .await
764 .map_err(ble_error)?;
765 loop {
766 if Instant::now() >= deadline {
767 break;
768 }
769 tokio::time::sleep(Duration::from_millis(250)).await;
770 matches.clear();
771 let peripherals =
772 match tokio::time::timeout_at(deadline, adapter.peripherals()).await {
773 Ok(result) => result.map_err(ble_error)?,
774 Err(_) => break,
775 };
776 for peripheral in peripherals {
777 let properties =
778 match tokio::time::timeout_at(deadline, peripheral.properties()).await {
779 Ok(result) => result.map_err(ble_error)?,
780 Err(_) => break,
781 };
782 let id = peripheral.id().to_string();
783 let name = properties
784 .as_ref()
785 .and_then(|properties| properties.local_name.as_deref());
786 let selected = selector.is_none_or(|selector| {
787 id == selector || name.is_some_and(|name| name.contains(selector))
788 });
789 let advertises_service = properties
790 .as_ref()
791 .is_some_and(|properties| properties.services.contains(&service));
792 if selected && advertises_service {
793 matches.push(peripheral);
794 }
795 }
796 if !matches.is_empty() || Instant::now() >= deadline {
797 break;
798 }
799 }
800 let _ = tokio::time::timeout(Duration::from_secs(1), adapter.stop_scan()).await;
803 if !matches.is_empty() {
804 break;
805 }
806 }
807
808 let peripheral = match matches.len() {
809 0 => {
810 return Err(UlcpError::Transport(
811 "no ULCP GATT Service peripheral found".into(),
812 ));
813 }
814 1 => matches.pop().unwrap(),
815 _ => {
816 return Err(UlcpError::Transport(
817 "multiple companion radios found; provide a selector".into(),
818 ));
819 }
820 };
821
822 let setup = async {
823 let is_connected =
824 tokio::time::timeout(config.operation_timeout, peripheral.is_connected())
825 .await
826 .map_err(|_| ble_timeout("querying connection state"))?
827 .map_err(ble_error)?;
828 if !is_connected {
829 tokio::time::timeout(config.operation_timeout, peripheral.connect())
830 .await
831 .map_err(|_| ble_timeout("connecting"))?
832 .map_err(ble_error)?;
833 }
834 tokio::time::timeout(config.operation_timeout, peripheral.discover_services())
835 .await
836 .map_err(|_| ble_timeout("discovering services"))?
837 .map_err(ble_error)?;
838
839 let frame_in_uuid = uuid::Uuid::from_u128(umsh_ulcp::gatt::FRAME_IN_UUID);
840 let frame_out_uuid = uuid::Uuid::from_u128(umsh_ulcp::gatt::FRAME_OUT_UUID);
841 let characteristics = peripheral.characteristics();
842 let frame_in = characteristics
843 .iter()
844 .find(|characteristic| characteristic.uuid == frame_in_uuid)
845 .cloned()
846 .ok_or(UlcpError::Protocol("missing BLE Frame In"))?;
847 let frame_out = characteristics
848 .iter()
849 .find(|characteristic| characteristic.uuid == frame_out_uuid)
850 .cloned()
851 .ok_or(UlcpError::Protocol("missing BLE Frame Out"))?;
852
853 let mut stream =
854 tokio::time::timeout(config.operation_timeout, peripheral.notifications())
855 .await
856 .map_err(|_| ble_timeout("opening notifications"))?
857 .map_err(ble_error)?;
858 let (tx, notifications) = tokio::sync::mpsc::channel(32);
859 tokio::spawn(async move {
860 while let Some(notification) = stream.next().await {
861 if notification.uuid == frame_out_uuid
862 && tx.send(notification.value).await.is_err()
863 {
864 break;
865 }
866 }
867 });
868 tokio::time::timeout(config.pairing_timeout, peripheral.subscribe(&frame_out))
871 .await
872 .map_err(|_| ble_timeout("subscribing to Frame Out"))?
873 .map_err(ble_error)?;
874 Ok::<_, UlcpError>((frame_in, notifications))
875 }
876 .await;
877
878 let (frame_in, notifications) = match setup {
879 Ok(setup) => setup,
880 Err(error) => {
881 let _ = tokio::time::timeout(Duration::from_secs(1), peripheral.disconnect()).await;
884 return Err(error);
885 }
886 };
887
888 Ok(Self {
889 peripheral,
890 frame_in,
891 receiver: BleNotificationReceiver::new(notifications),
892 segment_payload: config.segment_payload,
893 operation_timeout: config.operation_timeout,
894 })
895 }
896
897 async fn diagnose_and_disconnect(&self, failure: String) -> UlcpError {
901 use btleplug::api::Peripheral as _;
902
903 let connected = match tokio::time::timeout(
904 Duration::from_secs(2),
905 self.peripheral.is_connected(),
906 )
907 .await
908 {
909 Ok(Ok(value)) => value.to_string(),
910 Ok(Err(error)) => format!("error({error})"),
911 Err(_) => "query-timeout".into(),
912 };
913 let cleanup = match tokio::time::timeout(
914 Duration::from_secs(2),
915 self.peripheral.disconnect(),
916 )
917 .await
918 {
919 Ok(Ok(())) => "ok".into(),
920 Ok(Err(error)) => format!("error({error})"),
921 Err(_) => "timeout".into(),
922 };
923 UlcpError::Transport(format!(
924 "{failure}; backend is_connected={connected}; disconnect cleanup={cleanup}"
925 ))
926 }
927}
928
929#[cfg(feature = "ble-radio")]
930impl FrameLink for BleFrameLink {
931 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
932 use btleplug::api::{Peripheral as _, WriteType};
933
934 for segment in umsh_ulcp::gatt::segments(frame, self.segment_payload) {
935 let mut value = vec![0; segment.payload().len() + 1];
936 segment
937 .write_to(&mut value)
938 .expect("segment destination is exactly sized");
939 let write = tokio::time::timeout(
940 self.operation_timeout,
941 self.peripheral
942 .write(&self.frame_in, &value, WriteType::WithResponse),
943 )
944 .await;
945 match write {
946 Ok(Ok(())) => {}
947 Ok(Err(error)) => {
948 return Err(self
949 .diagnose_and_disconnect(format!("BLE Frame In write failed: {error}"))
950 .await);
951 }
952 Err(_) => {
953 return Err(self
954 .diagnose_and_disconnect("BLE timed out while writing Frame In".into())
955 .await);
956 }
957 }
958 }
959 Ok(())
960 }
961
962 fn poll_recv_frame(
963 &mut self,
964 cx: &mut core::task::Context<'_>,
965 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
966 self.receiver.poll_recv_frame(cx)
967 }
968}
969
970#[cfg(feature = "ble-radio")]
971fn ble_error(error: btleplug::Error) -> UlcpError {
972 UlcpError::Transport(error.to_string())
973}
974
975#[cfg(feature = "ble-radio")]
976fn ble_timeout(operation: &'static str) -> UlcpError {
977 UlcpError::Transport(format!("BLE timed out while {operation}"))
978}
979
980pub struct UlcpDevice<L> {
983 link: L,
984 config: UlcpDeviceConfig,
985 rx_queue: VecDeque<RxPacket>,
986 responses: VecDeque<Response>,
987 prop_events: VecDeque<PropEvent>,
988 seen_reset: Option<Status>,
990 max_frame_size: usize,
991 t_frame_ms: u32,
992 dev_version: String,
993 dev_model: Option<String>,
994 boot_status: Status,
996 tids: TidAllocator,
997 trace: Option<FrameTrace>,
999 mode: AttachMode,
1000}
1001
1002#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1011pub enum AttachMode {
1012 #[default]
1015 Tethered,
1016 Administrative,
1019 Remote,
1030}
1031
1032impl<L> UlcpDevice<L>
1033where
1034 L: FrameLink,
1035{
1036 fn bare(link: L, config: UlcpDeviceConfig) -> Self {
1037 Self {
1038 link,
1039 config,
1040 rx_queue: VecDeque::new(),
1041 responses: VecDeque::new(),
1042 prop_events: VecDeque::new(),
1043 seen_reset: None,
1044 max_frame_size: 0,
1045 t_frame_ms: 0,
1046 dev_version: String::new(),
1047 dev_model: None,
1048 boot_status: Status::RESET_UNKNOWN,
1049 tids: TidAllocator::new(),
1050 trace: None,
1051 mode: AttachMode::Tethered,
1052 }
1053 }
1054
1055 pub fn attach_mode(&self) -> AttachMode {
1057 self.mode
1058 }
1059
1060 pub fn is_remote(&self) -> bool {
1064 self.mode == AttachMode::Remote
1065 }
1066
1067 fn prop_reachable(&self, key: u32) -> bool {
1074 !self.is_remote() || ids::admin_reachable(key)
1075 }
1076
1077 fn require_tethered(&self, key: u32) -> Result<(), UlcpError> {
1079 let host_domain = matches!(
1080 key,
1081 prop::HOST_KEY
1082 | prop::HOST_CHANNEL_KEYS
1083 | prop::HOST_PEER_KEYS
1084 | prop::HOST_RX_FILTERS
1085 | prop::HOST_AUTO_ACK
1086 );
1087 match self.mode {
1088 AttachMode::Administrative | AttachMode::Remote if host_domain => {
1089 Err(UlcpError::AdministrativeAttach)
1090 }
1091 _ => Ok(()),
1092 }
1093 }
1094
1095 pub async fn new(link: L, config: UlcpDeviceConfig) -> Result<Self, UlcpError> {
1105 let mut radio = Self::bare(link, config);
1106 radio.initialize().await?;
1107 Ok(radio)
1108 }
1109
1110 pub async fn attach_existing(link: L, config: UlcpDeviceConfig) -> Result<Self, UlcpError> {
1127 Self::attach_with_mode(link, config, AttachMode::Tethered).await
1128 }
1129
1130 pub async fn attach_administrative(
1146 link: L,
1147 config: UlcpDeviceConfig,
1148 ) -> Result<Self, UlcpError> {
1149 Self::attach_with_mode(link, config, AttachMode::Administrative).await
1150 }
1151
1152 pub fn open_remote(link: L, config: UlcpDeviceConfig) -> Self {
1174 let mut radio = Self::bare(link, config);
1175 radio.mode = AttachMode::Remote;
1176 radio.max_frame_size = umsh_node_mgmt::REQUEST_MAX;
1177 radio.t_frame_ms = lora_airtime_ms(
1178 radio.config.spreading_factor,
1179 radio.config.bandwidth_hz,
1180 radio.config.coding_rate_denom,
1181 radio.max_frame_size,
1182 )
1183 .max(1);
1184 radio
1185 }
1186
1187 async fn attach_with_mode(
1188 link: L,
1189 config: UlcpDeviceConfig,
1190 mode: AttachMode,
1191 ) -> Result<Self, UlcpError> {
1192 let mut radio = Self::bare(link, config);
1193 radio.mode = mode;
1194 let boot_status = radio.get_prop(prop::LAST_STATUS).await?;
1203 radio.boot_status = decode_status(&boot_status);
1204
1205 const REST: [u32; 4] = [
1207 prop::PROTOCOL_VERSION,
1208 prop::DEV_VERSION,
1209 prop::DEV_MODEL,
1210 prop::PHY_MTU,
1211 ];
1212 let answers = radio.read_each(&REST).await?;
1213 let [version, dev_version, dev_model, mtu] = answers.as_slice() else {
1214 return Err(UlcpError::Protocol("short answer to the attach handshake"));
1215 };
1216 let required = |answer: &Result<Vec<u8>, Status>| match answer {
1217 Ok(value) => Ok(value.clone()),
1218 Err(status) => Err(UlcpError::Status(*status)),
1219 };
1220
1221 let version = required(version)?;
1222 if version.first().copied() != Some(ids::PROTOCOL_MAJOR_VERSION) {
1223 return Err(UlcpError::Protocol("protocol major version mismatch"));
1224 }
1225 radio.dev_version = String::from_utf8_lossy(&required(dev_version)?)
1226 .trim_end_matches('\0')
1227 .to_owned();
1228 radio.dev_model = dev_model.as_ref().ok().map(|value| {
1232 String::from_utf8_lossy(value)
1233 .trim_end_matches('\0')
1234 .to_owned()
1235 });
1236
1237 let mtu = required(mtu)?;
1238 let [mtu_lo, mtu_hi, ..] = mtu[..] else {
1239 return Err(UlcpError::Protocol("malformed PROP_PHY_MTU"));
1240 };
1241 radio.max_frame_size = usize::from(u16::from_le_bytes([mtu_lo, mtu_hi]));
1242 if radio.max_frame_size == 0 {
1243 return Err(UlcpError::Protocol("device advertised zero MTU"));
1244 }
1245 radio.t_frame_ms = lora_airtime_ms(
1246 radio.config.spreading_factor,
1247 radio.config.bandwidth_hz,
1248 radio.config.coding_rate_denom,
1249 radio.max_frame_size,
1250 )
1251 .max(1);
1252 Ok(radio)
1253 }
1254
1255 pub fn into_link(self) -> L {
1265 self.link
1266 }
1267
1268 pub fn set_frame_trace(&mut self, trace: Option<FrameTrace>) {
1273 self.trace = trace;
1274 }
1275
1276 async fn send(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
1278 if let Some(trace) = &mut self.trace {
1279 trace(TraceDirection::HostToDevice, &describe_frame(frame));
1280 }
1281 self.link.send_frame(frame).await
1282 }
1283
1284 pub fn dev_version(&self) -> &str {
1286 &self.dev_version
1287 }
1288
1289 pub fn dev_model(&self) -> Option<&str> {
1293 self.dev_model.as_deref()
1294 }
1295
1296 pub async fn device_name(&mut self) -> Result<String, UlcpError> {
1298 let value = self.get_prop(prop::DEV_NAME).await?;
1299 let name = core::str::from_utf8(&value)
1300 .map_err(|_| UlcpError::Protocol("malformed PROP_DEV_NAME"))?;
1301 if name.is_empty() || value.len() > 64 || value.contains(&0) {
1302 return Err(UlcpError::Protocol("malformed PROP_DEV_NAME"));
1303 }
1304 Ok(name.to_owned())
1305 }
1306
1307 pub async fn set_device_name(&mut self, name: &str) -> Result<(), UlcpError> {
1309 if name.is_empty() || name.len() > 64 || name.as_bytes().contains(&0) {
1310 return Err(UlcpError::Protocol("invalid PROP_DEV_NAME"));
1311 }
1312 let authoritative = self.set_prop(prop::DEV_NAME, name.as_bytes()).await?;
1313 if authoritative != name.as_bytes() {
1314 return Err(UlcpError::Protocol("PROP_DEV_NAME response mismatch"));
1315 }
1316 Ok(())
1317 }
1318
1319 pub async fn battery_status(&mut self) -> Result<Option<BatteryStatus>, UlcpError> {
1332 let value = match self.get_prop(prop::BATTERY).await {
1333 Ok(value) => value,
1334 Err(UlcpError::Status(Status::PROP_NOT_FOUND)) => return Ok(None),
1335 Err(error) => return Err(error),
1336 };
1337 match BatteryStatus::decode(&value) {
1338 Ok(status) => Ok(Some(status)),
1339 Err(_) => Err(UlcpError::Protocol("malformed PROP_BATTERY")),
1340 }
1341 }
1342
1343 pub async fn illuminance(&mut self) -> Result<Option<u32>, UlcpError> {
1352 if !self.capabilities().await?.contains(&cap::ILLUMINANCE) {
1353 return Ok(None);
1354 }
1355 let value = self.get_prop(prop::ILLUMINANCE).await?;
1356 match value.len() {
1357 0 => Ok(None),
1358 4 => Ok(Some(u32::from_le_bytes(value[..4].try_into().unwrap()))),
1359 _ => Err(UlcpError::Protocol("malformed PROP_ILLUMINANCE")),
1360 }
1361 }
1362
1363 pub async fn alert(&mut self) -> Result<Option<AlertState>, UlcpError> {
1368 if !self.capabilities().await?.contains(&cap::ALERT) {
1369 return Ok(None);
1370 }
1371 let value = self.get_prop(prop::ALERT).await?;
1372 Ok(Some(decode_alert(&value)?))
1373 }
1374
1375 pub async fn set_alert(&mut self, state: AlertState) -> Result<AlertState, UlcpError> {
1388 let mut value = [0u8; pui::MAX_LEN];
1389 let len = pui::encode(state.code(), &mut value)
1390 .map_err(|_| UlcpError::Protocol("PROP_ALERT encode"))?;
1391 let authoritative = self.set_prop(prop::ALERT, &value[..len]).await?;
1392 decode_alert(&authoritative)
1393 }
1394
1395 pub fn boot_status(&self) -> Status {
1397 self.boot_status
1398 }
1399
1400 pub async fn repeater_policy(&mut self) -> Result<Option<RepeaterPolicy>, UlcpError> {
1404 if !self.capabilities().await?.contains(&cap::REPEATER) {
1405 return Ok(None);
1406 }
1407 let enabled = self.get_prop(prop::MAC_REPEATER_ENABLED).await?;
1408 let enabled = match enabled.first() {
1409 Some(&byte) => byte != 0,
1410 None => return Err(UlcpError::Protocol("malformed PROP_MAC_REPEATER_ENABLED")),
1411 };
1412 let regions = decode_region_list(&self.get_prop(prop::MAC_REPEATER_REGIONS).await?)?;
1413 let default_region =
1414 decode_region_code(&self.get_prop(prop::MAC_REPEATER_DEFAULT_REGION).await?)?;
1415 let min_rssi = decode_opt_i16(&self.get_prop(prop::MAC_REPEATER_MIN_RSSI).await?)
1416 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_RSSI"))?;
1417 let min_snr = decode_opt_i8(&self.get_prop(prop::MAC_REPEATER_MIN_SNR).await?)
1418 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_SNR"))?;
1419 Ok(Some(RepeaterPolicy {
1420 enabled,
1421 regions,
1422 default_region,
1423 min_rssi,
1424 min_snr,
1425 }))
1426 }
1427
1428 pub async fn repeater_regions(&mut self) -> Result<Vec<String>, UlcpError> {
1431 decode_region_list(&self.get_prop(prop::MAC_REPEATER_REGIONS).await?)
1432 }
1433
1434 pub async fn set_repeater_regions(
1443 &mut self,
1444 regions: &[String],
1445 ) -> Result<Vec<String>, UlcpError> {
1446 let mut value = Vec::new();
1447 for region in regions {
1448 check_region(region)?;
1449 let mut item = vec![0u8; region.len() + 4];
1450 let len = items::encode_prefixed_item(region.as_bytes(), &mut item)
1451 .map_err(|_| UlcpError::Protocol("region item encode"))?;
1452 value.extend_from_slice(&item[..len]);
1453 }
1454 let authoritative = self.set_prop(prop::MAC_REPEATER_REGIONS, &value).await?;
1455 decode_region_list(&authoritative)
1456 }
1457
1458 pub async fn add_repeater_region(&mut self, region: &str) -> Result<(), UlcpError> {
1461 check_region(region)?;
1462 self.insert_prop_item(prop::MAC_REPEATER_REGIONS, region.as_bytes())
1463 .await
1464 .map(|_| ())
1465 }
1466
1467 pub async fn remove_repeater_region(&mut self, region: &str) -> Result<(), UlcpError> {
1470 check_region(region)?;
1471 self.remove_prop_item(prop::MAC_REPEATER_REGIONS, region.as_bytes())
1472 .await
1473 .map(|_| ())
1474 }
1475
1476 pub async fn set_repeater_default_region(
1484 &mut self,
1485 region: Option<RegionCode>,
1486 ) -> Result<Option<RegionCode>, UlcpError> {
1487 let value = region.map(|code| code.to_bytes()).unwrap_or_default();
1488 let value: &[u8] = match region {
1489 Some(_) => &value,
1490 None => &[],
1491 };
1492 let authoritative = self
1493 .set_prop(prop::MAC_REPEATER_DEFAULT_REGION, value)
1494 .await?;
1495 decode_region_code(&authoritative)
1496 }
1497
1498 pub async fn set_repeater_min_rssi(
1501 &mut self,
1502 min_rssi: Option<i16>,
1503 ) -> Result<Option<i16>, UlcpError> {
1504 let encoded = min_rssi.map(i16::to_le_bytes).unwrap_or_default();
1505 let value: &[u8] = match min_rssi {
1506 Some(_) => &encoded,
1507 None => &[],
1508 };
1509 let authoritative = self.set_prop(prop::MAC_REPEATER_MIN_RSSI, value).await?;
1510 decode_opt_i16(&authoritative)
1511 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_RSSI"))
1512 }
1513
1514 pub async fn set_repeater_min_snr(
1517 &mut self,
1518 min_snr: Option<i8>,
1519 ) -> Result<Option<i8>, UlcpError> {
1520 let encoded = [min_snr.unwrap_or_default() as u8];
1521 let value: &[u8] = match min_snr {
1522 Some(_) => &encoded,
1523 None => &[],
1524 };
1525 let authoritative = self.set_prop(prop::MAC_REPEATER_MIN_SNR, value).await?;
1526 decode_opt_i8(&authoritative)
1527 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_SNR"))
1528 }
1529
1530 pub async fn time(&mut self) -> Result<Option<DeviceTime>, UlcpError> {
1538 if !self.capabilities().await?.contains(&cap::TIME) {
1539 return Ok(None);
1540 }
1541 let epoch = decode_epoch(&self.get_prop(prop::TIME).await?)?;
1542 let tz_offset_min = decode_tz_offset(&self.get_prop(prop::TZ_OFFSET).await?)?;
1543 Ok(Some(DeviceTime {
1544 epoch,
1545 tz_offset_min,
1546 }))
1547 }
1548
1549 pub async fn set_time(&mut self, epoch: Option<u32>) -> Result<Option<u32>, UlcpError> {
1555 let encoded = epoch.map(u32::to_le_bytes).unwrap_or_default();
1556 let value: &[u8] = match epoch {
1557 Some(_) => &encoded,
1558 None => &[],
1559 };
1560 let authoritative = self.set_prop(prop::TIME, value).await?;
1561 decode_epoch(&authoritative)
1562 }
1563
1564 pub async fn set_tz_offset(&mut self, minutes: i16) -> Result<i16, UlcpError> {
1567 let authoritative = self
1568 .set_prop(prop::TZ_OFFSET, &minutes.to_le_bytes())
1569 .await?;
1570 decode_tz_offset(&authoritative)
1571 }
1572
1573 pub async fn gnss_status(&mut self) -> Result<Option<GnssStatus>, UlcpError> {
1580 if !self.capabilities().await?.contains(&cap::GNSS) {
1581 return Ok(None);
1582 }
1583 let enabled = decode_bool(
1584 &self.get_prop(prop::GNSS_ENABLED).await?,
1585 "PROP_GNSS_ENABLED",
1586 )?;
1587 let mut fix = GnssSnapshot::SEARCHING;
1588 for key in [
1589 prop::GNSS_FIX,
1590 prop::GNSS_LOCATION,
1591 prop::GNSS_ALTITUDE,
1592 prop::GNSS_PRECISION,
1593 prop::GNSS_SATELLITES,
1594 ] {
1595 let value = self.get_prop(key).await?;
1596 fix.absorb(key, &value)
1597 .map_err(|_| UlcpError::Protocol("malformed PROP_GNSS_* value"))?;
1598 }
1599 let ident_update = decode_bool(
1600 &self.get_prop(prop::GNSS_IDENT_UPDATE).await?,
1601 "PROP_GNSS_IDENT_UPDATE",
1602 )?;
1603 let ident_precision = match self.get_prop(prop::GNSS_IDENT_PRECISION).await?[..] {
1604 [precision] => precision,
1605 _ => return Err(UlcpError::Protocol("malformed PROP_GNSS_IDENT_PRECISION")),
1606 };
1607 let time_trust = decode_bool(
1608 &self.get_prop(prop::GNSS_TIME_TRUST).await?,
1609 "PROP_GNSS_TIME_TRUST",
1610 )?;
1611 Ok(Some(GnssStatus {
1612 enabled,
1613 fix,
1614 ident_update,
1615 ident_precision,
1616 time_trust,
1617 }))
1618 }
1619
1620 pub async fn advert_policy(&mut self) -> Result<Option<AdvertPolicy>, UlcpError> {
1623 if !self.capabilities().await?.contains(&cap::ADVERT) {
1624 return Ok(None);
1625 }
1626 let advert_interval_s = self.get_interval(prop::ADVERT_INTERVAL).await?;
1627 let beacon_interval_s = self.get_interval(prop::BEACON_INTERVAL).await?;
1628 let startup_beacon = decode_bool(
1629 &self.get_prop(prop::STARTUP_BEACON).await?,
1630 "PROP_STARTUP_BEACON",
1631 )?;
1632 Ok(Some(AdvertPolicy {
1633 advert_interval_s,
1634 beacon_interval_s,
1635 startup_beacon,
1636 }))
1637 }
1638
1639 async fn get_interval(&mut self, key: u32) -> Result<u32, UlcpError> {
1640 decode_interval(&self.get_prop(key).await?)
1641 }
1642
1643 pub async fn set_advert_interval(&mut self, seconds: u32) -> Result<u32, UlcpError> {
1646 let authoritative = self
1647 .set_prop(prop::ADVERT_INTERVAL, &seconds.to_le_bytes())
1648 .await?;
1649 decode_interval(&authoritative)
1650 }
1651
1652 pub async fn set_beacon_interval(&mut self, seconds: u32) -> Result<u32, UlcpError> {
1655 let authoritative = self
1656 .set_prop(prop::BEACON_INTERVAL, &seconds.to_le_bytes())
1657 .await?;
1658 decode_interval(&authoritative)
1659 }
1660
1661 pub async fn set_startup_beacon(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1663 let authoritative = self
1664 .set_prop(prop::STARTUP_BEACON, &[enabled as u8])
1665 .await?;
1666 decode_bool(&authoritative, "PROP_STARTUP_BEACON")
1667 }
1668
1669 pub async fn set_gnss_enabled(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1671 let authoritative = self.set_prop(prop::GNSS_ENABLED, &[enabled as u8]).await?;
1672 decode_bool(&authoritative, "PROP_GNSS_ENABLED")
1673 }
1674
1675 pub async fn set_gnss_ident_update(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1678 let authoritative = self
1679 .set_prop(prop::GNSS_IDENT_UPDATE, &[enabled as u8])
1680 .await?;
1681 decode_bool(&authoritative, "PROP_GNSS_IDENT_UPDATE")
1682 }
1683
1684 pub async fn set_gnss_ident_precision(&mut self, precision: u8) -> Result<u8, UlcpError> {
1687 let authoritative = self
1688 .set_prop(prop::GNSS_IDENT_PRECISION, &[precision])
1689 .await?;
1690 match authoritative[..] {
1691 [stored] => Ok(stored),
1692 _ => Err(UlcpError::Protocol("malformed PROP_GNSS_IDENT_PRECISION")),
1693 }
1694 }
1695
1696 pub async fn set_gnss_time_trust(&mut self, trust: bool) -> Result<bool, UlcpError> {
1702 let authoritative = self.set_prop(prop::GNSS_TIME_TRUST, &[trust as u8]).await?;
1703 decode_bool(&authoritative, "PROP_GNSS_TIME_TRUST")
1704 }
1705
1706 async fn initialize(&mut self) -> Result<(), UlcpError> {
1707 let boot_status = self.get_prop(prop::LAST_STATUS).await?;
1711 self.boot_status = decode_status(&boot_status);
1712
1713 let mut buf = [0u8; 2];
1716 let len = frame::reset(&mut buf, TID_UNSOLICITED)
1717 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1718 self.send(&buf[..len]).await?;
1719 let deadline = Instant::now() + self.config.response_timeout;
1720 self.wait_reset(deadline).await?;
1721
1722 let version = self.get_prop(prop::PROTOCOL_VERSION).await?;
1724 if version.first().copied() != Some(ids::PROTOCOL_MAJOR_VERSION) {
1725 return Err(UlcpError::Protocol("protocol major version mismatch"));
1726 }
1727
1728 let dev_version = self.get_prop(prop::DEV_VERSION).await?;
1729 self.dev_version = String::from_utf8_lossy(&dev_version)
1730 .trim_end_matches('\0')
1731 .to_owned();
1732 self.dev_model = self.get_prop_string_opt(prop::DEV_MODEL).await;
1733
1734 let mtu = self.get_prop(prop::PHY_MTU).await?;
1735 let [mtu_lo, mtu_hi, ..] = mtu[..] else {
1736 return Err(UlcpError::Protocol("malformed PROP_PHY_MTU"));
1737 };
1738 self.max_frame_size = usize::from(u16::from_le_bytes([mtu_lo, mtu_hi]));
1739 if self.max_frame_size == 0 {
1740 return Err(UlcpError::Protocol("device advertised zero MTU"));
1741 }
1742
1743 let config = self.config.clone();
1744 self.set_prop(prop::PHY_FREQ, &config.freq_khz.to_le_bytes())
1745 .await?;
1746 self.set_prop(prop::PHY_LORA_BW, &config.bandwidth_hz.to_le_bytes())
1747 .await?;
1748 self.set_prop(prop::PHY_LORA_SF, &[config.spreading_factor])
1749 .await?;
1750 self.set_prop(prop::PHY_LORA_CR, &[config.coding_rate_denom])
1751 .await?;
1752 self.set_prop(prop::PHY_TX_POWER, &[config.tx_power_dbm as u8])
1753 .await?;
1754 self.set_prop(prop::PHY_LORA_SW, &config.sync_word.to_le_bytes())
1755 .await?;
1756 self.set_prop(prop::PHY_ENABLED, &[1]).await?;
1757
1758 self.t_frame_ms = lora_airtime_ms(
1759 config.spreading_factor,
1760 config.bandwidth_hz,
1761 config.coding_rate_denom,
1762 self.max_frame_size,
1763 )
1764 .max(1);
1765 Ok(())
1766 }
1767
1768 pub async fn get_prop(&mut self, key: u32) -> Result<Vec<u8>, UlcpError> {
1770 let tid = self.alloc_tid();
1771 let mut buf = [0u8; 8];
1772 let len =
1773 frame::prop_get(&mut buf, tid, key).map_err(|_| UlcpError::Protocol("frame encode"))?;
1774 self.send(&buf[..len]).await?;
1775 self.finish_prop_transaction(tid, key, PropResponsePolicy::Value)
1776 .await
1777 }
1778
1779 async fn get_prop_string_opt(&mut self, key: u32) -> Option<String> {
1787 let value = self.get_prop(key).await.ok()?;
1788 Some(
1789 String::from_utf8_lossy(&value)
1790 .trim_end_matches('\0')
1791 .to_owned(),
1792 )
1793 }
1794
1795 pub async fn set_prop(&mut self, key: u32, value: &[u8]) -> Result<Vec<u8>, UlcpError> {
1798 self.require_tethered(key)?;
1799 let tid = self.alloc_tid();
1800 let mut buf = vec![0u8; value.len() + 8];
1801 let len = frame::prop_set(&mut buf, tid, key, value)
1802 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1803 self.send(&buf[..len]).await?;
1804 self.finish_prop_transaction(tid, key, PropResponsePolicy::Value)
1805 .await
1806 }
1807
1808 pub async fn read_each(
1820 &mut self,
1821 keys: &[u32],
1822 ) -> Result<Vec<Result<Vec<u8>, Status>>, UlcpError> {
1823 debug_assert!(
1824 !keys.contains(&prop::LAST_STATUS),
1825 "PROP_LAST_STATUS cannot share a multi-property read"
1826 );
1827 let mut answers: Vec<Result<Vec<u8>, Status>> = Vec::with_capacity(keys.len());
1828 while answers.len() < keys.len() {
1829 let remaining = &keys[answers.len()..];
1830 let entries = self.get_props(remaining).await?;
1831 if entries.is_empty() {
1832 return Err(UlcpError::Protocol("empty multi-property answer"));
1835 }
1836 for (offset, entry) in entries.into_iter().enumerate() {
1837 answers.push(match entry {
1838 Ok((key, value)) if remaining.get(offset) == Some(&key) => Ok(value),
1844 Ok(_) => {
1845 return Err(UlcpError::Protocol("multi-property answer out of order"));
1846 }
1847 Err(status) => Err(status),
1848 });
1849 }
1850 }
1851 Ok(answers)
1852 }
1853
1854 pub async fn get_props(&mut self, keys: &[u32]) -> Result<Vec<MultiValue>, UlcpError> {
1870 let tid = self.alloc_tid();
1871 let mut buf = vec![0u8; keys.len() * pui::MAX_LEN + 8];
1872 let len = frame::prop_multi_get(&mut buf, tid, keys)
1873 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1874 self.send(&buf[..len]).await?;
1875 self.finish_multi_transaction(tid).await
1876 }
1877
1878 pub async fn set_props(
1888 &mut self,
1889 entries: &[(u32, Vec<u8>)],
1890 ) -> Result<Vec<MultiValue>, UlcpError> {
1891 let mut capacity = 8;
1892 for (key, value) in entries {
1893 self.require_tethered(*key)?;
1894 capacity += value.len() + pui::MAX_LEN * 2;
1895 }
1896 let borrowed: Vec<(u32, &[u8])> = entries
1897 .iter()
1898 .map(|(key, value)| (*key, value.as_slice()))
1899 .collect();
1900 let tid = self.alloc_tid();
1901 let mut buf = vec![0u8; capacity];
1902 let len = frame::prop_multi_set(&mut buf, tid, &borrowed)
1903 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1904 self.send(&buf[..len]).await?;
1905 self.finish_multi_transaction(tid).await
1906 }
1907
1908 async fn finish_multi_transaction(&mut self, tid: u8) -> Result<Vec<MultiValue>, UlcpError> {
1911 let deadline = Instant::now() + self.config.response_timeout;
1912 let response = self.wait_response(tid, deadline).await?;
1913 match response.kind {
1914 ResponseKind::Are => {}
1915 ResponseKind::Is if response.key == prop::LAST_STATUS => {
1918 return Err(UlcpError::Status(decode_status(&response.value)));
1919 }
1920 _ => {
1921 return Err(UlcpError::Protocol(
1922 "single-property response answering a multi-property command",
1923 ));
1924 }
1925 }
1926 let mut values = Vec::new();
1927 for entry in MultiEntries::new(&response.value) {
1928 let entry = entry.map_err(|_| UlcpError::Protocol("malformed multi-property entry"))?;
1929 values.push(if entry.key == prop::LAST_STATUS {
1930 Err(decode_status(entry.value))
1931 } else {
1932 Ok((entry.key, entry.value.to_vec()))
1933 });
1934 }
1935 Ok(values)
1936 }
1937
1938 pub async fn insert_prop_item(&mut self, key: u32, item: &[u8]) -> Result<Vec<u8>, UlcpError> {
1946 self.require_tethered(key)?;
1947 let tid = self.alloc_tid();
1948 let mut buf = vec![0u8; item.len() + 8];
1949 let len = frame::prop_insert(&mut buf, tid, key, item)
1950 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1951 self.send(&buf[..len]).await?;
1952 self.finish_table_transaction(tid, key, ResponseKind::Inserted)
1953 .await
1954 }
1955
1956 pub async fn remove_prop_item(
1963 &mut self,
1964 key: u32,
1965 selector: &[u8],
1966 ) -> Result<Vec<u8>, UlcpError> {
1967 self.require_tethered(key)?;
1968 let tid = self.alloc_tid();
1969 let mut buf = vec![0u8; selector.len() + 8];
1970 let len = frame::prop_remove(&mut buf, tid, key, selector)
1971 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1972 self.send(&buf[..len]).await?;
1973 self.finish_table_transaction(tid, key, ResponseKind::Removed)
1974 .await
1975 }
1976
1977 async fn status_only_command(
1980 &mut self,
1981 encode: fn(&mut [u8], u8) -> Result<usize, frame::WriteError>,
1982 ) -> Result<(), UlcpError> {
1983 let tid = self.alloc_tid();
1984 let mut buf = [0u8; 4];
1985 let len = encode(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
1986 self.send(&buf[..len]).await?;
1987 self.finish_prop_transaction(tid, prop::LAST_STATUS, PropResponsePolicy::StatusOnly)
1988 .await
1989 .map(|_| ())
1990 }
1991
1992 pub async fn queue_drain(&mut self) -> Result<(), UlcpError> {
1998 self.queue_drain_with(|_data, _meta| {}).await
1999 }
2000
2001 pub async fn queue_drain_with(
2009 &mut self,
2010 mut on_frame: impl FnMut(&[u8], &[u8]),
2011 ) -> Result<(), UlcpError> {
2012 let tid = self.alloc_tid();
2013 let mut buf = [0u8; 4];
2014 let len =
2015 frame::queue_drain(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
2016 self.send(&buf[..len]).await?;
2017
2018 let deadline = Instant::now() + self.config.response_timeout;
2019 loop {
2020 while let Some(response) = self.responses.pop_front() {
2021 if response.tid != tid {
2022 continue;
2023 }
2024 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
2025 let status = decode_status(&response.value);
2026 return if status == Status::OK {
2027 Ok(())
2028 } else {
2029 Err(UlcpError::Status(status))
2030 };
2031 }
2032 return Err(UlcpError::Protocol("unexpected drain response"));
2033 }
2034 if let Some(status) = self.seen_reset.take() {
2035 return Err(UlcpError::UnexpectedReset(status));
2036 }
2037 if self.read_more(deadline).await? {
2041 let packet = self
2042 .rx_queue
2043 .back()
2044 .expect("read_more reported a queued frame");
2045 on_frame(&packet.data, &packet.raw_meta);
2046 }
2047 }
2048 }
2049
2050 pub async fn save(&mut self) -> Result<(), UlcpError> {
2053 self.status_only_command(frame::save).await
2054 }
2055
2056 pub async fn clear(&mut self) -> Result<(), UlcpError> {
2059 self.status_only_command(frame::clear).await
2060 }
2061
2062 pub async fn reset(&mut self) -> Result<Status, UlcpError> {
2068 let mut buf = [0u8; 2];
2069 let len = frame::reset(&mut buf, TID_UNSOLICITED)
2070 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2071 self.send(&buf[..len]).await?;
2072 let deadline = Instant::now() + self.config.response_timeout;
2073 self.wait_reset(deadline).await
2074 }
2075
2076 pub async fn factory_reset(&mut self) -> Result<(), UlcpError> {
2085 let mut buf = [0u8; 2];
2086 let len = frame::factory_reset(&mut buf, TID_UNSOLICITED)
2087 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2088 self.send(&buf[..len]).await?;
2089 Ok(())
2090 }
2091
2092 pub async fn reboot(&mut self) -> Result<bool, UlcpError> {
2102 if !self.capabilities().await?.contains(&cap::REBOOT) {
2103 return Ok(false);
2104 }
2105 let mut buf = [0u8; 2];
2106 let len = frame::reboot(&mut buf, TID_UNSOLICITED)
2107 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2108 self.send(&buf[..len]).await?;
2109 Ok(true)
2110 }
2111
2112 pub async fn ble_clear_bonds(&mut self) -> Result<bool, UlcpError> {
2126 if !self.capabilities().await?.contains(&cap::BLE) {
2127 return Ok(false);
2128 }
2129 self.status_only_command(frame::ble_clear_bonds).await?;
2130 Ok(true)
2131 }
2132
2133 pub async fn set_ble_pairing(&mut self, open: bool) -> Result<Option<bool>, UlcpError> {
2147 if !self.capabilities().await?.contains(&cap::BLE) {
2148 return Ok(None);
2149 }
2150 let value = self.set_prop(prop::BLE_PAIRING, &[open as u8]).await?;
2151 Ok(Some(value.first().copied() == Some(1)))
2152 }
2153
2154 pub async fn restore(&mut self) -> Result<RestoreCompletion, UlcpError> {
2157 let tid = self.alloc_tid();
2158 let mut buf = [0u8; 4];
2159 let len = frame::restore(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
2160 self.send(&buf[..len]).await?;
2161
2162 let deadline = Instant::now() + self.config.response_timeout;
2163 loop {
2164 while let Some(response) = self.responses.pop_front() {
2165 if response.tid != tid {
2166 continue;
2167 }
2168 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
2169 let status = decode_status(&response.value);
2170 return if status == Status::OK {
2171 Ok(RestoreCompletion::Updated)
2172 } else {
2173 Err(UlcpError::Status(status))
2174 };
2175 }
2176 return Err(UlcpError::Protocol("unexpected restore response"));
2177 }
2178 match self.seen_reset.take() {
2179 Some(status) if status == Status::RESET_RESTORED => {
2180 return Ok(RestoreCompletion::Reset);
2181 }
2182 Some(status) => return Err(UlcpError::UnexpectedReset(status)),
2183 None => {}
2184 }
2185 self.read_more(deadline).await?;
2186 }
2187 }
2188
2189 pub async fn set_ble_pairing_pin(&mut self, pin: Option<u32>) -> Result<(), UlcpError> {
2194 if pin.is_some_and(|pin| pin > 999_999) {
2195 return Err(UlcpError::Protocol("BLE pairing PIN out of range"));
2196 }
2197 let tid = self.alloc_tid();
2198 let value = pin.map(u32::to_le_bytes);
2199 let mut buf = [0u8; 12];
2200 let len = frame::prop_set(
2201 &mut buf,
2202 tid,
2203 prop::BLE_PAIRING_PIN,
2204 value.as_ref().map_or(&[], |value| &value[..]),
2205 )
2206 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2207 self.send(&buf[..len]).await?;
2208 self.finish_prop_transaction(tid, prop::BLE_PAIRING_PIN, PropResponsePolicy::StatusOnly)
2209 .await
2210 .map(|_| ())
2211 }
2212
2213 pub async fn capabilities(&mut self) -> Result<Vec<u32>, UlcpError> {
2215 decode_capabilities(&self.get_prop(prop::CAPS).await?)
2216 }
2217
2218 pub async fn sync(
2228 &mut self,
2229 expected_host_key: Option<&[u8; 32]>,
2230 ) -> Result<DeviceSync, UlcpError> {
2231 let last_status = decode_status(&self.get_prop(prop::LAST_STATUS).await?);
2234 let capabilities = self.capabilities().await?;
2235 let has = |capability: u32| capabilities.contains(&capability);
2236 let (host_key, ownership) = if !self.prop_reachable(prop::HOST_KEY) {
2243 (None, HostOwnership::Unreachable)
2244 } else if has(cap::HOST_FILTER) {
2245 let value = self.get_prop(prop::HOST_KEY).await?;
2246 match <[u8; 32]>::try_from(value.as_slice()) {
2247 Ok(key) => {
2248 let ownership = match expected_host_key {
2249 Some(expected) if *expected == key => HostOwnership::Ours,
2250 _ => HostOwnership::OtherHost(key),
2251 };
2252 (Some(key), ownership)
2253 }
2254 Err(_) if value.is_empty() => (None, HostOwnership::Unclaimed),
2255 Err(_) => return Err(UlcpError::Protocol("malformed PROP_HOST_KEY")),
2256 }
2257 } else {
2258 (None, HostOwnership::Unsupported)
2259 };
2260
2261 let phy_enabled = self.get_prop(prop::PHY_ENABLED).await? == [1];
2264 let freq = self.get_prop(prop::PHY_FREQ).await?;
2265 let freq_khz = u32::from_le_bytes(
2266 freq.as_slice()
2267 .try_into()
2268 .map_err(|_| UlcpError::Protocol("malformed PROP_PHY_FREQ"))?,
2269 );
2270 let device_name = self.device_name().await?;
2271 let saved = match has(cap::SAVE) {
2272 true => Some(SavedSnapshot::from_octet(
2273 &self.get_prop(prop::SAVED).await?,
2274 )?),
2275 false => None,
2276 };
2277 let (queue_count, queue_dropped) =
2278 if has(cap::HOST_RX_QUEUE) && self.prop_reachable(prop::HOST_RX_QUEUE_COUNT) {
2279 let count = self.get_prop(prop::HOST_RX_QUEUE_COUNT).await?;
2280 let dropped = self.get_prop(prop::HOST_RX_QUEUE_DROPPED).await?;
2281 (
2282 Some(u16::from_le_bytes(count.as_slice().try_into().map_err(
2283 |_| UlcpError::Protocol("malformed PROP_HOST_RX_QUEUE_COUNT"),
2284 )?)),
2285 Some(u32::from_le_bytes(dropped.as_slice().try_into().map_err(
2286 |_| UlcpError::Protocol("malformed PROP_HOST_RX_QUEUE_DROPPED"),
2287 )?)),
2288 )
2289 } else {
2290 (None, None)
2291 };
2292 let filters = match has(cap::HOST_FILTER) && self.prop_reachable(prop::HOST_RX_FILTERS) {
2293 true => Some(decode_filter_table(
2294 &self.get_prop(prop::HOST_RX_FILTERS).await?,
2295 )?),
2296 false => None,
2297 };
2298 let (host_channel_ids, host_peer_keys) =
2299 if has(cap::HOST_KEYS) && self.prop_reachable(prop::HOST_CHANNEL_KEYS) {
2300 (
2301 Some(decode_fixed_list::<{ items::CHANNEL_ID_LEN }>(
2302 &self.get_prop(prop::HOST_CHANNEL_KEYS).await?,
2303 "malformed PROP_HOST_CHANNEL_KEYS digest",
2304 )?),
2305 Some(decode_fixed_list::<{ items::PUBLIC_KEY_LEN }>(
2306 &self.get_prop(prop::HOST_PEER_KEYS).await?,
2307 "malformed PROP_HOST_PEER_KEYS digest",
2308 )?),
2309 )
2310 } else {
2311 (None, None)
2312 };
2313 let auto_ack = match has(cap::HOST_AUTO_ACK) && self.prop_reachable(prop::HOST_AUTO_ACK) {
2314 true => Some(self.get_prop(prop::HOST_AUTO_ACK).await? == [1]),
2315 false => None,
2316 };
2317 let dev_key = if has(cap::DEV_IDENTITY) {
2318 let value = self.get_prop(prop::DEV_KEY).await?;
2319 match <[u8; 32]>::try_from(value.as_slice()) {
2320 Ok(key) => Some(key),
2321 Err(_) if value.is_empty() => None,
2322 Err(_) => return Err(UlcpError::Protocol("malformed PROP_DEV_KEY")),
2323 }
2324 } else {
2325 None
2326 };
2327
2328 Ok(DeviceSync {
2329 reset_since_last_contact: last_status.is_reset(),
2330 last_status,
2331 capabilities,
2332 ownership,
2333 host_key,
2334 phy_enabled,
2335 freq_khz,
2336 device_name,
2337 saved,
2338 queue_count,
2339 queue_dropped,
2340 filters,
2341 host_channel_ids,
2342 host_peer_keys,
2343 auto_ack,
2344 dev_key,
2345 })
2346 }
2347
2348 pub async fn provision(
2384 &mut self,
2385 desired: &HostProvisioning,
2386 ) -> Result<ProvisionReport, UlcpError> {
2387 self.require_tethered(prop::HOST_KEY)?;
2388 let mut report = ProvisionReport::default();
2389 let current_key = self.get_prop(prop::HOST_KEY).await?;
2390 if current_key.as_slice() != desired.host_key.as_slice() {
2394 report.host_replaced = true;
2395 }
2396 self.set_prop(prop::HOST_KEY, &desired.host_key).await?;
2397
2398 let mut table = Vec::new();
2401 for filter in &desired.filters {
2402 let mut item = [0u8; items::Filter::MAX_WIRE_LEN];
2403 let item_len = filter
2404 .encode(&mut item)
2405 .map_err(|_| UlcpError::Protocol("filter encode"))?;
2406 let mut prefixed = [0u8; items::Filter::MAX_WIRE_LEN + 2];
2407 let prefixed_len = items::encode_prefixed_item(&item[..item_len], &mut prefixed)
2408 .map_err(|_| UlcpError::Protocol("filter encode"))?;
2409 table.extend_from_slice(&prefixed[..prefixed_len]);
2410 }
2411 self.set_prop(prop::HOST_RX_FILTERS, &table).await?;
2412 report.filters_replaced = true;
2413
2414 let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
2419 let desired_ids: Vec<[u8; items::CHANNEL_ID_LEN]> = desired
2420 .channel_keys
2421 .iter()
2422 .map(|key| engine.derive_channel_id(&ChannelKey(*key)).0)
2423 .collect();
2424 let current_ids = if report.host_replaced {
2425 Vec::new()
2426 } else {
2427 decode_fixed_list::<{ items::CHANNEL_ID_LEN }>(
2428 &self.get_prop(prop::HOST_CHANNEL_KEYS).await?,
2429 "malformed PROP_HOST_CHANNEL_KEYS digest",
2430 )?
2431 };
2432 if current_ids.iter().any(|id| !desired_ids.contains(id)) {
2433 let table: Vec<u8> = desired.channel_keys.concat();
2434 self.set_prop(prop::HOST_CHANNEL_KEYS, &table).await?;
2435 report.channels_replaced = true;
2436 } else {
2437 for key in &desired.channel_keys {
2445 match self.insert_prop_item(prop::HOST_CHANNEL_KEYS, key).await {
2446 Ok(_) => report.channels_inserted += 1,
2447 Err(UlcpError::Status(Status::ALREADY)) => {}
2448 Err(error) => return Err(error),
2449 }
2450 }
2451 }
2452
2453 let current_peers = if report.host_replaced {
2458 Vec::new()
2459 } else {
2460 decode_fixed_list::<{ items::PUBLIC_KEY_LEN }>(
2461 &self.get_prop(prop::HOST_PEER_KEYS).await?,
2462 "malformed PROP_HOST_PEER_KEYS digest",
2463 )?
2464 };
2465 for existing in ¤t_peers {
2466 if !desired
2467 .peer_keys
2468 .iter()
2469 .any(|entry| entry.public_key == *existing)
2470 {
2471 self.remove_prop_item(prop::HOST_PEER_KEYS, existing)
2472 .await?;
2473 report.peers_removed += 1;
2474 }
2475 }
2476 for entry in &desired.peer_keys {
2477 let mut item = [0u8; items::PeerKeyEntry::WIRE_LEN];
2478 entry
2479 .encode(&mut item)
2480 .map_err(|_| UlcpError::Protocol("peer entry encode"))?;
2481 self.insert_prop_item(prop::HOST_PEER_KEYS, &item).await?;
2482 report.peers_inserted += 1;
2483 }
2484
2485 self.set_prop(prop::HOST_AUTO_ACK, &[desired.auto_ack as u8])
2487 .await?;
2488 report.auto_ack_changed = true;
2489 Ok(report)
2490 }
2491
2492 pub async fn ensure_device_identity(&mut self) -> Result<[u8; 32], UlcpError> {
2500 let current = self.get_prop(prop::DEV_KEY).await?;
2501 if let Ok(key) = <[u8; 32]>::try_from(current.as_slice()) {
2502 return Ok(key);
2503 }
2504 if !current.is_empty() {
2505 return Err(UlcpError::Protocol("malformed PROP_DEV_KEY"));
2506 }
2507 let tid = self.alloc_tid();
2511 let mut buf = [0u8; 8];
2512 let len = frame::prop_set(&mut buf, tid, prop::DEV_PRIVATE_KEY, &[])
2513 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2514 self.send(&buf[..len]).await?;
2515 let value = self
2516 .finish_prop_transaction(tid, prop::DEV_KEY, PropResponsePolicy::Value)
2517 .await?;
2518 <[u8; 32]>::try_from(value.as_slice())
2519 .map_err(|_| UlcpError::Protocol("malformed PROP_DEV_KEY"))
2520 }
2521
2522 async fn finish_prop_transaction(
2523 &mut self,
2524 tid: u8,
2525 key: u32,
2526 policy: PropResponsePolicy,
2527 ) -> Result<Vec<u8>, UlcpError> {
2528 let deadline = Instant::now() + self.config.response_timeout;
2529 let response = self.wait_response(tid, deadline).await?;
2530 if response.kind != ResponseKind::Is {
2531 return Err(UlcpError::Protocol(
2532 "table notification answering a property command",
2533 ));
2534 }
2535 match (policy, response.key) {
2536 (PropResponsePolicy::Value, response_key) if response_key == key => Ok(response.value),
2537 (PropResponsePolicy::StatusOnly, prop::LAST_STATUS) => {
2538 let status = decode_status(&response.value);
2539 if status == Status::OK {
2540 Ok(Vec::new())
2541 } else {
2542 Err(UlcpError::Status(status))
2543 }
2544 }
2545 (PropResponsePolicy::Value, prop::LAST_STATUS) => {
2546 let status = decode_status(&response.value);
2547 if status == Status::OK {
2548 Err(UlcpError::Protocol(
2549 "unexpected status-only property response",
2550 ))
2551 } else {
2552 Err(UlcpError::Status(status))
2553 }
2554 }
2555 _ => Err(UlcpError::Protocol("response for unexpected property")),
2556 }
2557 }
2558
2559 async fn finish_table_transaction(
2563 &mut self,
2564 tid: u8,
2565 key: u32,
2566 expected: ResponseKind,
2567 ) -> Result<Vec<u8>, UlcpError> {
2568 let deadline = Instant::now() + self.config.response_timeout;
2569 let response = self.wait_response(tid, deadline).await?;
2570 match (response.kind, response.key) {
2571 (kind, response_key) if kind == expected && response_key == key => Ok(response.value),
2572 (ResponseKind::Is, prop::LAST_STATUS) => {
2573 let status = decode_status(&response.value);
2574 if status == Status::OK {
2575 Err(UlcpError::Protocol(
2576 "status-only success for a table mutation",
2577 ))
2578 } else {
2579 Err(UlcpError::Status(status))
2580 }
2581 }
2582 _ => Err(UlcpError::Protocol("response for unexpected property")),
2583 }
2584 }
2585
2586 fn alloc_tid(&mut self) -> u8 {
2587 self.tids.allocate()
2588 }
2589
2590 fn ingest_frame(&mut self, frame_bytes: &[u8]) -> bool {
2595 if let Some(trace) = &mut self.trace {
2596 trace(TraceDirection::DeviceToHost, &describe_frame(frame_bytes));
2597 }
2598 let Ok(frame) = Frame::parse(frame_bytes) else {
2599 return false;
2600 };
2601 match frame.command() {
2602 Some(Cmd::StrRecv) => {
2603 let Ok(payload) = StreamPayload::parse(frame.payload) else {
2604 return false;
2605 };
2606 if payload.stream != stream::PHY_RAW {
2607 return false;
2608 }
2609 let meta = RxMeta::decode(payload.metadata).unwrap_or_default();
2610 if self.rx_queue.len() >= RX_QUEUE_DEPTH {
2611 self.rx_queue.pop_front();
2612 }
2613 self.rx_queue.push_back(RxPacket {
2614 data: payload.data.to_vec(),
2615 meta,
2616 raw_meta: payload.metadata.to_vec(),
2617 });
2618 return true;
2619 }
2620 Some(Cmd::PropIs) => self.ingest_prop_notification(ResponseKind::Is, &frame),
2621 Some(Cmd::PropInserted) => {
2622 self.ingest_prop_notification(ResponseKind::Inserted, &frame)
2623 }
2624 Some(Cmd::PropRemoved) => self.ingest_prop_notification(ResponseKind::Removed, &frame),
2625 Some(Cmd::PropAre) => {
2630 let tid = frame.header.tid();
2631 if tid != TID_UNSOLICITED {
2632 if self.responses.len() >= RESPONSE_QUEUE_DEPTH {
2633 self.responses.pop_front();
2634 }
2635 self.responses.push_back(Response {
2636 tid,
2637 kind: ResponseKind::Are,
2638 key: prop::LAST_STATUS,
2639 value: frame.payload.to_vec(),
2640 });
2641 }
2642 }
2643 _ => {}
2644 }
2645 false
2646 }
2647
2648 fn ingest_prop_notification(&mut self, kind: ResponseKind, frame: &Frame<'_>) {
2649 let Ok(notification) = PropertyNotification::from_frame(frame) else {
2650 return;
2651 };
2652 if notification.kind != kind {
2655 return;
2656 }
2657 let tid = notification.tid;
2658 if tid != TID_UNSOLICITED {
2659 if self.responses.len() >= RESPONSE_QUEUE_DEPTH {
2660 self.responses.pop_front();
2661 }
2662 self.responses.push_back(Response {
2663 tid,
2664 kind,
2665 key: notification.key,
2666 value: notification.value.to_vec(),
2667 });
2668 return;
2669 }
2670 if kind == ResponseKind::Is && notification.key == prop::LAST_STATUS {
2673 let status = decode_status(notification.value);
2674 if status.is_reset() {
2675 self.seen_reset = Some(status);
2676 }
2677 return;
2678 }
2679 let event = match kind {
2680 ResponseKind::Is => PropEvent::Is {
2681 key: notification.key,
2682 value: notification.value.to_vec(),
2683 },
2684 ResponseKind::Inserted => PropEvent::Inserted {
2685 key: notification.key,
2686 digest: notification.value.to_vec(),
2687 },
2688 ResponseKind::Removed => PropEvent::Removed {
2689 key: notification.key,
2690 digest: notification.value.to_vec(),
2691 },
2692 ResponseKind::Are => return,
2696 };
2697 if self.prop_events.len() >= PROP_EVENT_DEPTH {
2698 self.prop_events.pop_front();
2699 }
2700 self.prop_events.push_back(event);
2701 }
2702
2703 pub fn pop_prop_event(&mut self) -> Option<PropEvent> {
2708 self.prop_events.pop_front()
2709 }
2710
2711 async fn wait_response(&mut self, tid: u8, deadline: Instant) -> Result<Response, UlcpError> {
2715 loop {
2716 while let Some(response) = self.responses.pop_front() {
2721 if response.tid == tid {
2722 return Ok(response);
2723 }
2724 }
2727 if let Some(status) = self.seen_reset.take() {
2728 return Err(UlcpError::UnexpectedReset(status));
2729 }
2730 self.read_more(deadline).await?;
2731 }
2732 }
2733
2734 async fn wait_reset(&mut self, deadline: Instant) -> Result<Status, UlcpError> {
2736 loop {
2737 if let Some(status) = self.seen_reset.take() {
2738 return Ok(status);
2739 }
2740 while let Some(response) = self.responses.pop_front() {
2742 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
2743 let status = decode_status(&response.value);
2744 if status.is_reset() {
2745 return Ok(status);
2746 }
2747 }
2748 }
2749 self.read_more(deadline).await?;
2750 }
2751 }
2752
2753 async fn read_more(&mut self, deadline: Instant) -> Result<bool, UlcpError> {
2756 let now = Instant::now();
2757 if now >= deadline {
2758 return Err(UlcpError::Timeout);
2759 }
2760 let frame = match tokio::time::timeout(deadline - now, self.link.recv_frame()).await {
2761 Err(_elapsed) => return Err(UlcpError::Timeout),
2762 Ok(Err(error)) => return Err(error),
2763 Ok(Ok(frame)) => frame,
2764 };
2765 Ok(self.ingest_frame(&frame))
2766 }
2767
2768 fn pop_rx(&mut self, buf: &mut [u8]) -> Option<RxInfo> {
2769 let packet = self.rx_queue.pop_front()?;
2770 let len = packet.data.len().min(buf.len());
2771 buf[..len].copy_from_slice(&packet.data[..len]);
2772 let self_tx = BufferedRxMeta::decode(&packet.raw_meta)
2781 .is_ok_and(|meta| meta.flags & RX_FLAG_SELF_TX != 0);
2782 Some(RxInfo {
2783 len,
2784 rssi: packet.meta.rssi_dbm.unwrap_or(0),
2785 snr: Snr::from_centibels(packet.meta.snr_cb.unwrap_or(0)),
2786 lqi: packet.meta.lqi,
2787 origin: if self_tx {
2788 RxOrigin::Backhaul
2789 } else {
2790 RxOrigin::Air
2791 },
2792 })
2793 }
2794
2795 async fn send_confirmed(
2799 &mut self,
2800 data: &[u8],
2801 metadata: &[u8],
2802 cca_deadline: Option<Instant>,
2803 ) -> Result<(), TxError<UlcpError>> {
2804 loop {
2805 let tid = self.alloc_tid();
2806 let mut frame_buf = vec![0u8; data.len() + metadata.len() + 16];
2807 let frame_len = frame::str_send(&mut frame_buf, tid, stream::PHY_RAW, data, metadata)
2808 .map_err(|_| TxError::Io(UlcpError::Protocol("frame encode")))?;
2809 self.send(&frame_buf[..frame_len])
2810 .await
2811 .map_err(TxError::Io)?;
2812
2813 let deadline = Instant::now()
2816 + self.config.response_timeout
2817 + Duration::from_millis(u64::from(self.t_frame_ms) * 2);
2818 let response = self
2819 .wait_response(tid, deadline)
2820 .await
2821 .map_err(TxError::Io)?;
2822 if response.kind != ResponseKind::Is || response.key != prop::LAST_STATUS {
2823 return Err(TxError::Io(UlcpError::Protocol(
2824 "unexpected transmit response",
2825 )));
2826 }
2827 match decode_status(&response.value) {
2828 Status::OK => return Ok(()),
2829 Status::CCA_FAILURE => match cca_deadline {
2830 Some(deadline) if Instant::now() < deadline => {
2831 tokio::time::sleep(CCA_RETRY_DELAY).await;
2832 }
2833 _ => return Err(TxError::CadTimeout),
2834 },
2835 status => return Err(TxError::Io(UlcpError::Status(status))),
2836 }
2837 }
2838 }
2839
2840 pub async fn transmit_raw_with_meta(
2854 &mut self,
2855 data: &[u8],
2856 metadata: &[u8],
2857 ) -> Result<(), TxError<UlcpError>> {
2858 if data.len() > self.max_frame_size {
2859 return Err(TxError::Io(UlcpError::FrameTooLarge(data.len())));
2860 }
2861 let skips_cca = metadata
2862 .get(1)
2863 .is_some_and(|flags| flags & TX_FLAG_NOCCA != 0);
2864 let cca_deadline = (!skips_cca).then(Instant::now);
2865 self.send_confirmed(data, metadata, cca_deadline).await
2866 }
2867
2868 pub fn poll_receive_raw(
2876 &mut self,
2877 cx: &mut core::task::Context<'_>,
2878 ) -> core::task::Poll<Result<RawRxFrame, UlcpError>> {
2879 loop {
2880 if let Some(status) = self.seen_reset.take() {
2881 return core::task::Poll::Ready(Err(UlcpError::UnexpectedReset(status)));
2882 }
2883 if let Some(packet) = self.rx_queue.pop_front() {
2884 return core::task::Poll::Ready(Ok(RawRxFrame {
2885 data: packet.data,
2886 metadata: packet.raw_meta,
2887 }));
2888 }
2889
2890 match self.link.poll_recv_frame(cx) {
2891 core::task::Poll::Ready(Ok(frame)) => {
2892 self.ingest_frame(&frame);
2893 }
2894 core::task::Poll::Ready(Err(error)) => return core::task::Poll::Ready(Err(error)),
2895 core::task::Poll::Pending => return core::task::Poll::Pending,
2896 }
2897 }
2898 }
2899
2900 pub async fn receive_raw(&mut self) -> Result<RawRxFrame, UlcpError> {
2905 core::future::poll_fn(|cx| self.poll_receive_raw(cx)).await
2906 }
2907}
2908
2909#[derive(Clone, Debug)]
2912pub struct RawRxFrame {
2913 pub data: Vec<u8>,
2914 pub metadata: Vec<u8>,
2915}
2916
2917#[cfg(feature = "serial-radio")]
2918impl UlcpDevice<SerialFrameLink<tokio_serial::SerialStream>> {
2919 pub async fn open_serial(
2921 path: impl AsRef<str>,
2922 baud_rate: u32,
2923 config: UlcpDeviceConfig,
2924 ) -> Result<Self, UlcpError> {
2925 use tokio_serial::SerialPortBuilderExt;
2926
2927 let stream = tokio_serial::new(path.as_ref(), baud_rate)
2928 .open_native_async()
2929 .map_err(|error| UlcpError::Io(error.into()))?;
2930 Self::new(SerialFrameLink::new(stream), config).await
2931 }
2932}
2933
2934#[cfg(feature = "ble-radio")]
2935impl UlcpDevice<BleFrameLink> {
2936 pub async fn open_ble(
2938 selector: Option<&str>,
2939 config: UlcpDeviceConfig,
2940 ) -> Result<Self, UlcpError> {
2941 Self::open_ble_with_link_config(selector, config, BleFrameLinkConfig::default()).await
2942 }
2943
2944 pub async fn open_ble_with_link_config(
2946 selector: Option<&str>,
2947 config: UlcpDeviceConfig,
2948 link_config: BleFrameLinkConfig,
2949 ) -> Result<Self, UlcpError> {
2950 let link = BleFrameLink::connect(selector, link_config).await?;
2951 Self::new(link, config).await
2952 }
2953}
2954
2955impl<L> Radio for UlcpDevice<L>
2956where
2957 L: FrameLink,
2958{
2959 type Error = UlcpError;
2960
2961 async fn transmit(
2975 &mut self,
2976 data: &[u8],
2977 options: TxOptions,
2978 ) -> Result<(), TxError<Self::Error>> {
2979 if data.len() > self.max_frame_size {
2980 return Err(TxError::Io(UlcpError::FrameTooLarge(data.len())));
2981 }
2982
2983 let mut meta = TxMeta::default();
2986 let cca_deadline = match options.cad {
2987 CadPolicy::Skip => {
2988 meta.flags |= TX_FLAG_NOCCA;
2989 None
2990 }
2991 CadPolicy::Gate => Some(Instant::now()),
2994 CadPolicy::RetryFor { timeout_ms } => {
2995 Some(Instant::now() + Duration::from_millis(timeout_ms.into()))
2996 }
2997 };
2998 let mut meta_buf = [0u8; TxMeta::WIRE_LEN];
2999 let meta_len = meta
3000 .encode(&mut meta_buf)
3001 .expect("buffer sized with WIRE_LEN");
3002
3003 self.send_confirmed(data, &meta_buf[..meta_len], cca_deadline)
3004 .await
3005 }
3006
3007 fn poll_receive(
3008 &mut self,
3009 cx: &mut core::task::Context<'_>,
3010 buf: &mut [u8],
3011 ) -> core::task::Poll<Result<RxInfo, Self::Error>> {
3012 loop {
3013 if let Some(status) = self.seen_reset.take() {
3014 return core::task::Poll::Ready(Err(UlcpError::UnexpectedReset(status)));
3015 }
3016 if let Some(info) = self.pop_rx(buf) {
3017 return core::task::Poll::Ready(Ok(info));
3018 }
3019
3020 match self.link.poll_recv_frame(cx) {
3021 core::task::Poll::Ready(Ok(frame)) => {
3022 self.ingest_frame(&frame);
3023 }
3024 core::task::Poll::Ready(Err(error)) => return core::task::Poll::Ready(Err(error)),
3025 core::task::Poll::Pending => return core::task::Poll::Pending,
3026 }
3027 }
3028 }
3029
3030 fn max_frame_size(&self) -> usize {
3031 self.max_frame_size
3032 }
3033
3034 fn t_frame_ms(&self) -> u32 {
3035 self.t_frame_ms
3036 }
3037}
3038
3039pub fn decode_status(value: &[u8]) -> Status {
3051 match pui::decode(value) {
3052 Ok((code, _)) => Status(code),
3053 Err(_) => Status::FAILURE,
3054 }
3055}
3056
3057pub fn decode_capabilities(value: &[u8]) -> Result<Vec<u32>, UlcpError> {
3059 let mut caps = Vec::new();
3060 let mut offset = 0;
3061 while offset < value.len() {
3062 let (code, used) = pui::decode(&value[offset..])
3063 .map_err(|_| UlcpError::Protocol("malformed PROP_CAPS"))?;
3064 caps.push(code);
3065 offset += used;
3066 }
3067 Ok(caps)
3068}
3069
3070pub fn decode_filter_table(value: &[u8]) -> Result<Vec<items::Filter>, UlcpError> {
3073 let mut filters = Vec::new();
3074 for item in items::prefixed_items(value) {
3075 let item = item.map_err(|_| UlcpError::Protocol("malformed PROP_HOST_RX_FILTERS"))?;
3076 filters.push(
3077 items::Filter::decode(item)
3078 .map_err(|_| UlcpError::Protocol("malformed PROP_HOST_RX_FILTERS"))?,
3079 );
3080 }
3081 Ok(filters)
3082}
3083
3084pub fn decode_region_list(value: &[u8]) -> Result<Vec<String>, UlcpError> {
3087 let mut regions = Vec::new();
3088 for item in items::prefixed_items(value) {
3089 let item = item.map_err(|_| UlcpError::Protocol("malformed PROP_MAC_REPEATER_REGIONS"))?;
3090 let text = core::str::from_utf8(item)
3091 .map_err(|_| UlcpError::Protocol("malformed PROP_MAC_REPEATER_REGIONS"))?;
3092 regions.push(text.to_owned());
3093 }
3094 Ok(regions)
3095}
3096
3097fn check_region(region: &str) -> Result<(), UlcpError> {
3101 match (1..=umsh_core::REGION_NAME_MAX_LEN).contains(®ion.len()) {
3102 true => Ok(()),
3103 false => Err(UlcpError::Protocol(
3104 "a region string is 1 to 24 octets of UTF-8",
3105 )),
3106 }
3107}
3108
3109pub fn decode_region_code(value: &[u8]) -> Result<Option<RegionCode>, UlcpError> {
3111 match value {
3112 [] => Ok(None),
3113 [high, low] => Ok(Some(RegionCode::from_bytes([*high, *low]))),
3114 _ => Err(UlcpError::Protocol(
3115 "malformed PROP_MAC_REPEATER_DEFAULT_REGION",
3116 )),
3117 }
3118}
3119
3120pub fn decode_opt_i16(value: &[u8]) -> Option<Option<i16>> {
3122 match value {
3123 [] => Some(None),
3124 [low, high] => Some(Some(i16::from_le_bytes([*low, *high]))),
3125 _ => None,
3126 }
3127}
3128
3129pub fn decode_opt_i8(value: &[u8]) -> Option<Option<i8>> {
3131 match value {
3132 [] => Some(None),
3133 [byte] => Some(Some(*byte as i8)),
3134 _ => None,
3135 }
3136}
3137
3138pub fn decode_alert(value: &[u8]) -> Result<AlertState, UlcpError> {
3140 const MALFORMED: &str = "malformed PROP_ALERT";
3141 let (code, consumed) = pui::decode(value).map_err(|_| UlcpError::Protocol(MALFORMED))?;
3142 if consumed != value.len() {
3143 return Err(UlcpError::Protocol(MALFORMED));
3144 }
3145 AlertState::from_code(code).ok_or(UlcpError::Protocol(MALFORMED))
3146}
3147
3148fn decode_epoch(value: &[u8]) -> Result<Option<u32>, UlcpError> {
3151 match value {
3152 [] => Ok(None),
3153 [a, b, c, d] => Ok(Some(u32::from_le_bytes([*a, *b, *c, *d]))),
3154 _ => Err(UlcpError::Protocol("malformed PROP_TIME")),
3155 }
3156}
3157
3158fn decode_tz_offset(value: &[u8]) -> Result<i16, UlcpError> {
3160 match value {
3161 [low, high] => Ok(i16::from_le_bytes([*low, *high])),
3162 _ => Err(UlcpError::Protocol("malformed PROP_TZ_OFFSET")),
3163 }
3164}
3165
3166fn decode_bool(value: &[u8], what: &'static str) -> Result<bool, UlcpError> {
3169 match value {
3170 [0] => Ok(false),
3171 [1] => Ok(true),
3172 _ => Err(UlcpError::Protocol(what)),
3173 }
3174}
3175
3176fn decode_interval(value: &[u8]) -> Result<u32, UlcpError> {
3178 match value {
3179 [a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
3180 _ => Err(UlcpError::Protocol("malformed announcement interval")),
3181 }
3182}
3183
3184fn decode_fixed_list<const N: usize>(
3186 value: &[u8],
3187 what: &'static str,
3188) -> Result<Vec<[u8; N]>, UlcpError> {
3189 items::fixed_items::<N>(value)
3190 .map(|iterator| iterator.copied().collect())
3191 .map_err(|_| UlcpError::Protocol(what))
3192}
3193
3194pub fn describe_frame(bytes: &[u8]) -> String {
3199 umsh_ulcp::FrameDescription(bytes).to_string()
3200}
3201
3202#[cfg(test)]
3203mod tests {
3204 use super::*;
3205 use std::collections::HashMap;
3206 use tokio::io::{AsyncReadExt, DuplexStream};
3207 use umsh_ulcp::PropPayload;
3208 use umsh_ulcp::meta::RX_FLAG_BUFFERED;
3209
3210 const CCA_FAIL: &[u8] = b"cca-fail";
3212 const RESET_AFTER: &[u8] = b"reset-after";
3215 const RESTORE_RESET_FORM_KEY: u32 = 59_999;
3218
3219 async fn fake_device<IO: AsyncRead + AsyncWrite + Unpin>(mut io: IO) {
3227 let mut decoder = hdlc::Decoder::<WIRE_BUF>::new();
3228 let mut props: HashMap<u32, Vec<u8>> = HashMap::new();
3229 let mut tables: HashMap<u32, Vec<Vec<u8>>> = HashMap::new();
3230 let mut chunk = [0u8; READ_CHUNK];
3231 loop {
3232 let read = match io.read(&mut chunk).await {
3233 Ok(0) | Err(_) => return,
3234 Ok(read) => read,
3235 };
3236 let mut replies: Vec<Vec<u8>> = Vec::new();
3237 for &byte in &chunk[..read] {
3238 let Some(Ok(frame_bytes)) = decoder.push(byte) else {
3239 continue;
3240 };
3241 let frame = Frame::parse(frame_bytes).expect("host sent malformed frame");
3242 let tid = frame.header.tid();
3243 let mut buf = vec![0u8; 512];
3244 match frame.command().expect("host sent unknown command") {
3245 Cmd::Reset => {
3246 let len =
3247 frame::last_status(&mut buf, TID_UNSOLICITED, Status::RESET_SOFTWARE)
3248 .unwrap();
3249 replies.push(buf[..len].to_vec());
3250 }
3251 Cmd::PropGet => {
3252 let key = PropPayload::parse(frame.payload).unwrap().key;
3253 let value: Vec<u8> = match key {
3254 prop::LAST_STATUS => vec![Status::RESET_POWER_ON.0 as u8],
3255 prop::PROTOCOL_VERSION => {
3256 vec![ids::PROTOCOL_MAJOR_VERSION, ids::PROTOCOL_MINOR_VERSION]
3257 }
3258 prop::DEV_VERSION => b"fake-dev/0.1\0".to_vec(),
3259 prop::DEV_MODEL => b"Fake Board\0".to_vec(),
3260 prop::PHY_MTU => 255u16.to_le_bytes().to_vec(),
3261 _ => props.get(&key).cloned().unwrap_or_default(),
3262 };
3263 let len = frame::prop_is(&mut buf, tid, key, &value).unwrap();
3264 replies.push(buf[..len].to_vec());
3265 }
3266 Cmd::PropSet => {
3267 let payload = PropPayload::parse(frame.payload).unwrap();
3268 props.insert(payload.key, payload.value.to_vec());
3269 let len = if payload.key == prop::BLE_PAIRING_PIN {
3270 frame::last_status(&mut buf, tid, Status::OK).unwrap()
3271 } else {
3272 frame::prop_is(&mut buf, tid, payload.key, payload.value).unwrap()
3273 };
3274 replies.push(buf[..len].to_vec());
3275 }
3276 Cmd::StrSend => {
3277 let payload = StreamPayload::parse(frame.payload).unwrap();
3278 assert_eq!(payload.stream, stream::PHY_RAW);
3279 if payload.data == CCA_FAIL {
3280 let len =
3281 frame::last_status(&mut buf, tid, Status::CCA_FAILURE).unwrap();
3282 replies.push(buf[..len].to_vec());
3283 continue;
3284 }
3285 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
3286 replies.push(buf[..len].to_vec());
3287 if payload.data == RESET_AFTER {
3288 let len = frame::last_status(
3289 &mut buf,
3290 TID_UNSOLICITED,
3291 Status::RESET_WATCHDOG,
3292 )
3293 .unwrap();
3294 replies.push(buf[..len].to_vec());
3295 continue;
3296 }
3297 let mut meta = [0u8; RxMeta::WIRE_LEN];
3299 RxMeta {
3300 rssi_dbm: Some(-91),
3301 lqi: None,
3302 snr_cb: Some(55),
3303 }
3304 .encode(&mut meta)
3305 .unwrap();
3306 let len = frame::str_recv(&mut buf, stream::PHY_RAW, payload.data, &meta)
3307 .unwrap();
3308 replies.push(buf[..len].to_vec());
3309 }
3310 Cmd::Nop => {
3311 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
3312 replies.push(buf[..len].to_vec());
3313 }
3314 Cmd::PropInsert => {
3315 let payload = PropPayload::parse(frame.payload).unwrap();
3316 let replaces = payload.key == prop::HOST_PEER_KEYS;
3321 let stored = payload.value.to_vec();
3322 let digest_len = if replaces {
3323 assert_eq!(stored.len(), 64);
3324 32
3325 } else {
3326 stored.len()
3327 };
3328 let table = tables.entry(payload.key).or_default();
3329 let existing = table.iter_mut().find(|item| {
3330 item[..digest_len.min(item.len())] == stored[..digest_len]
3331 });
3332 let len = match existing {
3333 Some(_) if !replaces => {
3334 frame::last_status(&mut buf, tid, Status::ALREADY).unwrap()
3335 }
3336 Some(existing) => {
3337 *existing = stored.clone();
3338 frame::prop_inserted(
3339 &mut buf,
3340 tid,
3341 payload.key,
3342 &stored[..digest_len],
3343 )
3344 .unwrap()
3345 }
3346 None => {
3347 table.push(stored.clone());
3348 frame::prop_inserted(
3349 &mut buf,
3350 tid,
3351 payload.key,
3352 &stored[..digest_len],
3353 )
3354 .unwrap()
3355 }
3356 };
3357 replies.push(buf[..len].to_vec());
3358 }
3359 Cmd::PropRemove => {
3360 let payload = PropPayload::parse(frame.payload).unwrap();
3361 let table = tables.entry(payload.key).or_default();
3362 let position = table.iter().position(|item| {
3363 item[..payload.value.len().min(item.len())] == *payload.value
3364 });
3365 let len = match position {
3366 Some(index) => {
3367 let removed = table.remove(index);
3368 let digest = &removed[..payload.value.len().min(removed.len())];
3369 frame::prop_removed(&mut buf, tid, payload.key, digest).unwrap()
3370 }
3371 None => {
3372 frame::last_status(&mut buf, tid, Status::ITEM_NOT_FOUND).unwrap()
3373 }
3374 };
3375 replies.push(buf[..len].to_vec());
3376 }
3377 Cmd::QueueDrain => {
3378 for (index, age_s) in [5u32, 3].into_iter().enumerate() {
3380 let mut meta = [0u8; BufferedRxMeta::WIRE_LEN];
3381 BufferedRxMeta {
3382 rx: RxMeta {
3383 rssi_dbm: Some(-80),
3384 lqi: None,
3385 snr_cb: Some(10),
3386 },
3387 flags: RX_FLAG_BUFFERED,
3388 age_s,
3389 }
3390 .encode(&mut meta)
3391 .unwrap();
3392 let data = [0xB0u8 + index as u8];
3393 let len =
3394 frame::str_recv(&mut buf, stream::PHY_RAW, &data, &meta).unwrap();
3395 replies.push(buf[..len].to_vec());
3396 }
3397 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
3398 replies.push(buf[..len].to_vec());
3399 }
3400 Cmd::Save | Cmd::Clear | Cmd::FactoryReset => {
3404 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
3405 replies.push(buf[..len].to_vec());
3406 }
3407 Cmd::Restore => {
3408 if props
3409 .get(&RESTORE_RESET_FORM_KEY)
3410 .is_some_and(|value| value == &[1])
3411 {
3412 let len = frame::last_status(
3413 &mut buf,
3414 TID_UNSOLICITED,
3415 Status::RESET_RESTORED,
3416 )
3417 .unwrap();
3418 replies.push(buf[..len].to_vec());
3419 } else {
3420 let len = frame::prop_is(
3423 &mut buf,
3424 TID_UNSOLICITED,
3425 prop::PHY_FREQ,
3426 &905_000u32.to_le_bytes(),
3427 )
3428 .unwrap();
3429 replies.push(buf[..len].to_vec());
3430 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
3431 replies.push(buf[..len].to_vec());
3432 }
3433 }
3434 Cmd::PropMultiGet | Cmd::PropMultiSet | Cmd::Reboot | Cmd::BleClearBonds => {
3439 let len = frame::last_status(&mut buf, tid, Status::UNIMPLEMENTED).unwrap();
3440 replies.push(buf[..len].to_vec());
3441 }
3442 Cmd::PropIs
3443 | Cmd::StrRecv
3444 | Cmd::PropInserted
3445 | Cmd::PropRemoved
3446 | Cmd::PropAre => {
3447 panic!("host sent a device-only command")
3448 }
3449 }
3450 }
3451 for reply in replies {
3452 let mut wire = vec![0u8; hdlc::max_encoded_len(reply.len())];
3453 let len = hdlc::encode_frame(&reply, &mut wire).unwrap();
3454 if io.write_all(&wire[..len]).await.is_err() {
3455 return;
3456 }
3457 }
3458 }
3459 }
3460
3461 fn test_config() -> UlcpDeviceConfig {
3462 let mut config = UlcpDeviceConfig::new(906_875, 250_000, 11, 5);
3463 config.tx_power_dbm = 10;
3464 config.response_timeout = Duration::from_millis(500);
3465 config
3466 }
3467
3468 async fn attached_radio() -> UlcpDevice<SerialFrameLink<DuplexStream>> {
3469 let (client, server) = tokio::io::duplex(4096);
3470 tokio::spawn(fake_device(server));
3471 UlcpDevice::new(SerialFrameLink::new(client), test_config())
3472 .await
3473 .unwrap()
3474 }
3475
3476 fn wire(frame: &[u8]) -> Vec<u8> {
3477 let mut encoded = vec![0; hdlc::max_encoded_len(frame.len())];
3478 let len = hdlc::encode_frame(frame, &mut encoded).unwrap();
3479 encoded.truncate(len);
3480 encoded
3481 }
3482
3483 #[tokio::test]
3484 async fn serial_link_preserves_two_frames_from_one_read() {
3485 let (client, mut server) = tokio::io::duplex(1024);
3486 let mut bytes = wire(b"first");
3487 bytes.extend_from_slice(&wire(b"second"));
3488 server.write_all(&bytes).await.unwrap();
3489
3490 let mut link = SerialFrameLink::new(client);
3491 assert_eq!(link.recv_frame().await.unwrap(), b"first");
3492 assert_eq!(link.recv_frame().await.unwrap(), b"second");
3493 }
3494
3495 #[tokio::test]
3496 async fn serial_link_cancellation_keeps_partial_and_buffered_tail() {
3497 let (client, mut server) = tokio::io::duplex(1024);
3498 let first = wire(b"first");
3499 let second = wire(b"second");
3500 let split = second.len() / 2;
3501 let mut initial = first;
3502 initial.extend_from_slice(&second[..split]);
3503 server.write_all(&initial).await.unwrap();
3504
3505 let mut link = SerialFrameLink::new(client);
3506 assert_eq!(link.recv_frame().await.unwrap(), b"first");
3507 assert!(
3508 tokio::time::timeout(Duration::from_millis(1), link.recv_frame())
3509 .await
3510 .is_err()
3511 );
3512 server.write_all(&second[split..]).await.unwrap();
3513 assert_eq!(link.recv_frame().await.unwrap(), b"second");
3514 }
3515
3516 #[tokio::test]
3522 async fn serial_link_frames_the_same_bytes_over_a_socket() {
3523 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3524 let endpoint = listener.local_addr().unwrap();
3525 let accepted = tokio::spawn(async move { listener.accept().await.unwrap().0 });
3526 let client = tokio::net::TcpStream::connect(endpoint).await.unwrap();
3527
3528 let mut device = SerialFrameLink::new(accepted.await.unwrap());
3529 let mut host = SerialFrameLink::new(client);
3530
3531 let hostile = [0x7E, 0x7D, 0x11, 0x13, 0x00, 0xFF];
3532 host.send_frame(&hostile).await.unwrap();
3533 assert_eq!(device.recv_frame().await.unwrap(), hostile);
3534
3535 device.send_frame(b"pong").await.unwrap();
3536 assert_eq!(host.recv_frame().await.unwrap(), b"pong");
3537 }
3538
3539 #[tokio::test]
3542 async fn a_radio_attaches_over_a_socket() {
3543 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3544 let endpoint = listener.local_addr().unwrap();
3545 tokio::spawn(async move {
3546 let (stream, _) = listener.accept().await.unwrap();
3547 fake_device(stream).await;
3548 });
3549
3550 let stream = tokio::net::TcpStream::connect(endpoint).await.unwrap();
3551 stream.set_nodelay(true).unwrap();
3552 let mut device = UlcpDevice::new(SerialFrameLink::new(stream), test_config())
3553 .await
3554 .unwrap();
3555
3556 device.set_prop(prop::PHY_TX_POWER, &[14]).await.unwrap();
3559 assert_eq!(device.get_prop(prop::PHY_TX_POWER).await.unwrap(), [14]);
3560 }
3561
3562 #[tokio::test]
3566 async fn serial_link_reports_a_closed_socket_as_a_lost_link() {
3567 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
3568 let endpoint = listener.local_addr().unwrap();
3569 let accepted = tokio::spawn(async move { listener.accept().await.unwrap().0 });
3570 let client = tokio::net::TcpStream::connect(endpoint).await.unwrap();
3571
3572 drop(accepted.await.unwrap());
3573 let mut host = SerialFrameLink::new(client);
3574 assert!(matches!(
3575 host.recv_frame().await,
3576 Err(UlcpError::Disconnected)
3577 ));
3578 }
3579
3580 #[cfg(feature = "ble-radio")]
3581 #[test]
3582 fn ble_link_config_rejects_invalid_values_without_opening_an_adapter() {
3583 let mut config = BleFrameLinkConfig::default();
3584 assert!(config.validate().is_ok());
3585 config.segment_payload = 0;
3586 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3587 config.segment_payload = 512;
3588 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3589 config.segment_payload = 19;
3590 config.operation_timeout = Duration::ZERO;
3591 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3592 config.operation_timeout = Duration::from_secs(1);
3593 config.pairing_timeout = Duration::ZERO;
3594 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3595 }
3596
3597 #[cfg(feature = "ble-radio")]
3598 #[tokio::test]
3599 async fn ble_notification_receiver_reassembles_and_recovers_from_malformed_segment() {
3600 let (tx, rx) = tokio::sync::mpsc::channel(8);
3601 let mut receiver = BleNotificationReceiver::new(rx);
3602
3603 tx.send(vec![0x01, 0xff]).await.unwrap();
3606 let frame = b"a frame larger than one tiny GATT segment";
3607 for segment in umsh_ulcp::gatt::segments(frame, 7) {
3608 let mut value = vec![0; segment.payload().len() + 1];
3609 segment.write_to(&mut value).unwrap();
3610 tx.send(value).await.unwrap();
3611 }
3612
3613 let received = core::future::poll_fn(|cx| receiver.poll_recv_frame(cx))
3614 .await
3615 .unwrap();
3616 assert_eq!(received, frame);
3617 }
3618
3619 #[cfg(feature = "ble-radio")]
3620 #[tokio::test]
3621 async fn ble_notification_channel_close_surfaces_disconnect() {
3622 let (tx, rx) = tokio::sync::mpsc::channel(1);
3623 let mut receiver = BleNotificationReceiver::new(rx);
3624 drop(tx);
3625 let result = core::future::poll_fn(|cx| receiver.poll_recv_frame(cx)).await;
3626 assert!(matches!(result, Err(UlcpError::Disconnected)));
3627 }
3628
3629 #[tokio::test]
3630 async fn initialization_handshake() {
3631 let radio = attached_radio().await;
3632 assert_eq!(radio.max_frame_size(), 255);
3633 assert_eq!(radio.dev_version(), "fake-dev/0.1");
3634 assert_eq!(radio.dev_model(), Some("Fake Board"));
3635 assert_eq!(radio.boot_status(), Status::RESET_POWER_ON);
3636 assert!(radio.t_frame_ms() > 0);
3637 }
3638
3639 #[tokio::test]
3640 async fn explicit_reset_returns_the_announced_status() {
3641 let mut radio = attached_radio().await;
3642 let status = radio.reset().await.unwrap();
3643 assert_eq!(status, Status::RESET_SOFTWARE);
3644 radio.get_prop(prop::LAST_STATUS).await.unwrap();
3646 }
3647
3648 #[tokio::test]
3649 async fn write_only_pairing_pin_accepts_status_completion() {
3650 let mut radio = attached_radio().await;
3651 radio.set_ble_pairing_pin(Some(123_456)).await.unwrap();
3652 radio.set_ble_pairing_pin(None).await.unwrap();
3653 assert!(radio.set_ble_pairing_pin(Some(1_000_000)).await.is_err());
3654
3655 let error = radio
3656 .set_prop(prop::BLE_PAIRING_PIN, &123_456u32.to_le_bytes())
3657 .await
3658 .unwrap_err();
3659 assert!(matches!(error, UlcpError::Protocol(_)));
3660 }
3661
3662 #[tokio::test]
3663 async fn device_name_typed_accessors_round_trip_and_validate() {
3664 let mut radio = attached_radio().await;
3665 radio.set_device_name("Field Radio 📻").await.unwrap();
3666 assert_eq!(radio.device_name().await.unwrap(), "Field Radio 📻");
3667 assert!(radio.set_device_name("").await.is_err());
3668 assert!(radio.set_device_name(&"x".repeat(65)).await.is_err());
3669 assert!(radio.set_device_name("bad\0name").await.is_err());
3670 }
3671
3672 #[tokio::test]
3673 async fn transmit_and_receive_round_trip() {
3674 let mut radio = attached_radio().await;
3675 let packet = [0x10u8, 0x20, 0x30, 0x40];
3676 radio.transmit(&packet, TxOptions::default()).await.unwrap();
3677
3678 let mut buf = [0u8; 256];
3679 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3680 .await
3681 .unwrap();
3682 assert_eq!(&buf[..info.len], &packet);
3683 assert_eq!(info.rssi, -91);
3684 assert_eq!(info.snr.as_centibels(), 55);
3685 }
3686
3687 #[tokio::test]
3688 async fn cca_failure_maps_to_cad_timeout() {
3689 let mut radio = attached_radio().await;
3690 let result = radio
3691 .transmit(
3692 CCA_FAIL,
3693 TxOptions {
3694 cad: CadPolicy::Gate,
3695 },
3696 )
3697 .await;
3698 assert!(matches!(result, Err(TxError::CadTimeout)));
3699 }
3700
3701 #[tokio::test]
3702 async fn oversized_frame_rejected() {
3703 let mut radio = attached_radio().await;
3704 let oversized = vec![0u8; radio.max_frame_size() + 1];
3705 let result = radio.transmit(&oversized, TxOptions::default()).await;
3706 assert!(matches!(
3707 result,
3708 Err(TxError::Io(UlcpError::FrameTooLarge(_)))
3709 ));
3710 }
3711
3712 #[tokio::test]
3713 async fn unexpected_reset_surfaces_on_receive() {
3714 let mut radio = attached_radio().await;
3715 radio
3716 .transmit(RESET_AFTER, TxOptions::default())
3717 .await
3718 .unwrap();
3719
3720 let mut buf = [0u8; 256];
3721 let result = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf)).await;
3722 assert!(matches!(
3723 result,
3724 Err(UlcpError::UnexpectedReset(status))
3725 if status == Status::RESET_WATCHDOG
3726 ));
3727 }
3728
3729 #[tokio::test]
3730 async fn table_insert_replace_remove_with_secret_free_digests() {
3731 let mut radio = attached_radio().await;
3732 let mut item = vec![0x11u8; 64];
3733 item[32..].fill(0x22);
3734 let digest = radio
3735 .insert_prop_item(prop::HOST_PEER_KEYS, &item)
3736 .await
3737 .unwrap();
3738 assert_eq!(digest, vec![0x11; 32]);
3740
3741 let mut replacement = item.clone();
3743 replacement[32..].fill(0x33);
3744 let digest = radio
3745 .insert_prop_item(prop::HOST_PEER_KEYS, &replacement)
3746 .await
3747 .unwrap();
3748 assert_eq!(digest, vec![0x11; 32]);
3749
3750 let removed = radio
3751 .remove_prop_item(prop::HOST_PEER_KEYS, &[0x11; 32])
3752 .await
3753 .unwrap();
3754 assert_eq!(removed, vec![0x11; 32]);
3755 let error = radio
3756 .remove_prop_item(prop::HOST_PEER_KEYS, &[0x11; 32])
3757 .await
3758 .unwrap_err();
3759 assert!(matches!(error, UlcpError::Status(status) if status == Status::ITEM_NOT_FOUND));
3760 }
3761
3762 #[tokio::test]
3763 async fn duplicate_insert_reports_already() {
3764 let mut radio = attached_radio().await;
3765 let filter = [2u8, 0]; radio
3767 .insert_prop_item(prop::HOST_RX_FILTERS, &filter)
3768 .await
3769 .unwrap();
3770 let error = radio
3771 .insert_prop_item(prop::HOST_RX_FILTERS, &filter)
3772 .await
3773 .unwrap_err();
3774 assert!(matches!(error, UlcpError::Status(status) if status == Status::ALREADY));
3775 }
3776
3777 #[tokio::test]
3778 async fn queue_drain_delivers_buffered_frames_then_completes() {
3779 let mut radio = attached_radio().await;
3780 let mut drained = Vec::new();
3781 radio
3782 .queue_drain_with(|data, meta| {
3783 drained.push((data.to_vec(), BufferedRxMeta::decode(meta).unwrap()));
3784 })
3785 .await
3786 .unwrap();
3787 assert_eq!(drained.len(), 2);
3788 assert!(
3789 drained
3790 .iter()
3791 .all(|(_, meta)| meta.flags & RX_FLAG_BUFFERED != 0)
3792 );
3793 assert_eq!((drained[0].1.age_s, drained[1].1.age_s), (5, 3));
3794
3795 let mut buf = [0u8; 16];
3798 for expected in [0xB0u8, 0xB1] {
3799 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3800 .await
3801 .unwrap();
3802 assert_eq!(&buf[..info.len], &[expected]);
3803 }
3804 }
3805
3806 #[tokio::test]
3811 async fn a_device_without_multi_property_support_reports_unimplemented() {
3812 let mut radio = attached_radio().await;
3813 let error = radio
3814 .get_props(&[prop::PHY_FREQ, prop::PHY_TX_POWER])
3815 .await
3816 .expect_err("the fake device does not implement CMD_PROP_MULTI_GET");
3817 assert!(matches!(error, UlcpError::Status(Status::UNIMPLEMENTED)));
3818 }
3819
3820 #[tokio::test]
3830 async fn an_administrative_handle_reads_the_host_domain_but_will_not_write_it() {
3831 let (client, server) = tokio::io::duplex(4096);
3832 tokio::spawn(fake_device(server));
3833 let mut radio = UlcpDevice::bare(SerialFrameLink::new(client), test_config());
3834 radio.mode = AttachMode::Administrative;
3835
3836 let error = radio
3837 .get_props(&[prop::HOST_KEY, prop::HOST_AUTO_ACK])
3838 .await
3839 .expect_err("the fake device does not implement CMD_PROP_MULTI_GET");
3840 assert!(
3841 matches!(error, UlcpError::Status(Status::UNIMPLEMENTED)),
3842 "a host-domain read must reach the device, got {error:?}"
3843 );
3844
3845 let error = radio
3846 .set_prop(prop::HOST_AUTO_ACK, &[1])
3847 .await
3848 .expect_err("a host-domain write needs a tethered attach");
3849 assert!(matches!(error, UlcpError::AdministrativeAttach));
3850 }
3851
3852 #[tokio::test]
3853 async fn save_and_clear_complete_on_status() {
3854 let mut radio = attached_radio().await;
3855 radio.save().await.unwrap();
3856 radio.clear().await.unwrap();
3857 }
3858
3859 #[tokio::test]
3860 async fn restore_update_form_reports_updated_and_retains_events() {
3861 let mut radio = attached_radio().await;
3862 assert_eq!(radio.restore().await.unwrap(), RestoreCompletion::Updated);
3863 assert_eq!(
3864 radio.pop_prop_event(),
3865 Some(PropEvent::Is {
3866 key: prop::PHY_FREQ,
3867 value: 905_000u32.to_le_bytes().to_vec(),
3868 })
3869 );
3870 assert_eq!(radio.pop_prop_event(), None);
3871 }
3872
3873 #[tokio::test]
3874 async fn restore_reset_form_is_success_not_unexpected_reset() {
3875 let mut radio = attached_radio().await;
3876 radio.set_prop(RESTORE_RESET_FORM_KEY, &[1]).await.unwrap();
3877 assert_eq!(radio.restore().await.unwrap(), RestoreCompletion::Reset);
3878
3879 radio.transmit(&[0x55], TxOptions::default()).await.unwrap();
3882 let mut buf = [0u8; 16];
3883 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3884 .await
3885 .unwrap();
3886 assert_eq!(&buf[..info.len], &[0x55]);
3887 }
3888
3889 #[tokio::test]
3890 async fn unsolicited_table_notifications_are_retained_events() {
3891 let mut radio = attached_radio().await;
3892 let mut buf = [0u8; 48];
3893 let len = frame::prop_inserted(&mut buf, TID_UNSOLICITED, prop::HOST_RX_FILTERS, &[2, 0])
3894 .unwrap();
3895 radio.ingest_frame(&buf[..len]);
3896 let len = frame::prop_removed(
3897 &mut buf,
3898 TID_UNSOLICITED,
3899 prop::HOST_CHANNEL_KEYS,
3900 &[0x12, 0x34],
3901 )
3902 .unwrap();
3903 radio.ingest_frame(&buf[..len]);
3904
3905 assert_eq!(
3906 radio.pop_prop_event(),
3907 Some(PropEvent::Inserted {
3908 key: prop::HOST_RX_FILTERS,
3909 digest: vec![2, 0],
3910 })
3911 );
3912 assert_eq!(
3913 radio.pop_prop_event(),
3914 Some(PropEvent::Removed {
3915 key: prop::HOST_CHANNEL_KEYS,
3916 digest: vec![0x12, 0x34],
3917 })
3918 );
3919 assert_eq!(radio.pop_prop_event(), None);
3920 }
3921
3922 #[test]
3923 fn airtime_is_plausible() {
3924 let airtime = lora_airtime_ms(11, 250_000, 5, 255);
3926 assert!((500..5_000).contains(&airtime), "airtime {airtime}");
3927 assert!(lora_airtime_ms(7, 250_000, 5, 255) < airtime);
3929 }
3930}