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, 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, 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::{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: 0x1424,
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
164struct Response {
167 tid: u8,
168 kind: ResponseKind,
169 key: u32,
170 value: Vec<u8>,
171}
172
173#[derive(Clone, Copy)]
174enum PropResponsePolicy {
175 Value,
176 StatusOnly,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
185pub enum PropEvent {
186 Is { key: u32, value: Vec<u8> },
188 Inserted { key: u32, digest: Vec<u8> },
190 Removed { key: u32, digest: Vec<u8> },
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum TraceDirection {
198 HostToDevice,
199 DeviceToHost,
200}
201
202impl core::fmt::Display for TraceDirection {
203 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
204 formatter.write_str(match self {
205 Self::HostToDevice => "host→device",
206 Self::DeviceToHost => "device→host",
207 })
208 }
209}
210
211pub type FrameTrace = Box<dyn FnMut(TraceDirection, &str) + Send>;
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub enum RestoreCompletion {
220 Updated,
223 Reset,
227}
228
229#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232pub enum HostOwnership {
233 Ours,
236 Unclaimed,
238 OtherHost([u8; 32]),
243 Unsupported,
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255pub enum SavedSnapshot {
256 None,
258 Current,
260 Fallback,
264 Unreadable,
267}
268
269impl SavedSnapshot {
270 fn from_octet(value: &[u8]) -> Result<Self, UlcpError> {
271 match value {
272 [ids::saved::NONE] => Ok(Self::None),
273 [ids::saved::CURRENT] => Ok(Self::Current),
274 [ids::saved::FALLBACK] => Ok(Self::Fallback),
275 [ids::saved::UNREADABLE] => Ok(Self::Unreadable),
276 _ => Err(UlcpError::Protocol("malformed PROP_SAVED")),
277 }
278 }
279
280 pub fn is_saved(self) -> bool {
283 matches!(self, Self::Current | Self::Fallback)
284 }
285}
286
287#[derive(Clone, Debug)]
292pub struct DeviceSync {
293 pub last_status: Status,
295 pub reset_since_last_contact: bool,
299 pub capabilities: Vec<u32>,
301 pub ownership: HostOwnership,
303 pub host_key: Option<[u8; 32]>,
305 pub phy_enabled: bool,
308 pub freq_khz: u32,
310 pub device_name: String,
312 pub saved: Option<SavedSnapshot>,
314 pub queue_count: Option<u16>,
316 pub queue_dropped: Option<u32>,
318 pub filters: Option<Vec<items::Filter>>,
320 pub host_channel_ids: Option<Vec<[u8; items::CHANNEL_ID_LEN]>>,
323 pub host_peer_keys: Option<Vec<[u8; items::PUBLIC_KEY_LEN]>>,
326 pub auto_ack: Option<bool>,
328 pub dev_key: Option<[u8; 32]>,
331}
332
333impl DeviceSync {
334 pub fn has_capability(&self, capability: u32) -> bool {
336 self.capabilities.contains(&capability)
337 }
338}
339
340#[derive(Clone, Debug, Default, PartialEq, Eq)]
346pub struct RepeaterPolicy {
347 pub enabled: bool,
350 pub regions: Vec<RegionCode>,
353 pub default_region: Option<RegionCode>,
356 pub min_rssi: Option<i16>,
359 pub min_snr: Option<i8>,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
365pub struct DeviceTime {
366 pub epoch: Option<u32>,
369 pub tz_offset_min: i16,
372}
373
374#[derive(Clone, Copy, Debug, PartialEq, Eq)]
381pub struct GnssStatus {
382 pub enabled: bool,
385 pub fix: GnssSnapshot,
387 pub ident_update: bool,
390 pub ident_precision: u8,
393 pub time_trust: bool,
396}
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
400pub struct AdvertPolicy {
401 pub advert_interval_s: u32,
404 pub beacon_interval_s: u32,
406 pub startup_beacon: bool,
408}
409
410#[derive(Clone, Debug)]
413pub struct HostProvisioning {
414 pub host_key: [u8; 32],
418 pub filters: Vec<items::Filter>,
420 pub channel_keys: Vec<[u8; items::CHANNEL_KEY_LEN]>,
422 pub peer_keys: Vec<items::PeerKeyEntry>,
428 pub auto_ack: bool,
430}
431
432#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
439pub struct ProvisionReport {
440 pub host_replaced: bool,
443 pub filters_replaced: bool,
445 pub channels_replaced: bool,
449 pub channels_inserted: usize,
451 pub peers_inserted: usize,
453 pub peers_removed: usize,
455 pub auto_ack_changed: bool,
457}
458
459#[allow(async_fn_in_trait)]
461pub trait FrameLink {
462 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError>;
464
465 fn poll_recv_frame(
470 &mut self,
471 cx: &mut core::task::Context<'_>,
472 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>>;
473
474 async fn recv_frame(&mut self) -> Result<Vec<u8>, UlcpError> {
476 core::future::poll_fn(|cx| self.poll_recv_frame(cx)).await
477 }
478}
479
480pub struct SerialFrameLink<IO> {
482 io: IO,
483 decoder: hdlc::Decoder<WIRE_BUF>,
484 read_buf: [u8; READ_CHUNK],
485 read_pos: usize,
486 read_len: usize,
487}
488
489impl<IO> SerialFrameLink<IO> {
490 pub fn new(io: IO) -> Self {
492 Self {
493 io,
494 decoder: hdlc::Decoder::new(),
495 read_buf: [0; READ_CHUNK],
496 read_pos: 0,
497 read_len: 0,
498 }
499 }
500
501 pub fn into_inner(self) -> IO {
503 self.io
504 }
505}
506
507impl<IO> FrameLink for SerialFrameLink<IO>
508where
509 IO: AsyncRead + AsyncWrite + Unpin,
510{
511 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
512 let mut wire = vec![0u8; hdlc::max_encoded_len(frame.len())];
513 let len = hdlc::encode_frame(frame, &mut wire).expect("buffer sized with max_encoded_len");
514 self.io.write_all(&wire[..len]).await?;
515 self.io.flush().await?;
516 Ok(())
517 }
518
519 fn poll_recv_frame(
520 &mut self,
521 cx: &mut core::task::Context<'_>,
522 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
523 loop {
524 while self.read_pos < self.read_len {
525 let byte = self.read_buf[self.read_pos];
526 self.read_pos += 1;
527 if let Some(Ok(frame)) = self.decoder.push(byte) {
528 return core::task::Poll::Ready(Ok(frame.to_vec()));
529 }
530 }
531
532 self.read_pos = 0;
533 self.read_len = 0;
534 let mut read_buf = ReadBuf::new(&mut self.read_buf);
535 match core::pin::Pin::new(&mut self.io).poll_read(cx, &mut read_buf) {
536 core::task::Poll::Ready(Ok(())) => {
537 self.read_len = read_buf.filled().len();
538 if self.read_len == 0 {
539 return core::task::Poll::Ready(Err(UlcpError::Disconnected));
540 }
541 }
542 core::task::Poll::Ready(Err(error)) => {
543 return core::task::Poll::Ready(Err(UlcpError::Io(error)));
544 }
545 core::task::Poll::Pending => return core::task::Poll::Pending,
546 }
547 }
548 }
549}
550
551#[cfg(feature = "ble-radio")]
553#[derive(Clone, Copy, Debug)]
554pub struct BleFrameLinkConfig {
555 pub segment_payload: usize,
557 pub discovery_timeout: Duration,
559 pub operation_timeout: Duration,
561 pub pairing_timeout: Duration,
565}
566
567#[cfg(feature = "ble-radio")]
568impl Default for BleFrameLinkConfig {
569 fn default() -> Self {
570 Self {
571 segment_payload: 19,
573 discovery_timeout: Duration::from_secs(10),
574 operation_timeout: Duration::from_secs(10),
575 pairing_timeout: Duration::from_secs(90),
576 }
577 }
578}
579
580#[cfg(feature = "ble-radio")]
581impl BleFrameLinkConfig {
582 fn validate(&self) -> Result<(), UlcpError> {
583 if !(1..=511).contains(&self.segment_payload) {
584 return Err(UlcpError::Protocol(
585 "BLE segment payload must be in 1..=511",
586 ));
587 }
588 if self.discovery_timeout.is_zero()
589 || self.operation_timeout.is_zero()
590 || self.pairing_timeout.is_zero()
591 {
592 return Err(UlcpError::Protocol(
593 "BLE discovery, operation, and pairing timeouts must be nonzero",
594 ));
595 }
596 Ok(())
597 }
598}
599
600#[cfg(feature = "ble-radio")]
601struct BleNotificationReceiver {
602 notifications: tokio::sync::mpsc::Receiver<Vec<u8>>,
603 reassembler: umsh_ulcp::gatt::Reassembler<{ umsh_ulcp::gatt::MAX_FRAME }>,
604}
605
606#[cfg(feature = "ble-radio")]
607impl BleNotificationReceiver {
608 fn new(notifications: tokio::sync::mpsc::Receiver<Vec<u8>>) -> Self {
609 Self {
610 notifications,
611 reassembler: umsh_ulcp::gatt::Reassembler::new(),
612 }
613 }
614
615 fn poll_recv_frame(
616 &mut self,
617 cx: &mut core::task::Context<'_>,
618 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
619 loop {
620 match self.notifications.poll_recv(cx) {
621 core::task::Poll::Ready(Some(segment)) => {
622 if let Some(Ok(frame)) = self.reassembler.push(&segment) {
623 return core::task::Poll::Ready(Ok(frame.to_vec()));
624 }
625 }
627 core::task::Poll::Ready(None) => {
628 self.reassembler.reset();
629 return core::task::Poll::Ready(Err(UlcpError::Disconnected));
630 }
631 core::task::Poll::Pending => return core::task::Poll::Pending,
632 }
633 }
634 }
635}
636
637#[cfg(feature = "ble-radio")]
640#[derive(Clone, Debug)]
641pub struct BleScanResult {
642 pub id: String,
644 pub name: Option<String>,
646 pub rssi: Option<i16>,
648}
649
650#[cfg(feature = "ble-radio")]
652pub struct BleFrameLink {
653 peripheral: btleplug::platform::Peripheral,
654 frame_in: btleplug::api::Characteristic,
655 receiver: BleNotificationReceiver,
656 segment_payload: usize,
657 operation_timeout: Duration,
658}
659
660#[cfg(feature = "ble-radio")]
661impl BleFrameLink {
662 pub async fn scan(timeout: Duration) -> Result<Vec<BleScanResult>, UlcpError> {
669 use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
670
671 let manager = btleplug::platform::Manager::new()
672 .await
673 .map_err(ble_error)?;
674 let adapters = manager.adapters().await.map_err(ble_error)?;
675 let service = uuid::Uuid::from_u128(umsh_ulcp::gatt::SERVICE_UUID);
676 let deadline = Instant::now() + timeout;
677 let mut results: Vec<BleScanResult> = Vec::new();
678
679 for adapter in adapters {
680 adapter
681 .start_scan(ScanFilter {
682 services: vec![service],
683 })
684 .await
685 .map_err(ble_error)?;
686 while Instant::now() < deadline {
687 tokio::time::sleep(Duration::from_millis(250)).await;
688 let peripherals =
689 match tokio::time::timeout_at(deadline, adapter.peripherals()).await {
690 Ok(result) => result.map_err(ble_error)?,
691 Err(_) => break,
692 };
693 for peripheral in peripherals {
694 let properties =
695 match tokio::time::timeout_at(deadline, peripheral.properties()).await {
696 Ok(result) => result.map_err(ble_error)?,
697 Err(_) => break,
698 };
699 let advertises_service = properties
700 .as_ref()
701 .is_some_and(|properties| properties.services.contains(&service));
702 if !advertises_service {
703 continue;
704 }
705 let id = peripheral.id().to_string();
706 let name = properties
707 .as_ref()
708 .and_then(|properties| properties.local_name.clone());
709 let rssi = properties.as_ref().and_then(|properties| properties.rssi);
710 match results.iter_mut().find(|result| result.id == id) {
711 Some(existing) => {
712 existing.name = name.or(existing.name.take());
713 existing.rssi = rssi.or(existing.rssi);
714 }
715 None => results.push(BleScanResult { id, name, rssi }),
716 }
717 }
718 }
719 let _ = tokio::time::timeout(Duration::from_secs(1), adapter.stop_scan()).await;
722 }
723 Ok(results)
724 }
725
726 pub async fn connect(
731 selector: Option<&str>,
732 config: BleFrameLinkConfig,
733 ) -> Result<Self, UlcpError> {
734 use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
735 use futures_util::StreamExt;
736
737 config.validate()?;
738
739 let manager = btleplug::platform::Manager::new()
740 .await
741 .map_err(ble_error)?;
742 let adapters = manager.adapters().await.map_err(ble_error)?;
743 let service = uuid::Uuid::from_u128(umsh_ulcp::gatt::SERVICE_UUID);
744 let deadline = Instant::now() + config.discovery_timeout;
745 let mut matches = Vec::new();
746
747 for adapter in adapters {
748 adapter
749 .start_scan(ScanFilter {
750 services: vec![service],
751 })
752 .await
753 .map_err(ble_error)?;
754 loop {
755 if Instant::now() >= deadline {
756 break;
757 }
758 tokio::time::sleep(Duration::from_millis(250)).await;
759 matches.clear();
760 let peripherals =
761 match tokio::time::timeout_at(deadline, adapter.peripherals()).await {
762 Ok(result) => result.map_err(ble_error)?,
763 Err(_) => break,
764 };
765 for peripheral in peripherals {
766 let properties =
767 match tokio::time::timeout_at(deadline, peripheral.properties()).await {
768 Ok(result) => result.map_err(ble_error)?,
769 Err(_) => break,
770 };
771 let id = peripheral.id().to_string();
772 let name = properties
773 .as_ref()
774 .and_then(|properties| properties.local_name.as_deref());
775 let selected = selector.is_none_or(|selector| {
776 id == selector || name.is_some_and(|name| name.contains(selector))
777 });
778 let advertises_service = properties
779 .as_ref()
780 .is_some_and(|properties| properties.services.contains(&service));
781 if selected && advertises_service {
782 matches.push(peripheral);
783 }
784 }
785 if !matches.is_empty() || Instant::now() >= deadline {
786 break;
787 }
788 }
789 let _ = tokio::time::timeout(Duration::from_secs(1), adapter.stop_scan()).await;
792 if !matches.is_empty() {
793 break;
794 }
795 }
796
797 let peripheral = match matches.len() {
798 0 => {
799 return Err(UlcpError::Transport(
800 "no ULCP GATT Service peripheral found".into(),
801 ));
802 }
803 1 => matches.pop().unwrap(),
804 _ => {
805 return Err(UlcpError::Transport(
806 "multiple companion radios found; provide a selector".into(),
807 ));
808 }
809 };
810
811 let setup = async {
812 let is_connected =
813 tokio::time::timeout(config.operation_timeout, peripheral.is_connected())
814 .await
815 .map_err(|_| ble_timeout("querying connection state"))?
816 .map_err(ble_error)?;
817 if !is_connected {
818 tokio::time::timeout(config.operation_timeout, peripheral.connect())
819 .await
820 .map_err(|_| ble_timeout("connecting"))?
821 .map_err(ble_error)?;
822 }
823 tokio::time::timeout(config.operation_timeout, peripheral.discover_services())
824 .await
825 .map_err(|_| ble_timeout("discovering services"))?
826 .map_err(ble_error)?;
827
828 let frame_in_uuid = uuid::Uuid::from_u128(umsh_ulcp::gatt::FRAME_IN_UUID);
829 let frame_out_uuid = uuid::Uuid::from_u128(umsh_ulcp::gatt::FRAME_OUT_UUID);
830 let characteristics = peripheral.characteristics();
831 let frame_in = characteristics
832 .iter()
833 .find(|characteristic| characteristic.uuid == frame_in_uuid)
834 .cloned()
835 .ok_or(UlcpError::Protocol("missing BLE Frame In"))?;
836 let frame_out = characteristics
837 .iter()
838 .find(|characteristic| characteristic.uuid == frame_out_uuid)
839 .cloned()
840 .ok_or(UlcpError::Protocol("missing BLE Frame Out"))?;
841
842 let mut stream =
843 tokio::time::timeout(config.operation_timeout, peripheral.notifications())
844 .await
845 .map_err(|_| ble_timeout("opening notifications"))?
846 .map_err(ble_error)?;
847 let (tx, notifications) = tokio::sync::mpsc::channel(32);
848 tokio::spawn(async move {
849 while let Some(notification) = stream.next().await {
850 if notification.uuid == frame_out_uuid
851 && tx.send(notification.value).await.is_err()
852 {
853 break;
854 }
855 }
856 });
857 tokio::time::timeout(config.pairing_timeout, peripheral.subscribe(&frame_out))
860 .await
861 .map_err(|_| ble_timeout("subscribing to Frame Out"))?
862 .map_err(ble_error)?;
863 Ok::<_, UlcpError>((frame_in, notifications))
864 }
865 .await;
866
867 let (frame_in, notifications) = match setup {
868 Ok(setup) => setup,
869 Err(error) => {
870 let _ = tokio::time::timeout(Duration::from_secs(1), peripheral.disconnect()).await;
873 return Err(error);
874 }
875 };
876
877 Ok(Self {
878 peripheral,
879 frame_in,
880 receiver: BleNotificationReceiver::new(notifications),
881 segment_payload: config.segment_payload,
882 operation_timeout: config.operation_timeout,
883 })
884 }
885
886 async fn diagnose_and_disconnect(&self, failure: String) -> UlcpError {
890 use btleplug::api::Peripheral as _;
891
892 let connected = match tokio::time::timeout(
893 Duration::from_secs(2),
894 self.peripheral.is_connected(),
895 )
896 .await
897 {
898 Ok(Ok(value)) => value.to_string(),
899 Ok(Err(error)) => format!("error({error})"),
900 Err(_) => "query-timeout".into(),
901 };
902 let cleanup = match tokio::time::timeout(
903 Duration::from_secs(2),
904 self.peripheral.disconnect(),
905 )
906 .await
907 {
908 Ok(Ok(())) => "ok".into(),
909 Ok(Err(error)) => format!("error({error})"),
910 Err(_) => "timeout".into(),
911 };
912 UlcpError::Transport(format!(
913 "{failure}; backend is_connected={connected}; disconnect cleanup={cleanup}"
914 ))
915 }
916}
917
918#[cfg(feature = "ble-radio")]
919impl FrameLink for BleFrameLink {
920 async fn send_frame(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
921 use btleplug::api::{Peripheral as _, WriteType};
922
923 for segment in umsh_ulcp::gatt::segments(frame, self.segment_payload) {
924 let mut value = vec![0; segment.payload().len() + 1];
925 segment
926 .write_to(&mut value)
927 .expect("segment destination is exactly sized");
928 let write = tokio::time::timeout(
929 self.operation_timeout,
930 self.peripheral
931 .write(&self.frame_in, &value, WriteType::WithResponse),
932 )
933 .await;
934 match write {
935 Ok(Ok(())) => {}
936 Ok(Err(error)) => {
937 return Err(self
938 .diagnose_and_disconnect(format!("BLE Frame In write failed: {error}"))
939 .await);
940 }
941 Err(_) => {
942 return Err(self
943 .diagnose_and_disconnect("BLE timed out while writing Frame In".into())
944 .await);
945 }
946 }
947 }
948 Ok(())
949 }
950
951 fn poll_recv_frame(
952 &mut self,
953 cx: &mut core::task::Context<'_>,
954 ) -> core::task::Poll<Result<Vec<u8>, UlcpError>> {
955 self.receiver.poll_recv_frame(cx)
956 }
957}
958
959#[cfg(feature = "ble-radio")]
960fn ble_error(error: btleplug::Error) -> UlcpError {
961 UlcpError::Transport(error.to_string())
962}
963
964#[cfg(feature = "ble-radio")]
965fn ble_timeout(operation: &'static str) -> UlcpError {
966 UlcpError::Transport(format!("BLE timed out while {operation}"))
967}
968
969pub struct UlcpDevice<L> {
972 link: L,
973 config: UlcpDeviceConfig,
974 rx_queue: VecDeque<RxPacket>,
975 responses: VecDeque<Response>,
976 prop_events: VecDeque<PropEvent>,
977 seen_reset: Option<Status>,
979 max_frame_size: usize,
980 t_frame_ms: u32,
981 dev_version: String,
982 boot_status: Status,
984 tids: TidAllocator,
985 trace: Option<FrameTrace>,
987 mode: AttachMode,
988}
989
990#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
999pub enum AttachMode {
1000 #[default]
1003 Tethered,
1004 Administrative,
1007}
1008
1009impl<L> UlcpDevice<L>
1010where
1011 L: FrameLink,
1012{
1013 fn bare(link: L, config: UlcpDeviceConfig) -> Self {
1014 Self {
1015 link,
1016 config,
1017 rx_queue: VecDeque::new(),
1018 responses: VecDeque::new(),
1019 prop_events: VecDeque::new(),
1020 seen_reset: None,
1021 max_frame_size: 0,
1022 t_frame_ms: 0,
1023 dev_version: String::new(),
1024 boot_status: Status::RESET_UNKNOWN,
1025 tids: TidAllocator::new(),
1026 trace: None,
1027 mode: AttachMode::Tethered,
1028 }
1029 }
1030
1031 pub fn attach_mode(&self) -> AttachMode {
1033 self.mode
1034 }
1035
1036 fn require_tethered(&self, key: u32) -> Result<(), UlcpError> {
1038 let host_domain = matches!(
1039 key,
1040 prop::HOST_KEY
1041 | prop::HOST_CHANNEL_KEYS
1042 | prop::HOST_PEER_KEYS
1043 | prop::HOST_RX_FILTERS
1044 | prop::HOST_AUTO_ACK
1045 );
1046 match self.mode {
1047 AttachMode::Administrative if host_domain => Err(UlcpError::AdministrativeAttach),
1048 _ => Ok(()),
1049 }
1050 }
1051
1052 pub async fn new(link: L, config: UlcpDeviceConfig) -> Result<Self, UlcpError> {
1062 let mut radio = Self::bare(link, config);
1063 radio.initialize().await?;
1064 Ok(radio)
1065 }
1066
1067 pub async fn attach_existing(link: L, config: UlcpDeviceConfig) -> Result<Self, UlcpError> {
1084 Self::attach_with_mode(link, config, AttachMode::Tethered).await
1085 }
1086
1087 pub async fn attach_administrative(
1103 link: L,
1104 config: UlcpDeviceConfig,
1105 ) -> Result<Self, UlcpError> {
1106 Self::attach_with_mode(link, config, AttachMode::Administrative).await
1107 }
1108
1109 async fn attach_with_mode(
1110 link: L,
1111 config: UlcpDeviceConfig,
1112 mode: AttachMode,
1113 ) -> Result<Self, UlcpError> {
1114 let mut radio = Self::bare(link, config);
1115 radio.mode = mode;
1116 let boot_status = radio.get_prop(prop::LAST_STATUS).await?;
1119 radio.boot_status = decode_status(&boot_status);
1120
1121 let version = radio.get_prop(prop::PROTOCOL_VERSION).await?;
1122 if version.first().copied() != Some(ids::PROTOCOL_MAJOR_VERSION) {
1123 return Err(UlcpError::Protocol("protocol major version mismatch"));
1124 }
1125 let dev_version = radio.get_prop(prop::DEV_VERSION).await?;
1126 radio.dev_version = String::from_utf8_lossy(&dev_version)
1127 .trim_end_matches('\0')
1128 .to_owned();
1129
1130 let mtu = radio.get_prop(prop::PHY_MTU).await?;
1131 let [mtu_lo, mtu_hi, ..] = mtu[..] else {
1132 return Err(UlcpError::Protocol("malformed PROP_PHY_MTU"));
1133 };
1134 radio.max_frame_size = usize::from(u16::from_le_bytes([mtu_lo, mtu_hi]));
1135 if radio.max_frame_size == 0 {
1136 return Err(UlcpError::Protocol("device advertised zero MTU"));
1137 }
1138 radio.t_frame_ms = lora_airtime_ms(
1139 radio.config.spreading_factor,
1140 radio.config.bandwidth_hz,
1141 radio.config.coding_rate_denom,
1142 radio.max_frame_size,
1143 )
1144 .max(1);
1145 Ok(radio)
1146 }
1147
1148 pub fn into_link(self) -> L {
1158 self.link
1159 }
1160
1161 pub fn set_frame_trace(&mut self, trace: Option<FrameTrace>) {
1166 self.trace = trace;
1167 }
1168
1169 async fn send(&mut self, frame: &[u8]) -> Result<(), UlcpError> {
1171 if let Some(trace) = &mut self.trace {
1172 trace(TraceDirection::HostToDevice, &describe_frame(frame));
1173 }
1174 self.link.send_frame(frame).await
1175 }
1176
1177 pub fn dev_version(&self) -> &str {
1179 &self.dev_version
1180 }
1181
1182 pub async fn device_name(&mut self) -> Result<String, UlcpError> {
1184 let value = self.get_prop(prop::DEV_NAME).await?;
1185 let name = core::str::from_utf8(&value)
1186 .map_err(|_| UlcpError::Protocol("malformed PROP_DEV_NAME"))?;
1187 if name.is_empty() || value.len() > 64 || value.contains(&0) {
1188 return Err(UlcpError::Protocol("malformed PROP_DEV_NAME"));
1189 }
1190 Ok(name.to_owned())
1191 }
1192
1193 pub async fn set_device_name(&mut self, name: &str) -> Result<(), UlcpError> {
1195 if name.is_empty() || name.len() > 64 || name.as_bytes().contains(&0) {
1196 return Err(UlcpError::Protocol("invalid PROP_DEV_NAME"));
1197 }
1198 let authoritative = self.set_prop(prop::DEV_NAME, name.as_bytes()).await?;
1199 if authoritative != name.as_bytes() {
1200 return Err(UlcpError::Protocol("PROP_DEV_NAME response mismatch"));
1201 }
1202 Ok(())
1203 }
1204
1205 pub async fn battery_status(&mut self) -> Result<Option<BatteryStatus>, UlcpError> {
1214 if !self.capabilities().await?.contains(&cap::BATTERY) {
1215 return Ok(None);
1216 }
1217 let value = self.get_prop(prop::BATTERY).await?;
1218 match BatteryStatus::decode(&value) {
1219 Ok(status) => Ok(Some(status)),
1220 Err(_) => Err(UlcpError::Protocol("malformed PROP_BATTERY")),
1221 }
1222 }
1223
1224 pub async fn illuminance(&mut self) -> Result<Option<u32>, UlcpError> {
1233 if !self.capabilities().await?.contains(&cap::ILLUMINANCE) {
1234 return Ok(None);
1235 }
1236 let value = self.get_prop(prop::ILLUMINANCE).await?;
1237 match value.len() {
1238 0 => Ok(None),
1239 4 => Ok(Some(u32::from_le_bytes(value[..4].try_into().unwrap()))),
1240 _ => Err(UlcpError::Protocol("malformed PROP_ILLUMINANCE")),
1241 }
1242 }
1243
1244 pub async fn alert(&mut self) -> Result<Option<AlertState>, UlcpError> {
1249 if !self.capabilities().await?.contains(&cap::ALERT) {
1250 return Ok(None);
1251 }
1252 let value = self.get_prop(prop::ALERT).await?;
1253 Ok(Some(decode_alert(&value)?))
1254 }
1255
1256 pub async fn set_alert(&mut self, state: AlertState) -> Result<AlertState, UlcpError> {
1269 let mut value = [0u8; pui::MAX_LEN];
1270 let len = pui::encode(state.code(), &mut value)
1271 .map_err(|_| UlcpError::Protocol("PROP_ALERT encode"))?;
1272 let authoritative = self.set_prop(prop::ALERT, &value[..len]).await?;
1273 decode_alert(&authoritative)
1274 }
1275
1276 pub fn boot_status(&self) -> Status {
1278 self.boot_status
1279 }
1280
1281 pub async fn repeater_policy(&mut self) -> Result<Option<RepeaterPolicy>, UlcpError> {
1285 if !self.capabilities().await?.contains(&cap::REPEATER) {
1286 return Ok(None);
1287 }
1288 let enabled = self.get_prop(prop::MAC_REPEATER_ENABLED).await?;
1289 let enabled = match enabled.first() {
1290 Some(&byte) => byte != 0,
1291 None => return Err(UlcpError::Protocol("malformed PROP_MAC_REPEATER_ENABLED")),
1292 };
1293 let regions = decode_region_list(&self.get_prop(prop::MAC_REPEATER_REGIONS).await?)?;
1294 let default_region =
1295 decode_region_code(&self.get_prop(prop::MAC_REPEATER_DEFAULT_REGION).await?)?;
1296 let min_rssi = decode_opt_i16(&self.get_prop(prop::MAC_REPEATER_MIN_RSSI).await?)
1297 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_RSSI"))?;
1298 let min_snr = decode_opt_i8(&self.get_prop(prop::MAC_REPEATER_MIN_SNR).await?)
1299 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_SNR"))?;
1300 Ok(Some(RepeaterPolicy {
1301 enabled,
1302 regions,
1303 default_region,
1304 min_rssi,
1305 min_snr,
1306 }))
1307 }
1308
1309 pub async fn set_repeater_regions(
1318 &mut self,
1319 regions: &[RegionCode],
1320 ) -> Result<Vec<RegionCode>, UlcpError> {
1321 let mut value = Vec::with_capacity(regions.len() * 2);
1322 for region in regions {
1323 value.extend_from_slice(®ion.to_bytes());
1324 }
1325 let authoritative = self.set_prop(prop::MAC_REPEATER_REGIONS, &value).await?;
1326 decode_region_list(&authoritative)
1327 }
1328
1329 pub async fn set_repeater_default_region(
1337 &mut self,
1338 region: Option<RegionCode>,
1339 ) -> Result<Option<RegionCode>, UlcpError> {
1340 let value = region.map(|code| code.to_bytes()).unwrap_or_default();
1341 let value: &[u8] = match region {
1342 Some(_) => &value,
1343 None => &[],
1344 };
1345 let authoritative = self
1346 .set_prop(prop::MAC_REPEATER_DEFAULT_REGION, value)
1347 .await?;
1348 decode_region_code(&authoritative)
1349 }
1350
1351 pub async fn set_repeater_min_rssi(
1354 &mut self,
1355 min_rssi: Option<i16>,
1356 ) -> Result<Option<i16>, UlcpError> {
1357 let encoded = min_rssi.map(i16::to_le_bytes).unwrap_or_default();
1358 let value: &[u8] = match min_rssi {
1359 Some(_) => &encoded,
1360 None => &[],
1361 };
1362 let authoritative = self.set_prop(prop::MAC_REPEATER_MIN_RSSI, value).await?;
1363 decode_opt_i16(&authoritative)
1364 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_RSSI"))
1365 }
1366
1367 pub async fn set_repeater_min_snr(
1370 &mut self,
1371 min_snr: Option<i8>,
1372 ) -> Result<Option<i8>, UlcpError> {
1373 let encoded = [min_snr.unwrap_or_default() as u8];
1374 let value: &[u8] = match min_snr {
1375 Some(_) => &encoded,
1376 None => &[],
1377 };
1378 let authoritative = self.set_prop(prop::MAC_REPEATER_MIN_SNR, value).await?;
1379 decode_opt_i8(&authoritative)
1380 .ok_or(UlcpError::Protocol("malformed PROP_MAC_REPEATER_MIN_SNR"))
1381 }
1382
1383 pub async fn time(&mut self) -> Result<Option<DeviceTime>, UlcpError> {
1391 if !self.capabilities().await?.contains(&cap::TIME) {
1392 return Ok(None);
1393 }
1394 let epoch = decode_epoch(&self.get_prop(prop::TIME).await?)?;
1395 let tz_offset_min = decode_tz_offset(&self.get_prop(prop::TZ_OFFSET).await?)?;
1396 Ok(Some(DeviceTime {
1397 epoch,
1398 tz_offset_min,
1399 }))
1400 }
1401
1402 pub async fn set_time(&mut self, epoch: Option<u32>) -> Result<Option<u32>, UlcpError> {
1408 let encoded = epoch.map(u32::to_le_bytes).unwrap_or_default();
1409 let value: &[u8] = match epoch {
1410 Some(_) => &encoded,
1411 None => &[],
1412 };
1413 let authoritative = self.set_prop(prop::TIME, value).await?;
1414 decode_epoch(&authoritative)
1415 }
1416
1417 pub async fn set_tz_offset(&mut self, minutes: i16) -> Result<i16, UlcpError> {
1420 let authoritative = self
1421 .set_prop(prop::TZ_OFFSET, &minutes.to_le_bytes())
1422 .await?;
1423 decode_tz_offset(&authoritative)
1424 }
1425
1426 pub async fn gnss_status(&mut self) -> Result<Option<GnssStatus>, UlcpError> {
1433 if !self.capabilities().await?.contains(&cap::GNSS) {
1434 return Ok(None);
1435 }
1436 let enabled = decode_bool(
1437 &self.get_prop(prop::GNSS_ENABLED).await?,
1438 "PROP_GNSS_ENABLED",
1439 )?;
1440 let mut fix = GnssSnapshot::SEARCHING;
1441 for key in [
1442 prop::GNSS_FIX,
1443 prop::GNSS_LOCATION,
1444 prop::GNSS_ALTITUDE,
1445 prop::GNSS_PRECISION,
1446 prop::GNSS_SATELLITES,
1447 ] {
1448 let value = self.get_prop(key).await?;
1449 fix.absorb(key, &value)
1450 .map_err(|_| UlcpError::Protocol("malformed PROP_GNSS_* value"))?;
1451 }
1452 let ident_update = decode_bool(
1453 &self.get_prop(prop::GNSS_IDENT_UPDATE).await?,
1454 "PROP_GNSS_IDENT_UPDATE",
1455 )?;
1456 let ident_precision = match self.get_prop(prop::GNSS_IDENT_PRECISION).await?[..] {
1457 [precision] => precision,
1458 _ => return Err(UlcpError::Protocol("malformed PROP_GNSS_IDENT_PRECISION")),
1459 };
1460 let time_trust = decode_bool(
1461 &self.get_prop(prop::GNSS_TIME_TRUST).await?,
1462 "PROP_GNSS_TIME_TRUST",
1463 )?;
1464 Ok(Some(GnssStatus {
1465 enabled,
1466 fix,
1467 ident_update,
1468 ident_precision,
1469 time_trust,
1470 }))
1471 }
1472
1473 pub async fn advert_policy(&mut self) -> Result<Option<AdvertPolicy>, UlcpError> {
1476 if !self.capabilities().await?.contains(&cap::ADVERT) {
1477 return Ok(None);
1478 }
1479 let advert_interval_s = self.get_interval(prop::ADVERT_INTERVAL).await?;
1480 let beacon_interval_s = self.get_interval(prop::BEACON_INTERVAL).await?;
1481 let startup_beacon = decode_bool(
1482 &self.get_prop(prop::STARTUP_BEACON).await?,
1483 "PROP_STARTUP_BEACON",
1484 )?;
1485 Ok(Some(AdvertPolicy {
1486 advert_interval_s,
1487 beacon_interval_s,
1488 startup_beacon,
1489 }))
1490 }
1491
1492 async fn get_interval(&mut self, key: u32) -> Result<u32, UlcpError> {
1493 decode_interval(&self.get_prop(key).await?)
1494 }
1495
1496 pub async fn set_advert_interval(&mut self, seconds: u32) -> Result<u32, UlcpError> {
1499 let authoritative = self
1500 .set_prop(prop::ADVERT_INTERVAL, &seconds.to_le_bytes())
1501 .await?;
1502 decode_interval(&authoritative)
1503 }
1504
1505 pub async fn set_beacon_interval(&mut self, seconds: u32) -> Result<u32, UlcpError> {
1508 let authoritative = self
1509 .set_prop(prop::BEACON_INTERVAL, &seconds.to_le_bytes())
1510 .await?;
1511 decode_interval(&authoritative)
1512 }
1513
1514 pub async fn set_startup_beacon(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1516 let authoritative = self
1517 .set_prop(prop::STARTUP_BEACON, &[enabled as u8])
1518 .await?;
1519 decode_bool(&authoritative, "PROP_STARTUP_BEACON")
1520 }
1521
1522 pub async fn set_gnss_enabled(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1524 let authoritative = self.set_prop(prop::GNSS_ENABLED, &[enabled as u8]).await?;
1525 decode_bool(&authoritative, "PROP_GNSS_ENABLED")
1526 }
1527
1528 pub async fn set_gnss_ident_update(&mut self, enabled: bool) -> Result<bool, UlcpError> {
1531 let authoritative = self
1532 .set_prop(prop::GNSS_IDENT_UPDATE, &[enabled as u8])
1533 .await?;
1534 decode_bool(&authoritative, "PROP_GNSS_IDENT_UPDATE")
1535 }
1536
1537 pub async fn set_gnss_ident_precision(&mut self, precision: u8) -> Result<u8, UlcpError> {
1540 let authoritative = self
1541 .set_prop(prop::GNSS_IDENT_PRECISION, &[precision])
1542 .await?;
1543 match authoritative[..] {
1544 [stored] => Ok(stored),
1545 _ => Err(UlcpError::Protocol("malformed PROP_GNSS_IDENT_PRECISION")),
1546 }
1547 }
1548
1549 pub async fn set_gnss_time_trust(&mut self, trust: bool) -> Result<bool, UlcpError> {
1555 let authoritative = self.set_prop(prop::GNSS_TIME_TRUST, &[trust as u8]).await?;
1556 decode_bool(&authoritative, "PROP_GNSS_TIME_TRUST")
1557 }
1558
1559 async fn initialize(&mut self) -> Result<(), UlcpError> {
1560 let boot_status = self.get_prop(prop::LAST_STATUS).await?;
1564 self.boot_status = decode_status(&boot_status);
1565
1566 let mut buf = [0u8; 2];
1569 let len = frame::reset(&mut buf, TID_UNSOLICITED)
1570 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1571 self.send(&buf[..len]).await?;
1572 let deadline = Instant::now() + self.config.response_timeout;
1573 self.wait_reset(deadline).await?;
1574
1575 let version = self.get_prop(prop::PROTOCOL_VERSION).await?;
1577 if version.first().copied() != Some(ids::PROTOCOL_MAJOR_VERSION) {
1578 return Err(UlcpError::Protocol("protocol major version mismatch"));
1579 }
1580
1581 let dev_version = self.get_prop(prop::DEV_VERSION).await?;
1582 self.dev_version = String::from_utf8_lossy(&dev_version)
1583 .trim_end_matches('\0')
1584 .to_owned();
1585
1586 let mtu = self.get_prop(prop::PHY_MTU).await?;
1587 let [mtu_lo, mtu_hi, ..] = mtu[..] else {
1588 return Err(UlcpError::Protocol("malformed PROP_PHY_MTU"));
1589 };
1590 self.max_frame_size = usize::from(u16::from_le_bytes([mtu_lo, mtu_hi]));
1591 if self.max_frame_size == 0 {
1592 return Err(UlcpError::Protocol("device advertised zero MTU"));
1593 }
1594
1595 let config = self.config.clone();
1596 self.set_prop(prop::PHY_FREQ, &config.freq_khz.to_le_bytes())
1597 .await?;
1598 self.set_prop(prop::PHY_LORA_BW, &config.bandwidth_hz.to_le_bytes())
1599 .await?;
1600 self.set_prop(prop::PHY_LORA_SF, &[config.spreading_factor])
1601 .await?;
1602 self.set_prop(prop::PHY_LORA_CR, &[config.coding_rate_denom])
1603 .await?;
1604 self.set_prop(prop::PHY_TX_POWER, &[config.tx_power_dbm as u8])
1605 .await?;
1606 self.set_prop(prop::PHY_LORA_SW, &config.sync_word.to_le_bytes())
1607 .await?;
1608 self.set_prop(prop::PHY_ENABLED, &[1]).await?;
1609
1610 self.t_frame_ms = lora_airtime_ms(
1611 config.spreading_factor,
1612 config.bandwidth_hz,
1613 config.coding_rate_denom,
1614 self.max_frame_size,
1615 )
1616 .max(1);
1617 Ok(())
1618 }
1619
1620 pub async fn get_prop(&mut self, key: u32) -> Result<Vec<u8>, UlcpError> {
1622 let tid = self.alloc_tid();
1623 let mut buf = [0u8; 8];
1624 let len =
1625 frame::prop_get(&mut buf, tid, key).map_err(|_| UlcpError::Protocol("frame encode"))?;
1626 self.send(&buf[..len]).await?;
1627 self.finish_prop_transaction(tid, key, PropResponsePolicy::Value)
1628 .await
1629 }
1630
1631 pub async fn set_prop(&mut self, key: u32, value: &[u8]) -> Result<Vec<u8>, UlcpError> {
1634 self.require_tethered(key)?;
1635 let tid = self.alloc_tid();
1636 let mut buf = vec![0u8; value.len() + 8];
1637 let len = frame::prop_set(&mut buf, tid, key, value)
1638 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1639 self.send(&buf[..len]).await?;
1640 self.finish_prop_transaction(tid, key, PropResponsePolicy::Value)
1641 .await
1642 }
1643
1644 pub async fn insert_prop_item(&mut self, key: u32, item: &[u8]) -> Result<Vec<u8>, UlcpError> {
1652 self.require_tethered(key)?;
1653 let tid = self.alloc_tid();
1654 let mut buf = vec![0u8; item.len() + 8];
1655 let len = frame::prop_insert(&mut buf, tid, key, item)
1656 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1657 self.send(&buf[..len]).await?;
1658 self.finish_table_transaction(tid, key, ResponseKind::Inserted)
1659 .await
1660 }
1661
1662 pub async fn remove_prop_item(
1669 &mut self,
1670 key: u32,
1671 selector: &[u8],
1672 ) -> Result<Vec<u8>, UlcpError> {
1673 self.require_tethered(key)?;
1674 let tid = self.alloc_tid();
1675 let mut buf = vec![0u8; selector.len() + 8];
1676 let len = frame::prop_remove(&mut buf, tid, key, selector)
1677 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1678 self.send(&buf[..len]).await?;
1679 self.finish_table_transaction(tid, key, ResponseKind::Removed)
1680 .await
1681 }
1682
1683 async fn status_only_command(
1686 &mut self,
1687 encode: fn(&mut [u8], u8) -> Result<usize, frame::WriteError>,
1688 ) -> Result<(), UlcpError> {
1689 let tid = self.alloc_tid();
1690 let mut buf = [0u8; 4];
1691 let len = encode(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
1692 self.send(&buf[..len]).await?;
1693 self.finish_prop_transaction(tid, prop::LAST_STATUS, PropResponsePolicy::StatusOnly)
1694 .await
1695 .map(|_| ())
1696 }
1697
1698 pub async fn queue_drain(&mut self) -> Result<(), UlcpError> {
1704 self.queue_drain_with(|_data, _meta| {}).await
1705 }
1706
1707 pub async fn queue_drain_with(
1715 &mut self,
1716 mut on_frame: impl FnMut(&[u8], &[u8]),
1717 ) -> Result<(), UlcpError> {
1718 let tid = self.alloc_tid();
1719 let mut buf = [0u8; 4];
1720 let len =
1721 frame::queue_drain(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
1722 self.send(&buf[..len]).await?;
1723
1724 let deadline = Instant::now() + self.config.response_timeout;
1725 loop {
1726 while let Some(response) = self.responses.pop_front() {
1727 if response.tid != tid {
1728 continue;
1729 }
1730 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
1731 let status = decode_status(&response.value);
1732 return if status == Status::OK {
1733 Ok(())
1734 } else {
1735 Err(UlcpError::Status(status))
1736 };
1737 }
1738 return Err(UlcpError::Protocol("unexpected drain response"));
1739 }
1740 if let Some(status) = self.seen_reset.take() {
1741 return Err(UlcpError::UnexpectedReset(status));
1742 }
1743 if self.read_more(deadline).await? {
1747 let packet = self
1748 .rx_queue
1749 .back()
1750 .expect("read_more reported a queued frame");
1751 on_frame(&packet.data, &packet.raw_meta);
1752 }
1753 }
1754 }
1755
1756 pub async fn save(&mut self) -> Result<(), UlcpError> {
1759 self.status_only_command(frame::save).await
1760 }
1761
1762 pub async fn clear(&mut self) -> Result<(), UlcpError> {
1765 self.status_only_command(frame::clear).await
1766 }
1767
1768 pub async fn reset(&mut self) -> Result<Status, UlcpError> {
1774 let mut buf = [0u8; 2];
1775 let len = frame::reset(&mut buf, TID_UNSOLICITED)
1776 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1777 self.send(&buf[..len]).await?;
1778 let deadline = Instant::now() + self.config.response_timeout;
1779 self.wait_reset(deadline).await
1780 }
1781
1782 pub async fn factory_reset(&mut self) -> Result<(), UlcpError> {
1791 let mut buf = [0u8; 2];
1792 let len = frame::factory_reset(&mut buf, TID_UNSOLICITED)
1793 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1794 self.send(&buf[..len]).await?;
1795 Ok(())
1796 }
1797
1798 pub async fn restore(&mut self) -> Result<RestoreCompletion, UlcpError> {
1801 let tid = self.alloc_tid();
1802 let mut buf = [0u8; 4];
1803 let len = frame::restore(&mut buf, tid).map_err(|_| UlcpError::Protocol("frame encode"))?;
1804 self.send(&buf[..len]).await?;
1805
1806 let deadline = Instant::now() + self.config.response_timeout;
1807 loop {
1808 while let Some(response) = self.responses.pop_front() {
1809 if response.tid != tid {
1810 continue;
1811 }
1812 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
1813 let status = decode_status(&response.value);
1814 return if status == Status::OK {
1815 Ok(RestoreCompletion::Updated)
1816 } else {
1817 Err(UlcpError::Status(status))
1818 };
1819 }
1820 return Err(UlcpError::Protocol("unexpected restore response"));
1821 }
1822 match self.seen_reset.take() {
1823 Some(status) if status == Status::RESET_RESTORED => {
1824 return Ok(RestoreCompletion::Reset);
1825 }
1826 Some(status) => return Err(UlcpError::UnexpectedReset(status)),
1827 None => {}
1828 }
1829 self.read_more(deadline).await?;
1830 }
1831 }
1832
1833 pub async fn set_ble_pairing_pin(&mut self, pin: Option<u32>) -> Result<(), UlcpError> {
1838 if pin.is_some_and(|pin| pin > 999_999) {
1839 return Err(UlcpError::Protocol("BLE pairing PIN out of range"));
1840 }
1841 let tid = self.alloc_tid();
1842 let value = pin.map(u32::to_le_bytes);
1843 let mut buf = [0u8; 12];
1844 let len = frame::prop_set(
1845 &mut buf,
1846 tid,
1847 prop::BLE_PAIRING_PIN,
1848 value.as_ref().map_or(&[], |value| &value[..]),
1849 )
1850 .map_err(|_| UlcpError::Protocol("frame encode"))?;
1851 self.send(&buf[..len]).await?;
1852 self.finish_prop_transaction(tid, prop::BLE_PAIRING_PIN, PropResponsePolicy::StatusOnly)
1853 .await
1854 .map(|_| ())
1855 }
1856
1857 pub async fn capabilities(&mut self) -> Result<Vec<u32>, UlcpError> {
1859 let raw = self.get_prop(prop::CAPS).await?;
1860 let mut caps = Vec::new();
1861 let mut offset = 0;
1862 while offset < raw.len() {
1863 let (value, used) = pui::decode(&raw[offset..])
1864 .map_err(|_| UlcpError::Protocol("malformed PROP_CAPS"))?;
1865 caps.push(value);
1866 offset += used;
1867 }
1868 Ok(caps)
1869 }
1870
1871 pub async fn sync(
1881 &mut self,
1882 expected_host_key: Option<&[u8; 32]>,
1883 ) -> Result<DeviceSync, UlcpError> {
1884 let last_status = decode_status(&self.get_prop(prop::LAST_STATUS).await?);
1887 let capabilities = self.capabilities().await?;
1888 let has = |capability: u32| capabilities.contains(&capability);
1889
1890 let (host_key, ownership) = if has(cap::HOST_FILTER) {
1892 let value = self.get_prop(prop::HOST_KEY).await?;
1893 match <[u8; 32]>::try_from(value.as_slice()) {
1894 Ok(key) => {
1895 let ownership = match expected_host_key {
1896 Some(expected) if *expected == key => HostOwnership::Ours,
1897 _ => HostOwnership::OtherHost(key),
1898 };
1899 (Some(key), ownership)
1900 }
1901 Err(_) if value.is_empty() => (None, HostOwnership::Unclaimed),
1902 Err(_) => return Err(UlcpError::Protocol("malformed PROP_HOST_KEY")),
1903 }
1904 } else {
1905 (None, HostOwnership::Unsupported)
1906 };
1907
1908 let phy_enabled = self.get_prop(prop::PHY_ENABLED).await? == [1];
1911 let freq = self.get_prop(prop::PHY_FREQ).await?;
1912 let freq_khz = u32::from_le_bytes(
1913 freq.as_slice()
1914 .try_into()
1915 .map_err(|_| UlcpError::Protocol("malformed PROP_PHY_FREQ"))?,
1916 );
1917 let device_name = self.device_name().await?;
1918 let saved = match has(cap::SAVE) {
1919 true => Some(SavedSnapshot::from_octet(
1920 &self.get_prop(prop::SAVED).await?,
1921 )?),
1922 false => None,
1923 };
1924 let (queue_count, queue_dropped) = if has(cap::HOST_RX_QUEUE) {
1925 let count = self.get_prop(prop::HOST_RX_QUEUE_COUNT).await?;
1926 let dropped = self.get_prop(prop::HOST_RX_QUEUE_DROPPED).await?;
1927 (
1928 Some(u16::from_le_bytes(count.as_slice().try_into().map_err(
1929 |_| UlcpError::Protocol("malformed PROP_HOST_RX_QUEUE_COUNT"),
1930 )?)),
1931 Some(u32::from_le_bytes(dropped.as_slice().try_into().map_err(
1932 |_| UlcpError::Protocol("malformed PROP_HOST_RX_QUEUE_DROPPED"),
1933 )?)),
1934 )
1935 } else {
1936 (None, None)
1937 };
1938 let filters = match has(cap::HOST_FILTER) {
1939 true => Some(decode_filter_table(
1940 &self.get_prop(prop::HOST_RX_FILTERS).await?,
1941 )?),
1942 false => None,
1943 };
1944 let (host_channel_ids, host_peer_keys) = if has(cap::HOST_KEYS) {
1945 (
1946 Some(decode_fixed_list::<{ items::CHANNEL_ID_LEN }>(
1947 &self.get_prop(prop::HOST_CHANNEL_KEYS).await?,
1948 "malformed PROP_HOST_CHANNEL_KEYS digest",
1949 )?),
1950 Some(decode_fixed_list::<{ items::PUBLIC_KEY_LEN }>(
1951 &self.get_prop(prop::HOST_PEER_KEYS).await?,
1952 "malformed PROP_HOST_PEER_KEYS digest",
1953 )?),
1954 )
1955 } else {
1956 (None, None)
1957 };
1958 let auto_ack = match has(cap::HOST_AUTO_ACK) {
1959 true => Some(self.get_prop(prop::HOST_AUTO_ACK).await? == [1]),
1960 false => None,
1961 };
1962 let dev_key = if has(cap::DEV_IDENTITY) {
1963 let value = self.get_prop(prop::DEV_KEY).await?;
1964 match <[u8; 32]>::try_from(value.as_slice()) {
1965 Ok(key) => Some(key),
1966 Err(_) if value.is_empty() => None,
1967 Err(_) => return Err(UlcpError::Protocol("malformed PROP_DEV_KEY")),
1968 }
1969 } else {
1970 None
1971 };
1972
1973 Ok(DeviceSync {
1974 reset_since_last_contact: last_status.is_reset(),
1975 last_status,
1976 capabilities,
1977 ownership,
1978 host_key,
1979 phy_enabled,
1980 freq_khz,
1981 device_name,
1982 saved,
1983 queue_count,
1984 queue_dropped,
1985 filters,
1986 host_channel_ids,
1987 host_peer_keys,
1988 auto_ack,
1989 dev_key,
1990 })
1991 }
1992
1993 pub async fn provision(
2029 &mut self,
2030 desired: &HostProvisioning,
2031 ) -> Result<ProvisionReport, UlcpError> {
2032 self.require_tethered(prop::HOST_KEY)?;
2033 let mut report = ProvisionReport::default();
2034 let current_key = self.get_prop(prop::HOST_KEY).await?;
2035 if current_key.as_slice() != desired.host_key.as_slice() {
2039 report.host_replaced = true;
2040 }
2041 self.set_prop(prop::HOST_KEY, &desired.host_key).await?;
2042
2043 let mut table = Vec::new();
2046 for filter in &desired.filters {
2047 let mut item = [0u8; items::Filter::MAX_WIRE_LEN];
2048 let item_len = filter
2049 .encode(&mut item)
2050 .map_err(|_| UlcpError::Protocol("filter encode"))?;
2051 let mut prefixed = [0u8; items::Filter::MAX_WIRE_LEN + 2];
2052 let prefixed_len = items::encode_prefixed_item(&item[..item_len], &mut prefixed)
2053 .map_err(|_| UlcpError::Protocol("filter encode"))?;
2054 table.extend_from_slice(&prefixed[..prefixed_len]);
2055 }
2056 self.set_prop(prop::HOST_RX_FILTERS, &table).await?;
2057 report.filters_replaced = true;
2058
2059 let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
2064 let desired_ids: Vec<[u8; items::CHANNEL_ID_LEN]> = desired
2065 .channel_keys
2066 .iter()
2067 .map(|key| engine.derive_channel_id(&ChannelKey(*key)).0)
2068 .collect();
2069 let current_ids = if report.host_replaced {
2070 Vec::new()
2071 } else {
2072 decode_fixed_list::<{ items::CHANNEL_ID_LEN }>(
2073 &self.get_prop(prop::HOST_CHANNEL_KEYS).await?,
2074 "malformed PROP_HOST_CHANNEL_KEYS digest",
2075 )?
2076 };
2077 if current_ids.iter().any(|id| !desired_ids.contains(id)) {
2078 let table: Vec<u8> = desired.channel_keys.concat();
2079 self.set_prop(prop::HOST_CHANNEL_KEYS, &table).await?;
2080 report.channels_replaced = true;
2081 } else {
2082 for key in &desired.channel_keys {
2090 match self.insert_prop_item(prop::HOST_CHANNEL_KEYS, key).await {
2091 Ok(_) => report.channels_inserted += 1,
2092 Err(UlcpError::Status(Status::ALREADY)) => {}
2093 Err(error) => return Err(error),
2094 }
2095 }
2096 }
2097
2098 let current_peers = if report.host_replaced {
2103 Vec::new()
2104 } else {
2105 decode_fixed_list::<{ items::PUBLIC_KEY_LEN }>(
2106 &self.get_prop(prop::HOST_PEER_KEYS).await?,
2107 "malformed PROP_HOST_PEER_KEYS digest",
2108 )?
2109 };
2110 for existing in ¤t_peers {
2111 if !desired
2112 .peer_keys
2113 .iter()
2114 .any(|entry| entry.public_key == *existing)
2115 {
2116 self.remove_prop_item(prop::HOST_PEER_KEYS, existing)
2117 .await?;
2118 report.peers_removed += 1;
2119 }
2120 }
2121 for entry in &desired.peer_keys {
2122 let mut item = [0u8; items::PeerKeyEntry::WIRE_LEN];
2123 entry
2124 .encode(&mut item)
2125 .map_err(|_| UlcpError::Protocol("peer entry encode"))?;
2126 self.insert_prop_item(prop::HOST_PEER_KEYS, &item).await?;
2127 report.peers_inserted += 1;
2128 }
2129
2130 self.set_prop(prop::HOST_AUTO_ACK, &[desired.auto_ack as u8])
2132 .await?;
2133 report.auto_ack_changed = true;
2134 Ok(report)
2135 }
2136
2137 pub async fn ensure_device_identity(&mut self) -> Result<[u8; 32], UlcpError> {
2145 let current = self.get_prop(prop::DEV_KEY).await?;
2146 if let Ok(key) = <[u8; 32]>::try_from(current.as_slice()) {
2147 return Ok(key);
2148 }
2149 if !current.is_empty() {
2150 return Err(UlcpError::Protocol("malformed PROP_DEV_KEY"));
2151 }
2152 let tid = self.alloc_tid();
2156 let mut buf = [0u8; 8];
2157 let len = frame::prop_set(&mut buf, tid, prop::DEV_PRIVATE_KEY, &[])
2158 .map_err(|_| UlcpError::Protocol("frame encode"))?;
2159 self.send(&buf[..len]).await?;
2160 let value = self
2161 .finish_prop_transaction(tid, prop::DEV_KEY, PropResponsePolicy::Value)
2162 .await?;
2163 <[u8; 32]>::try_from(value.as_slice())
2164 .map_err(|_| UlcpError::Protocol("malformed PROP_DEV_KEY"))
2165 }
2166
2167 async fn finish_prop_transaction(
2168 &mut self,
2169 tid: u8,
2170 key: u32,
2171 policy: PropResponsePolicy,
2172 ) -> Result<Vec<u8>, UlcpError> {
2173 let deadline = Instant::now() + self.config.response_timeout;
2174 let response = self.wait_response(tid, deadline).await?;
2175 if response.kind != ResponseKind::Is {
2176 return Err(UlcpError::Protocol(
2177 "table notification answering a property command",
2178 ));
2179 }
2180 match (policy, response.key) {
2181 (PropResponsePolicy::Value, response_key) if response_key == key => Ok(response.value),
2182 (PropResponsePolicy::StatusOnly, prop::LAST_STATUS) => {
2183 let status = decode_status(&response.value);
2184 if status == Status::OK {
2185 Ok(Vec::new())
2186 } else {
2187 Err(UlcpError::Status(status))
2188 }
2189 }
2190 (PropResponsePolicy::Value, prop::LAST_STATUS) => {
2191 let status = decode_status(&response.value);
2192 if status == Status::OK {
2193 Err(UlcpError::Protocol(
2194 "unexpected status-only property response",
2195 ))
2196 } else {
2197 Err(UlcpError::Status(status))
2198 }
2199 }
2200 _ => Err(UlcpError::Protocol("response for unexpected property")),
2201 }
2202 }
2203
2204 async fn finish_table_transaction(
2208 &mut self,
2209 tid: u8,
2210 key: u32,
2211 expected: ResponseKind,
2212 ) -> Result<Vec<u8>, UlcpError> {
2213 let deadline = Instant::now() + self.config.response_timeout;
2214 let response = self.wait_response(tid, deadline).await?;
2215 match (response.kind, response.key) {
2216 (kind, response_key) if kind == expected && response_key == key => Ok(response.value),
2217 (ResponseKind::Is, prop::LAST_STATUS) => {
2218 let status = decode_status(&response.value);
2219 if status == Status::OK {
2220 Err(UlcpError::Protocol(
2221 "status-only success for a table mutation",
2222 ))
2223 } else {
2224 Err(UlcpError::Status(status))
2225 }
2226 }
2227 _ => Err(UlcpError::Protocol("response for unexpected property")),
2228 }
2229 }
2230
2231 fn alloc_tid(&mut self) -> u8 {
2232 self.tids.allocate()
2233 }
2234
2235 fn ingest_frame(&mut self, frame_bytes: &[u8]) -> bool {
2240 if let Some(trace) = &mut self.trace {
2241 trace(TraceDirection::DeviceToHost, &describe_frame(frame_bytes));
2242 }
2243 let Ok(frame) = Frame::parse(frame_bytes) else {
2244 return false;
2245 };
2246 match frame.command() {
2247 Some(Cmd::StrRecv) => {
2248 let Ok(payload) = StreamPayload::parse(frame.payload) else {
2249 return false;
2250 };
2251 if payload.stream != stream::PHY_RAW {
2252 return false;
2253 }
2254 let meta = RxMeta::decode(payload.metadata).unwrap_or_default();
2255 if self.rx_queue.len() >= RX_QUEUE_DEPTH {
2256 self.rx_queue.pop_front();
2257 }
2258 self.rx_queue.push_back(RxPacket {
2259 data: payload.data.to_vec(),
2260 meta,
2261 raw_meta: payload.metadata.to_vec(),
2262 });
2263 return true;
2264 }
2265 Some(Cmd::PropIs) => self.ingest_prop_notification(ResponseKind::Is, &frame),
2266 Some(Cmd::PropInserted) => {
2267 self.ingest_prop_notification(ResponseKind::Inserted, &frame)
2268 }
2269 Some(Cmd::PropRemoved) => self.ingest_prop_notification(ResponseKind::Removed, &frame),
2270 _ => {}
2271 }
2272 false
2273 }
2274
2275 fn ingest_prop_notification(&mut self, kind: ResponseKind, frame: &Frame<'_>) {
2276 let Ok(notification) = PropertyNotification::from_frame(frame) else {
2277 return;
2278 };
2279 if notification.kind != kind {
2282 return;
2283 }
2284 let tid = notification.tid;
2285 if tid != TID_UNSOLICITED {
2286 if self.responses.len() >= RESPONSE_QUEUE_DEPTH {
2287 self.responses.pop_front();
2288 }
2289 self.responses.push_back(Response {
2290 tid,
2291 kind,
2292 key: notification.key,
2293 value: notification.value.to_vec(),
2294 });
2295 return;
2296 }
2297 if kind == ResponseKind::Is && notification.key == prop::LAST_STATUS {
2300 let status = decode_status(notification.value);
2301 if status.is_reset() {
2302 self.seen_reset = Some(status);
2303 }
2304 return;
2305 }
2306 let event = match kind {
2307 ResponseKind::Is => PropEvent::Is {
2308 key: notification.key,
2309 value: notification.value.to_vec(),
2310 },
2311 ResponseKind::Inserted => PropEvent::Inserted {
2312 key: notification.key,
2313 digest: notification.value.to_vec(),
2314 },
2315 ResponseKind::Removed => PropEvent::Removed {
2316 key: notification.key,
2317 digest: notification.value.to_vec(),
2318 },
2319 };
2320 if self.prop_events.len() >= PROP_EVENT_DEPTH {
2321 self.prop_events.pop_front();
2322 }
2323 self.prop_events.push_back(event);
2324 }
2325
2326 pub fn pop_prop_event(&mut self) -> Option<PropEvent> {
2331 self.prop_events.pop_front()
2332 }
2333
2334 async fn wait_response(&mut self, tid: u8, deadline: Instant) -> Result<Response, UlcpError> {
2338 loop {
2339 while let Some(response) = self.responses.pop_front() {
2344 if response.tid == tid {
2345 return Ok(response);
2346 }
2347 }
2350 if let Some(status) = self.seen_reset.take() {
2351 return Err(UlcpError::UnexpectedReset(status));
2352 }
2353 self.read_more(deadline).await?;
2354 }
2355 }
2356
2357 async fn wait_reset(&mut self, deadline: Instant) -> Result<Status, UlcpError> {
2359 loop {
2360 if let Some(status) = self.seen_reset.take() {
2361 return Ok(status);
2362 }
2363 while let Some(response) = self.responses.pop_front() {
2365 if response.kind == ResponseKind::Is && response.key == prop::LAST_STATUS {
2366 let status = decode_status(&response.value);
2367 if status.is_reset() {
2368 return Ok(status);
2369 }
2370 }
2371 }
2372 self.read_more(deadline).await?;
2373 }
2374 }
2375
2376 async fn read_more(&mut self, deadline: Instant) -> Result<bool, UlcpError> {
2379 let now = Instant::now();
2380 if now >= deadline {
2381 return Err(UlcpError::Timeout);
2382 }
2383 let frame = match tokio::time::timeout(deadline - now, self.link.recv_frame()).await {
2384 Err(_elapsed) => return Err(UlcpError::Timeout),
2385 Ok(Err(error)) => return Err(error),
2386 Ok(Ok(frame)) => frame,
2387 };
2388 Ok(self.ingest_frame(&frame))
2389 }
2390
2391 fn pop_rx(&mut self, buf: &mut [u8]) -> Option<RxInfo> {
2392 let packet = self.rx_queue.pop_front()?;
2393 let len = packet.data.len().min(buf.len());
2394 buf[..len].copy_from_slice(&packet.data[..len]);
2395 Some(RxInfo {
2396 len,
2397 rssi: packet.meta.rssi_dbm.unwrap_or(0),
2398 snr: Snr::from_centibels(packet.meta.snr_cb.unwrap_or(0)),
2399 lqi: packet.meta.lqi,
2400 })
2401 }
2402
2403 async fn send_confirmed(
2407 &mut self,
2408 data: &[u8],
2409 metadata: &[u8],
2410 cca_deadline: Option<Instant>,
2411 ) -> Result<(), TxError<UlcpError>> {
2412 loop {
2413 let tid = self.alloc_tid();
2414 let mut frame_buf = vec![0u8; data.len() + metadata.len() + 16];
2415 let frame_len = frame::str_send(&mut frame_buf, tid, stream::PHY_RAW, data, metadata)
2416 .map_err(|_| TxError::Io(UlcpError::Protocol("frame encode")))?;
2417 self.send(&frame_buf[..frame_len])
2418 .await
2419 .map_err(TxError::Io)?;
2420
2421 let deadline = Instant::now()
2424 + self.config.response_timeout
2425 + Duration::from_millis(u64::from(self.t_frame_ms) * 2);
2426 let response = self
2427 .wait_response(tid, deadline)
2428 .await
2429 .map_err(TxError::Io)?;
2430 if response.kind != ResponseKind::Is || response.key != prop::LAST_STATUS {
2431 return Err(TxError::Io(UlcpError::Protocol(
2432 "unexpected transmit response",
2433 )));
2434 }
2435 match decode_status(&response.value) {
2436 Status::OK => return Ok(()),
2437 Status::CCA_FAILURE => match cca_deadline {
2438 Some(deadline) if Instant::now() < deadline => {
2439 tokio::time::sleep(CCA_RETRY_DELAY).await;
2440 }
2441 _ => return Err(TxError::CadTimeout),
2442 },
2443 status => return Err(TxError::Io(UlcpError::Status(status))),
2444 }
2445 }
2446 }
2447
2448 pub async fn transmit_raw_with_meta(
2462 &mut self,
2463 data: &[u8],
2464 metadata: &[u8],
2465 ) -> Result<(), TxError<UlcpError>> {
2466 if data.len() > self.max_frame_size {
2467 return Err(TxError::Io(UlcpError::FrameTooLarge(data.len())));
2468 }
2469 let skips_cca = metadata
2470 .get(1)
2471 .is_some_and(|flags| flags & TX_FLAG_NOCCA != 0);
2472 let cca_deadline = (!skips_cca).then(Instant::now);
2473 self.send_confirmed(data, metadata, cca_deadline).await
2474 }
2475
2476 pub fn poll_receive_raw(
2484 &mut self,
2485 cx: &mut core::task::Context<'_>,
2486 ) -> core::task::Poll<Result<RawRxFrame, UlcpError>> {
2487 loop {
2488 if let Some(status) = self.seen_reset.take() {
2489 return core::task::Poll::Ready(Err(UlcpError::UnexpectedReset(status)));
2490 }
2491 if let Some(packet) = self.rx_queue.pop_front() {
2492 return core::task::Poll::Ready(Ok(RawRxFrame {
2493 data: packet.data,
2494 metadata: packet.raw_meta,
2495 }));
2496 }
2497
2498 match self.link.poll_recv_frame(cx) {
2499 core::task::Poll::Ready(Ok(frame)) => {
2500 self.ingest_frame(&frame);
2501 }
2502 core::task::Poll::Ready(Err(error)) => return core::task::Poll::Ready(Err(error)),
2503 core::task::Poll::Pending => return core::task::Poll::Pending,
2504 }
2505 }
2506 }
2507
2508 pub async fn receive_raw(&mut self) -> Result<RawRxFrame, UlcpError> {
2513 core::future::poll_fn(|cx| self.poll_receive_raw(cx)).await
2514 }
2515}
2516
2517#[derive(Clone, Debug)]
2520pub struct RawRxFrame {
2521 pub data: Vec<u8>,
2522 pub metadata: Vec<u8>,
2523}
2524
2525#[cfg(feature = "serial-radio")]
2526impl UlcpDevice<SerialFrameLink<tokio_serial::SerialStream>> {
2527 pub async fn open_serial(
2529 path: impl AsRef<str>,
2530 baud_rate: u32,
2531 config: UlcpDeviceConfig,
2532 ) -> Result<Self, UlcpError> {
2533 use tokio_serial::SerialPortBuilderExt;
2534
2535 let stream = tokio_serial::new(path.as_ref(), baud_rate)
2536 .open_native_async()
2537 .map_err(|error| UlcpError::Io(error.into()))?;
2538 Self::new(SerialFrameLink::new(stream), config).await
2539 }
2540}
2541
2542#[cfg(feature = "ble-radio")]
2543impl UlcpDevice<BleFrameLink> {
2544 pub async fn open_ble(
2546 selector: Option<&str>,
2547 config: UlcpDeviceConfig,
2548 ) -> Result<Self, UlcpError> {
2549 Self::open_ble_with_link_config(selector, config, BleFrameLinkConfig::default()).await
2550 }
2551
2552 pub async fn open_ble_with_link_config(
2554 selector: Option<&str>,
2555 config: UlcpDeviceConfig,
2556 link_config: BleFrameLinkConfig,
2557 ) -> Result<Self, UlcpError> {
2558 let link = BleFrameLink::connect(selector, link_config).await?;
2559 Self::new(link, config).await
2560 }
2561}
2562
2563impl<L> Radio for UlcpDevice<L>
2564where
2565 L: FrameLink,
2566{
2567 type Error = UlcpError;
2568
2569 async fn transmit(
2583 &mut self,
2584 data: &[u8],
2585 options: TxOptions,
2586 ) -> Result<(), TxError<Self::Error>> {
2587 if data.len() > self.max_frame_size {
2588 return Err(TxError::Io(UlcpError::FrameTooLarge(data.len())));
2589 }
2590
2591 let mut meta = TxMeta::default();
2594 let cca_deadline = match options.cad {
2595 CadPolicy::Skip => {
2596 meta.flags |= TX_FLAG_NOCCA;
2597 None
2598 }
2599 CadPolicy::Gate => Some(Instant::now()),
2602 CadPolicy::RetryFor { timeout_ms } => {
2603 Some(Instant::now() + Duration::from_millis(timeout_ms.into()))
2604 }
2605 };
2606 let mut meta_buf = [0u8; TxMeta::WIRE_LEN];
2607 let meta_len = meta
2608 .encode(&mut meta_buf)
2609 .expect("buffer sized with WIRE_LEN");
2610
2611 self.send_confirmed(data, &meta_buf[..meta_len], cca_deadline)
2612 .await
2613 }
2614
2615 fn poll_receive(
2616 &mut self,
2617 cx: &mut core::task::Context<'_>,
2618 buf: &mut [u8],
2619 ) -> core::task::Poll<Result<RxInfo, Self::Error>> {
2620 loop {
2621 if let Some(status) = self.seen_reset.take() {
2622 return core::task::Poll::Ready(Err(UlcpError::UnexpectedReset(status)));
2623 }
2624 if let Some(info) = self.pop_rx(buf) {
2625 return core::task::Poll::Ready(Ok(info));
2626 }
2627
2628 match self.link.poll_recv_frame(cx) {
2629 core::task::Poll::Ready(Ok(frame)) => {
2630 self.ingest_frame(&frame);
2631 }
2632 core::task::Poll::Ready(Err(error)) => return core::task::Poll::Ready(Err(error)),
2633 core::task::Poll::Pending => return core::task::Poll::Pending,
2634 }
2635 }
2636 }
2637
2638 fn max_frame_size(&self) -> usize {
2639 self.max_frame_size
2640 }
2641
2642 fn t_frame_ms(&self) -> u32 {
2643 self.t_frame_ms
2644 }
2645}
2646
2647fn decode_status(value: &[u8]) -> Status {
2648 match pui::decode(value) {
2649 Ok((code, _)) => Status(code),
2650 Err(_) => Status::FAILURE,
2651 }
2652}
2653
2654fn decode_filter_table(value: &[u8]) -> Result<Vec<items::Filter>, UlcpError> {
2657 let mut filters = Vec::new();
2658 for item in items::prefixed_items(value) {
2659 let item = item.map_err(|_| UlcpError::Protocol("malformed PROP_HOST_RX_FILTERS"))?;
2660 filters.push(
2661 items::Filter::decode(item)
2662 .map_err(|_| UlcpError::Protocol("malformed PROP_HOST_RX_FILTERS"))?,
2663 );
2664 }
2665 Ok(filters)
2666}
2667
2668fn decode_region_list(value: &[u8]) -> Result<Vec<RegionCode>, UlcpError> {
2671 if value.len() % 2 != 0 {
2672 return Err(UlcpError::Protocol("malformed PROP_MAC_REPEATER_REGIONS"));
2673 }
2674 Ok(value
2675 .chunks_exact(2)
2676 .map(|code| RegionCode::from_bytes([code[0], code[1]]))
2677 .collect())
2678}
2679
2680fn decode_region_code(value: &[u8]) -> Result<Option<RegionCode>, UlcpError> {
2682 match value {
2683 [] => Ok(None),
2684 [high, low] => Ok(Some(RegionCode::from_bytes([*high, *low]))),
2685 _ => Err(UlcpError::Protocol(
2686 "malformed PROP_MAC_REPEATER_DEFAULT_REGION",
2687 )),
2688 }
2689}
2690
2691fn decode_opt_i16(value: &[u8]) -> Option<Option<i16>> {
2693 match value {
2694 [] => Some(None),
2695 [low, high] => Some(Some(i16::from_le_bytes([*low, *high]))),
2696 _ => None,
2697 }
2698}
2699
2700fn decode_opt_i8(value: &[u8]) -> Option<Option<i8>> {
2702 match value {
2703 [] => Some(None),
2704 [byte] => Some(Some(*byte as i8)),
2705 _ => None,
2706 }
2707}
2708
2709fn decode_alert(value: &[u8]) -> Result<AlertState, UlcpError> {
2711 const MALFORMED: &str = "malformed PROP_ALERT";
2712 let (code, consumed) = pui::decode(value).map_err(|_| UlcpError::Protocol(MALFORMED))?;
2713 if consumed != value.len() {
2714 return Err(UlcpError::Protocol(MALFORMED));
2715 }
2716 AlertState::from_code(code).ok_or(UlcpError::Protocol(MALFORMED))
2717}
2718
2719fn decode_epoch(value: &[u8]) -> Result<Option<u32>, UlcpError> {
2722 match value {
2723 [] => Ok(None),
2724 [a, b, c, d] => Ok(Some(u32::from_le_bytes([*a, *b, *c, *d]))),
2725 _ => Err(UlcpError::Protocol("malformed PROP_TIME")),
2726 }
2727}
2728
2729fn decode_tz_offset(value: &[u8]) -> Result<i16, UlcpError> {
2731 match value {
2732 [low, high] => Ok(i16::from_le_bytes([*low, *high])),
2733 _ => Err(UlcpError::Protocol("malformed PROP_TZ_OFFSET")),
2734 }
2735}
2736
2737fn decode_bool(value: &[u8], what: &'static str) -> Result<bool, UlcpError> {
2740 match value {
2741 [0] => Ok(false),
2742 [1] => Ok(true),
2743 _ => Err(UlcpError::Protocol(what)),
2744 }
2745}
2746
2747fn decode_interval(value: &[u8]) -> Result<u32, UlcpError> {
2749 match value {
2750 [a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
2751 _ => Err(UlcpError::Protocol("malformed announcement interval")),
2752 }
2753}
2754
2755fn decode_fixed_list<const N: usize>(
2757 value: &[u8],
2758 what: &'static str,
2759) -> Result<Vec<[u8; N]>, UlcpError> {
2760 items::fixed_items::<N>(value)
2761 .map(|iterator| iterator.copied().collect())
2762 .map_err(|_| UlcpError::Protocol(what))
2763}
2764
2765pub fn describe_frame(bytes: &[u8]) -> String {
2770 umsh_ulcp::FrameDescription(bytes).to_string()
2771}
2772
2773#[cfg(test)]
2774mod tests {
2775 use super::*;
2776 use std::collections::HashMap;
2777 use tokio::io::{AsyncReadExt, DuplexStream};
2778 use umsh_ulcp::PropPayload;
2779 use umsh_ulcp::meta::{BufferedRxMeta, RX_FLAG_BUFFERED};
2780
2781 const CCA_FAIL: &[u8] = b"cca-fail";
2783 const RESET_AFTER: &[u8] = b"reset-after";
2786 const RESTORE_RESET_FORM_KEY: u32 = 59_999;
2789
2790 async fn fake_device(mut io: DuplexStream) {
2794 let mut decoder = hdlc::Decoder::<WIRE_BUF>::new();
2795 let mut props: HashMap<u32, Vec<u8>> = HashMap::new();
2796 let mut tables: HashMap<u32, Vec<Vec<u8>>> = HashMap::new();
2797 let mut chunk = [0u8; READ_CHUNK];
2798 loop {
2799 let read = match io.read(&mut chunk).await {
2800 Ok(0) | Err(_) => return,
2801 Ok(read) => read,
2802 };
2803 let mut replies: Vec<Vec<u8>> = Vec::new();
2804 for &byte in &chunk[..read] {
2805 let Some(Ok(frame_bytes)) = decoder.push(byte) else {
2806 continue;
2807 };
2808 let frame = Frame::parse(frame_bytes).expect("host sent malformed frame");
2809 let tid = frame.header.tid();
2810 let mut buf = vec![0u8; 512];
2811 match frame.command().expect("host sent unknown command") {
2812 Cmd::Reset => {
2813 let len =
2814 frame::last_status(&mut buf, TID_UNSOLICITED, Status::RESET_SOFTWARE)
2815 .unwrap();
2816 replies.push(buf[..len].to_vec());
2817 }
2818 Cmd::PropGet => {
2819 let key = PropPayload::parse(frame.payload).unwrap().key;
2820 let value: Vec<u8> = match key {
2821 prop::LAST_STATUS => vec![Status::RESET_POWER_ON.0 as u8],
2822 prop::PROTOCOL_VERSION => {
2823 vec![ids::PROTOCOL_MAJOR_VERSION, ids::PROTOCOL_MINOR_VERSION]
2824 }
2825 prop::DEV_VERSION => b"fake-dev/0.1\0".to_vec(),
2826 prop::PHY_MTU => 255u16.to_le_bytes().to_vec(),
2827 _ => props.get(&key).cloned().unwrap_or_default(),
2828 };
2829 let len = frame::prop_is(&mut buf, tid, key, &value).unwrap();
2830 replies.push(buf[..len].to_vec());
2831 }
2832 Cmd::PropSet => {
2833 let payload = PropPayload::parse(frame.payload).unwrap();
2834 props.insert(payload.key, payload.value.to_vec());
2835 let len = if payload.key == prop::BLE_PAIRING_PIN {
2836 frame::last_status(&mut buf, tid, Status::OK).unwrap()
2837 } else {
2838 frame::prop_is(&mut buf, tid, payload.key, payload.value).unwrap()
2839 };
2840 replies.push(buf[..len].to_vec());
2841 }
2842 Cmd::StrSend => {
2843 let payload = StreamPayload::parse(frame.payload).unwrap();
2844 assert_eq!(payload.stream, stream::PHY_RAW);
2845 if payload.data == CCA_FAIL {
2846 let len =
2847 frame::last_status(&mut buf, tid, Status::CCA_FAILURE).unwrap();
2848 replies.push(buf[..len].to_vec());
2849 continue;
2850 }
2851 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
2852 replies.push(buf[..len].to_vec());
2853 if payload.data == RESET_AFTER {
2854 let len = frame::last_status(
2855 &mut buf,
2856 TID_UNSOLICITED,
2857 Status::RESET_WATCHDOG,
2858 )
2859 .unwrap();
2860 replies.push(buf[..len].to_vec());
2861 continue;
2862 }
2863 let mut meta = [0u8; RxMeta::WIRE_LEN];
2865 RxMeta {
2866 rssi_dbm: Some(-91),
2867 lqi: None,
2868 snr_cb: Some(55),
2869 }
2870 .encode(&mut meta)
2871 .unwrap();
2872 let len = frame::str_recv(&mut buf, stream::PHY_RAW, payload.data, &meta)
2873 .unwrap();
2874 replies.push(buf[..len].to_vec());
2875 }
2876 Cmd::Nop => {
2877 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
2878 replies.push(buf[..len].to_vec());
2879 }
2880 Cmd::PropInsert => {
2881 let payload = PropPayload::parse(frame.payload).unwrap();
2882 let replaces = payload.key == prop::HOST_PEER_KEYS;
2887 let stored = payload.value.to_vec();
2888 let digest_len = if replaces {
2889 assert_eq!(stored.len(), 64);
2890 32
2891 } else {
2892 stored.len()
2893 };
2894 let table = tables.entry(payload.key).or_default();
2895 let existing = table.iter_mut().find(|item| {
2896 item[..digest_len.min(item.len())] == stored[..digest_len]
2897 });
2898 let len = match existing {
2899 Some(_) if !replaces => {
2900 frame::last_status(&mut buf, tid, Status::ALREADY).unwrap()
2901 }
2902 Some(existing) => {
2903 *existing = stored.clone();
2904 frame::prop_inserted(
2905 &mut buf,
2906 tid,
2907 payload.key,
2908 &stored[..digest_len],
2909 )
2910 .unwrap()
2911 }
2912 None => {
2913 table.push(stored.clone());
2914 frame::prop_inserted(
2915 &mut buf,
2916 tid,
2917 payload.key,
2918 &stored[..digest_len],
2919 )
2920 .unwrap()
2921 }
2922 };
2923 replies.push(buf[..len].to_vec());
2924 }
2925 Cmd::PropRemove => {
2926 let payload = PropPayload::parse(frame.payload).unwrap();
2927 let table = tables.entry(payload.key).or_default();
2928 let position = table.iter().position(|item| {
2929 item[..payload.value.len().min(item.len())] == *payload.value
2930 });
2931 let len = match position {
2932 Some(index) => {
2933 let removed = table.remove(index);
2934 let digest = &removed[..payload.value.len().min(removed.len())];
2935 frame::prop_removed(&mut buf, tid, payload.key, digest).unwrap()
2936 }
2937 None => {
2938 frame::last_status(&mut buf, tid, Status::ITEM_NOT_FOUND).unwrap()
2939 }
2940 };
2941 replies.push(buf[..len].to_vec());
2942 }
2943 Cmd::QueueDrain => {
2944 for (index, age_s) in [5u32, 3].into_iter().enumerate() {
2946 let mut meta = [0u8; BufferedRxMeta::WIRE_LEN];
2947 BufferedRxMeta {
2948 rx: RxMeta {
2949 rssi_dbm: Some(-80),
2950 lqi: None,
2951 snr_cb: Some(10),
2952 },
2953 flags: RX_FLAG_BUFFERED,
2954 age_s,
2955 }
2956 .encode(&mut meta)
2957 .unwrap();
2958 let data = [0xB0u8 + index as u8];
2959 let len =
2960 frame::str_recv(&mut buf, stream::PHY_RAW, &data, &meta).unwrap();
2961 replies.push(buf[..len].to_vec());
2962 }
2963 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
2964 replies.push(buf[..len].to_vec());
2965 }
2966 Cmd::Save | Cmd::Clear | Cmd::FactoryReset => {
2970 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
2971 replies.push(buf[..len].to_vec());
2972 }
2973 Cmd::Restore => {
2974 if props
2975 .get(&RESTORE_RESET_FORM_KEY)
2976 .is_some_and(|value| value == &[1])
2977 {
2978 let len = frame::last_status(
2979 &mut buf,
2980 TID_UNSOLICITED,
2981 Status::RESET_RESTORED,
2982 )
2983 .unwrap();
2984 replies.push(buf[..len].to_vec());
2985 } else {
2986 let len = frame::prop_is(
2989 &mut buf,
2990 TID_UNSOLICITED,
2991 prop::PHY_FREQ,
2992 &905_000u32.to_le_bytes(),
2993 )
2994 .unwrap();
2995 replies.push(buf[..len].to_vec());
2996 let len = frame::last_status(&mut buf, tid, Status::OK).unwrap();
2997 replies.push(buf[..len].to_vec());
2998 }
2999 }
3000 Cmd::PropIs | Cmd::StrRecv | Cmd::PropInserted | Cmd::PropRemoved => {
3001 panic!("host sent a device-only command")
3002 }
3003 }
3004 }
3005 for reply in replies {
3006 let mut wire = vec![0u8; hdlc::max_encoded_len(reply.len())];
3007 let len = hdlc::encode_frame(&reply, &mut wire).unwrap();
3008 if io.write_all(&wire[..len]).await.is_err() {
3009 return;
3010 }
3011 }
3012 }
3013 }
3014
3015 fn test_config() -> UlcpDeviceConfig {
3016 let mut config = UlcpDeviceConfig::new(906_875, 250_000, 11, 5);
3017 config.tx_power_dbm = 10;
3018 config.response_timeout = Duration::from_millis(500);
3019 config
3020 }
3021
3022 async fn attached_radio() -> UlcpDevice<SerialFrameLink<DuplexStream>> {
3023 let (client, server) = tokio::io::duplex(4096);
3024 tokio::spawn(fake_device(server));
3025 UlcpDevice::new(SerialFrameLink::new(client), test_config())
3026 .await
3027 .unwrap()
3028 }
3029
3030 fn wire(frame: &[u8]) -> Vec<u8> {
3031 let mut encoded = vec![0; hdlc::max_encoded_len(frame.len())];
3032 let len = hdlc::encode_frame(frame, &mut encoded).unwrap();
3033 encoded.truncate(len);
3034 encoded
3035 }
3036
3037 #[tokio::test]
3038 async fn serial_link_preserves_two_frames_from_one_read() {
3039 let (client, mut server) = tokio::io::duplex(1024);
3040 let mut bytes = wire(b"first");
3041 bytes.extend_from_slice(&wire(b"second"));
3042 server.write_all(&bytes).await.unwrap();
3043
3044 let mut link = SerialFrameLink::new(client);
3045 assert_eq!(link.recv_frame().await.unwrap(), b"first");
3046 assert_eq!(link.recv_frame().await.unwrap(), b"second");
3047 }
3048
3049 #[tokio::test]
3050 async fn serial_link_cancellation_keeps_partial_and_buffered_tail() {
3051 let (client, mut server) = tokio::io::duplex(1024);
3052 let first = wire(b"first");
3053 let second = wire(b"second");
3054 let split = second.len() / 2;
3055 let mut initial = first;
3056 initial.extend_from_slice(&second[..split]);
3057 server.write_all(&initial).await.unwrap();
3058
3059 let mut link = SerialFrameLink::new(client);
3060 assert_eq!(link.recv_frame().await.unwrap(), b"first");
3061 assert!(
3062 tokio::time::timeout(Duration::from_millis(1), link.recv_frame())
3063 .await
3064 .is_err()
3065 );
3066 server.write_all(&second[split..]).await.unwrap();
3067 assert_eq!(link.recv_frame().await.unwrap(), b"second");
3068 }
3069
3070 #[cfg(feature = "ble-radio")]
3071 #[test]
3072 fn ble_link_config_rejects_invalid_values_without_opening_an_adapter() {
3073 let mut config = BleFrameLinkConfig::default();
3074 assert!(config.validate().is_ok());
3075 config.segment_payload = 0;
3076 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3077 config.segment_payload = 512;
3078 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3079 config.segment_payload = 19;
3080 config.operation_timeout = Duration::ZERO;
3081 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3082 config.operation_timeout = Duration::from_secs(1);
3083 config.pairing_timeout = Duration::ZERO;
3084 assert!(matches!(config.validate(), Err(UlcpError::Protocol(_))));
3085 }
3086
3087 #[cfg(feature = "ble-radio")]
3088 #[tokio::test]
3089 async fn ble_notification_receiver_reassembles_and_recovers_from_malformed_segment() {
3090 let (tx, rx) = tokio::sync::mpsc::channel(8);
3091 let mut receiver = BleNotificationReceiver::new(rx);
3092
3093 tx.send(vec![0x01, 0xff]).await.unwrap();
3096 let frame = b"a frame larger than one tiny GATT segment";
3097 for segment in umsh_ulcp::gatt::segments(frame, 7) {
3098 let mut value = vec![0; segment.payload().len() + 1];
3099 segment.write_to(&mut value).unwrap();
3100 tx.send(value).await.unwrap();
3101 }
3102
3103 let received = core::future::poll_fn(|cx| receiver.poll_recv_frame(cx))
3104 .await
3105 .unwrap();
3106 assert_eq!(received, frame);
3107 }
3108
3109 #[cfg(feature = "ble-radio")]
3110 #[tokio::test]
3111 async fn ble_notification_channel_close_surfaces_disconnect() {
3112 let (tx, rx) = tokio::sync::mpsc::channel(1);
3113 let mut receiver = BleNotificationReceiver::new(rx);
3114 drop(tx);
3115 let result = core::future::poll_fn(|cx| receiver.poll_recv_frame(cx)).await;
3116 assert!(matches!(result, Err(UlcpError::Disconnected)));
3117 }
3118
3119 #[tokio::test]
3120 async fn initialization_handshake() {
3121 let radio = attached_radio().await;
3122 assert_eq!(radio.max_frame_size(), 255);
3123 assert_eq!(radio.dev_version(), "fake-dev/0.1");
3124 assert_eq!(radio.boot_status(), Status::RESET_POWER_ON);
3125 assert!(radio.t_frame_ms() > 0);
3126 }
3127
3128 #[tokio::test]
3129 async fn explicit_reset_returns_the_announced_status() {
3130 let mut radio = attached_radio().await;
3131 let status = radio.reset().await.unwrap();
3132 assert_eq!(status, Status::RESET_SOFTWARE);
3133 radio.get_prop(prop::LAST_STATUS).await.unwrap();
3135 }
3136
3137 #[tokio::test]
3138 async fn write_only_pairing_pin_accepts_status_completion() {
3139 let mut radio = attached_radio().await;
3140 radio.set_ble_pairing_pin(Some(123_456)).await.unwrap();
3141 radio.set_ble_pairing_pin(None).await.unwrap();
3142 assert!(radio.set_ble_pairing_pin(Some(1_000_000)).await.is_err());
3143
3144 let error = radio
3145 .set_prop(prop::BLE_PAIRING_PIN, &123_456u32.to_le_bytes())
3146 .await
3147 .unwrap_err();
3148 assert!(matches!(error, UlcpError::Protocol(_)));
3149 }
3150
3151 #[tokio::test]
3152 async fn device_name_typed_accessors_round_trip_and_validate() {
3153 let mut radio = attached_radio().await;
3154 radio.set_device_name("Field Radio 📻").await.unwrap();
3155 assert_eq!(radio.device_name().await.unwrap(), "Field Radio 📻");
3156 assert!(radio.set_device_name("").await.is_err());
3157 assert!(radio.set_device_name(&"x".repeat(65)).await.is_err());
3158 assert!(radio.set_device_name("bad\0name").await.is_err());
3159 }
3160
3161 #[tokio::test]
3162 async fn transmit_and_receive_round_trip() {
3163 let mut radio = attached_radio().await;
3164 let packet = [0x10u8, 0x20, 0x30, 0x40];
3165 radio.transmit(&packet, TxOptions::default()).await.unwrap();
3166
3167 let mut buf = [0u8; 256];
3168 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3169 .await
3170 .unwrap();
3171 assert_eq!(&buf[..info.len], &packet);
3172 assert_eq!(info.rssi, -91);
3173 assert_eq!(info.snr.as_centibels(), 55);
3174 }
3175
3176 #[tokio::test]
3177 async fn cca_failure_maps_to_cad_timeout() {
3178 let mut radio = attached_radio().await;
3179 let result = radio
3180 .transmit(
3181 CCA_FAIL,
3182 TxOptions {
3183 cad: CadPolicy::Gate,
3184 },
3185 )
3186 .await;
3187 assert!(matches!(result, Err(TxError::CadTimeout)));
3188 }
3189
3190 #[tokio::test]
3191 async fn oversized_frame_rejected() {
3192 let mut radio = attached_radio().await;
3193 let oversized = vec![0u8; radio.max_frame_size() + 1];
3194 let result = radio.transmit(&oversized, TxOptions::default()).await;
3195 assert!(matches!(
3196 result,
3197 Err(TxError::Io(UlcpError::FrameTooLarge(_)))
3198 ));
3199 }
3200
3201 #[tokio::test]
3202 async fn unexpected_reset_surfaces_on_receive() {
3203 let mut radio = attached_radio().await;
3204 radio
3205 .transmit(RESET_AFTER, TxOptions::default())
3206 .await
3207 .unwrap();
3208
3209 let mut buf = [0u8; 256];
3210 let result = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf)).await;
3211 assert!(matches!(
3212 result,
3213 Err(UlcpError::UnexpectedReset(status))
3214 if status == Status::RESET_WATCHDOG
3215 ));
3216 }
3217
3218 #[tokio::test]
3219 async fn table_insert_replace_remove_with_secret_free_digests() {
3220 let mut radio = attached_radio().await;
3221 let mut item = vec![0x11u8; 64];
3222 item[32..].fill(0x22);
3223 let digest = radio
3224 .insert_prop_item(prop::HOST_PEER_KEYS, &item)
3225 .await
3226 .unwrap();
3227 assert_eq!(digest, vec![0x11; 32]);
3229
3230 let mut replacement = item.clone();
3232 replacement[32..].fill(0x33);
3233 let digest = radio
3234 .insert_prop_item(prop::HOST_PEER_KEYS, &replacement)
3235 .await
3236 .unwrap();
3237 assert_eq!(digest, vec![0x11; 32]);
3238
3239 let removed = radio
3240 .remove_prop_item(prop::HOST_PEER_KEYS, &[0x11; 32])
3241 .await
3242 .unwrap();
3243 assert_eq!(removed, vec![0x11; 32]);
3244 let error = radio
3245 .remove_prop_item(prop::HOST_PEER_KEYS, &[0x11; 32])
3246 .await
3247 .unwrap_err();
3248 assert!(matches!(error, UlcpError::Status(status) if status == Status::ITEM_NOT_FOUND));
3249 }
3250
3251 #[tokio::test]
3252 async fn duplicate_insert_reports_already() {
3253 let mut radio = attached_radio().await;
3254 let filter = [2u8, 0]; radio
3256 .insert_prop_item(prop::HOST_RX_FILTERS, &filter)
3257 .await
3258 .unwrap();
3259 let error = radio
3260 .insert_prop_item(prop::HOST_RX_FILTERS, &filter)
3261 .await
3262 .unwrap_err();
3263 assert!(matches!(error, UlcpError::Status(status) if status == Status::ALREADY));
3264 }
3265
3266 #[tokio::test]
3267 async fn queue_drain_delivers_buffered_frames_then_completes() {
3268 let mut radio = attached_radio().await;
3269 let mut drained = Vec::new();
3270 radio
3271 .queue_drain_with(|data, meta| {
3272 drained.push((data.to_vec(), BufferedRxMeta::decode(meta).unwrap()));
3273 })
3274 .await
3275 .unwrap();
3276 assert_eq!(drained.len(), 2);
3277 assert!(
3278 drained
3279 .iter()
3280 .all(|(_, meta)| meta.flags & RX_FLAG_BUFFERED != 0)
3281 );
3282 assert_eq!((drained[0].1.age_s, drained[1].1.age_s), (5, 3));
3283
3284 let mut buf = [0u8; 16];
3287 for expected in [0xB0u8, 0xB1] {
3288 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3289 .await
3290 .unwrap();
3291 assert_eq!(&buf[..info.len], &[expected]);
3292 }
3293 }
3294
3295 #[tokio::test]
3296 async fn save_and_clear_complete_on_status() {
3297 let mut radio = attached_radio().await;
3298 radio.save().await.unwrap();
3299 radio.clear().await.unwrap();
3300 }
3301
3302 #[tokio::test]
3303 async fn restore_update_form_reports_updated_and_retains_events() {
3304 let mut radio = attached_radio().await;
3305 assert_eq!(radio.restore().await.unwrap(), RestoreCompletion::Updated);
3306 assert_eq!(
3307 radio.pop_prop_event(),
3308 Some(PropEvent::Is {
3309 key: prop::PHY_FREQ,
3310 value: 905_000u32.to_le_bytes().to_vec(),
3311 })
3312 );
3313 assert_eq!(radio.pop_prop_event(), None);
3314 }
3315
3316 #[tokio::test]
3317 async fn restore_reset_form_is_success_not_unexpected_reset() {
3318 let mut radio = attached_radio().await;
3319 radio.set_prop(RESTORE_RESET_FORM_KEY, &[1]).await.unwrap();
3320 assert_eq!(radio.restore().await.unwrap(), RestoreCompletion::Reset);
3321
3322 radio.transmit(&[0x55], TxOptions::default()).await.unwrap();
3325 let mut buf = [0u8; 16];
3326 let info = core::future::poll_fn(|cx| radio.poll_receive(cx, &mut buf))
3327 .await
3328 .unwrap();
3329 assert_eq!(&buf[..info.len], &[0x55]);
3330 }
3331
3332 #[tokio::test]
3333 async fn unsolicited_table_notifications_are_retained_events() {
3334 let mut radio = attached_radio().await;
3335 let mut buf = [0u8; 48];
3336 let len = frame::prop_inserted(&mut buf, TID_UNSOLICITED, prop::HOST_RX_FILTERS, &[2, 0])
3337 .unwrap();
3338 radio.ingest_frame(&buf[..len]);
3339 let len = frame::prop_removed(
3340 &mut buf,
3341 TID_UNSOLICITED,
3342 prop::HOST_CHANNEL_KEYS,
3343 &[0x12, 0x34],
3344 )
3345 .unwrap();
3346 radio.ingest_frame(&buf[..len]);
3347
3348 assert_eq!(
3349 radio.pop_prop_event(),
3350 Some(PropEvent::Inserted {
3351 key: prop::HOST_RX_FILTERS,
3352 digest: vec![2, 0],
3353 })
3354 );
3355 assert_eq!(
3356 radio.pop_prop_event(),
3357 Some(PropEvent::Removed {
3358 key: prop::HOST_CHANNEL_KEYS,
3359 digest: vec![0x12, 0x34],
3360 })
3361 );
3362 assert_eq!(radio.pop_prop_event(), None);
3363 }
3364
3365 #[test]
3366 fn airtime_is_plausible() {
3367 let airtime = lora_airtime_ms(11, 250_000, 5, 255);
3369 assert!((500..5_000).contains(&airtime), "airtime {airtime}");
3370 assert!(lora_airtime_ms(7, 250_000, 5, 255) < airtime);
3372 }
3373}