UlcpDevice

Struct UlcpDevice 

Source
pub struct UlcpDevice<L> { /* private fields */ }
Expand description

Companion radio attached over a frame link, usable as a [umsh_hal::Radio].

Implementations§

Source§

impl<L> UlcpDevice<L>
where L: FrameLink,

Source

pub fn attach_mode(&self) -> AttachMode

Which relationship this handle has with the device.

Source

pub async fn new(link: L, config: UlcpDeviceConfig) -> Result<Self, UlcpError>

Attach to a device: reset it, verify the protocol version, apply the RF configuration, and enable the PHY.

This is the minimal-protocol attach: CMD_RST discards a full-protocol device’s session-independent state visibility (and with a saved snapshot the post-reset values come from the snapshot, not the documented defaults). A host cooperating with an autonomously operating device should use Self::attach_existing instead.

Source

pub async fn attach_existing( link: L, config: UlcpDeviceConfig, ) -> Result<Self, UlcpError>

Attach to an already-operating device as its tethered host: the one host whose traffic it filters, queues and acknowledges.

This is the full-protocol attach (spec §Attach, Detach, and Synchronization): attach implies no known state, so the host synchronizes by fetching. Only the identity handshake runs here — retained PROP_LAST_STATUS (the reset cause, preserved for Self::boot_status and Self::sync), the protocol version check, PROP_DEV_VERSION, and PROP_PHY_MTU. The PHY keeps whatever configuration and enable state it had; queued frames and provisioning are untouched. Follow with Self::sync, Self::provision, and drain the queue when ready.

Use Self::attach_administrative to configure a device you do not intend to tether to — one phone administering ten repeaters must not write PROP_HOST_KEY on any of them.

Source

pub async fn attach_administrative( link: L, config: UlcpDeviceConfig, ) -> Result<Self, UlcpError>

Attach to an already-operating device to administer it: configure its own identity, radio, and behavior without becoming its host.

Commissioning and tethering are different relationships and this is the difference made mechanical. The handle refuses every host-domain write — PROP_HOST_KEY, the host key tables, the filter table, the delegation policy, and Self::provision — with UlcpError::AdministrativeAttach. Everything else, including the device identity and the saved snapshot, works normally.

A device may be administered by many hosts over its lifetime and tethered to at most one at a time; nothing about administering it disturbs whichever host it is currently serving.

Give up this handle and recover the transport underneath it.

The link stays open, so the device sees no detach and keeps its session-scoped state: this releases the host’s bookkeeping, not the connection. Re-attaching the returned link produces a fresh handle, which is how a long-lived interactive host changes AttachMode — administrative for inspection, tethered for the one command that establishes a host domain — without making the user wait through a BLE reconnect.

Source

pub fn set_frame_trace(&mut self, trace: Option<FrameTrace>)

Install (or clear) a per-frame trace sink. Every frame sent and every frame received is reported as a one-line summary (see describe_frame), so a failure can be placed at the host API, framing, session, storage, or radio boundary.

Source

pub fn dev_version(&self) -> &str

The device’s firmware version string (PROP_DEV_VERSION).

Source

pub async fn device_name(&mut self) -> Result<String, UlcpError>

Fetch the device’s human-readable PROP_DEV_NAME.

Source

pub async fn set_device_name(&mut self, name: &str) -> Result<(), UlcpError>

Set the device’s human-readable PROP_DEV_NAME.

Source

pub async fn battery_status( &mut self, ) -> Result<Option<BatteryStatus>, UlcpError>

Fetch a live battery status snapshot (PROP_BATTERY).

Ok(None) means the device does not advertise CAP_BATTERY (not battery powered). Ok(Some(status)) with every field None means battery powered with unsupported reporting. A measurement the device cannot currently obtain surfaces as a command failure, never as None — battery is live telemetry, so this is deliberately not part of UlcpDevice::sync.

Source

pub async fn illuminance(&mut self) -> Result<Option<u32>, UlcpError>

Fetch a live ambient illuminance reading in millilux (PROP_ILLUMINANCE).

Ok(None) means either that the device does not advertise CAP_ILLUMINANCE — no light sensor is fitted — or that a device which does could not read the sensor just now. Both are “there is no reading”, which is what a caller acts on; neither is an error. Live telemetry, so deliberately not part of UlcpDevice::sync.

Source

pub async fn alert(&mut self) -> Result<Option<AlertState>, UlcpError>

Read the device’s locate-alert state (PROP_ALERT).

Ok(None) means the device does not advertise CAP_ALERT — it has no way to make itself conspicuous.

Source

pub async fn set_alert( &mut self, state: AlertState, ) -> Result<AlertState, UlcpError>

Start or stop the device’s locate alert (PROP_ALERT).

Setting [AlertState::Locate] while an alert is already running restarts the device’s deadline rather than failing, so a host that wants an alert to outlast the board’s own bound re-sends this. Returns the authoritative state the device reported.

The alert also ends when someone cancels it at the device or the deadline expires; both arrive as an unsolicited PROP_ALERT update rather than as a response to this call.

A device without CAP_ALERT answers STATUS_PROP_NOT_FOUND.

Source

pub fn boot_status(&self) -> Status

Reset cause reported by the device immediately after transport attach.

Source

pub async fn repeater_policy( &mut self, ) -> Result<Option<RepeaterPolicy>, UlcpError>

Read the device’s full repeater forwarding policy.

Ok(None) means the device does not advertise CAP_REPEATER.

Source

pub async fn set_repeater_regions( &mut self, regions: &[RegionCode], ) -> Result<Vec<RegionCode>, UlcpError>

Set which region-tagged floods the device forwards (PROP_MAC_REPEATER_REGIONS). An empty list clears the filter, which imposes no regional restriction rather than blocking every flood.

Returns the list the device actually stored. A device with less capacity than the caller offered keeps a prefix, so a shorter return is a truncation, not an error.

Source

pub async fn set_repeater_default_region( &mut self, region: Option<RegionCode>, ) -> Result<Option<RegionCode>, UlcpError>

Set the region code inserted into untagged floods before forwarding (PROP_MAC_REPEATER_DEFAULT_REGION). None forwards untagged.

Deliberately not cross-checked against set_repeater_regions: the two are written in either order.

Source

pub async fn set_repeater_min_rssi( &mut self, min_rssi: Option<i16>, ) -> Result<Option<i16>, UlcpError>

Set the RSSI floor for forwarding in dBm (PROP_MAC_REPEATER_MIN_RSSI). None accepts any.

Source

pub async fn set_repeater_min_snr( &mut self, min_snr: Option<i8>, ) -> Result<Option<i8>, UlcpError>

Set the SNR floor for forwarding in dB (PROP_MAC_REPEATER_MIN_SNR). None accepts any.

Source

pub async fn time(&mut self) -> Result<Option<DeviceTime>, UlcpError>

Read the device’s wall clock and time zone (PROP_TIME, PROP_TZ_OFFSET).

Ok(None) means the device does not advertise CAP_TIME. Ok(Some(time)) with time.epoch == None means it has one and does not know what time it is — the state in which a device with a screen must show no clock at all.

Source

pub async fn set_time( &mut self, epoch: Option<u32>, ) -> Result<Option<u32>, UlcpError>

Set the device’s wall clock (PROP_TIME). None returns it to not knowing what time it is.

A manual set outranks every receiver-derived one, including while PROP_GNSS_TIME_TRUST is clear.

Source

pub async fn set_tz_offset(&mut self, minutes: i16) -> Result<i16, UlcpError>

Set the device’s local time-zone offset in minutes east of UTC (PROP_TZ_OFFSET).

Source

pub async fn gnss_status(&mut self) -> Result<Option<GnssStatus>, UlcpError>

Read everything the device reports about positioning (PROP_GNSS_*).

Ok(None) means the device does not advertise CAP_GNSS. The fix is live telemetry, so a disabled or searching receiver reports [GnssSnapshot::SEARCHING] rather than an error.

Source

pub async fn advert_policy(&mut self) -> Result<Option<AdvertPolicy>, UlcpError>

Read the device’s advertisement policy, or None on a device without CAP_ADVERT.

Source

pub async fn set_advert_interval( &mut self, seconds: u32, ) -> Result<u32, UlcpError>

Set the seconds between signed identity advertisements, 0 for none (PROP_ADVERT_INTERVAL).

Source

pub async fn set_beacon_interval( &mut self, seconds: u32, ) -> Result<u32, UlcpError>

Set the seconds between empty beacons, 0 for none (PROP_BEACON_INTERVAL).

Source

pub async fn set_startup_beacon( &mut self, enabled: bool, ) -> Result<bool, UlcpError>

Set whether one beacon goes out at bring-up (PROP_STARTUP_BEACON).

Source

pub async fn set_gnss_enabled( &mut self, enabled: bool, ) -> Result<bool, UlcpError>

Power the GNSS receiver on or off (PROP_GNSS_ENABLED).

Source

pub async fn set_gnss_ident_update( &mut self, enabled: bool, ) -> Result<bool, UlcpError>

Set whether fixes refresh the advertised node identity’s location (PROP_GNSS_IDENT_UPDATE).

Source

pub async fn set_gnss_ident_precision( &mut self, precision: u8, ) -> Result<u8, UlcpError>

Set the precision the advertised location is clamped to, in location bytes (PROP_GNSS_IDENT_PRECISION).

Source

pub async fn set_gnss_time_trust( &mut self, trust: bool, ) -> Result<bool, UlcpError>

Set whether receiver-derived time may set the wall clock (PROP_GNSS_TIME_TRUST).

Clearing it leaves a manually-set clock proof against a jammed or spoofed sky; position reporting is unaffected.

Source

pub async fn get_prop(&mut self, key: u32) -> Result<Vec<u8>, UlcpError>

Fetch a property’s raw value via CMD_PROP_GET.

Source

pub async fn set_prop( &mut self, key: u32, value: &[u8], ) -> Result<Vec<u8>, UlcpError>

Set a property via CMD_PROP_SET, returning the authoritative value echoed by the device.

Source

pub async fn insert_prop_item( &mut self, key: u32, item: &[u8], ) -> Result<Vec<u8>, UlcpError>

Insert one item into a multi-value property via CMD_PROP_INSERT, returning the inserted item’s digest form from the correlated CMD_PROP_INSERTED.

item is in the property’s item form with no length prefix. A duplicate fails with STATUS_ALREADY unless the property defines replacement semantics (PROP_HOST_PEER_KEYS).

Source

pub async fn remove_prop_item( &mut self, key: u32, selector: &[u8], ) -> Result<Vec<u8>, UlcpError>

Remove one item from a multi-value property via CMD_PROP_REMOVE, returning the removed item’s digest form from the correlated CMD_PROP_REMOVED.

selector is the property’s documented remove selector. A missing item fails with STATUS_ITEM_NOT_FOUND.

Source

pub async fn queue_drain(&mut self) -> Result<(), UlcpError>

Drain the device’s inbound queue (CMD_QUEUE_DRAIN).

Buffered frames are delivered as ordinary CMD_STR_RECV and land in the receive queue for [Radio::poll_receive]; this future resolves on the correlated completion status.

Source

pub async fn queue_drain_with( &mut self, on_frame: impl FnMut(&[u8], &[u8]), ) -> Result<(), UlcpError>

As Self::queue_drain, invoking on_frame with each frame (data, trailing metadata bytes) delivered before completion — buffered and interleaved live frames alike. The callback sees every such frame: an device queue larger than this driver’s bounded receive buffer drains losslessly through it. Frames are additionally queued for [Radio::poll_receive], where the bounded buffer’s oldest-dropped policy still applies.

Source

pub async fn save(&mut self) -> Result<(), UlcpError>

Save the device’s device and host domains to non-volatile storage (CMD_SAVE; requires CAP_SAVE).

Source

pub async fn clear(&mut self) -> Result<(), UlcpError>

Erase the device’s saved snapshot and other persisted provisioning (CMD_CLEAR; base protocol, BLE bonds and pairing PIN exempt).

Source

pub async fn reset(&mut self) -> Result<Status, UlcpError>

Reset the device (CMD_RST) and wait for the reset notification, returning the announced reset status. The device comes up as from a power cycle — restoring its saved snapshot when one exists, factory configuration otherwise. All session-scoped state and cached views are gone; follow with Self::sync.

Source

pub async fn factory_reset(&mut self) -> Result<(), UlcpError>

Factory-reset the device (CMD_FACTORY_RESET): erase ALL mutable state — saved provisioning, the device identity, BLE bonds, and the pairing PIN — and reboot to a blank factory state. Unlike Self::reset this sends no expectation of a reply and does not wait: the device wipes storage and reboots without responding, which drops the transport link. Treat the ensuing disconnect as completion; a caller that needs the radio again must re-open the transport and re-pair, since the bond it used is now gone.

Source

pub async fn restore(&mut self) -> Result<RestoreCompletion, UlcpError>

Revert the device to its saved snapshot (CMD_RESTORE; requires CAP_SAVE), accepting both spec-permitted completion forms.

Source

pub async fn set_ble_pairing_pin( &mut self, pin: Option<u32>, ) -> Result<(), UlcpError>

Set or clear the device’s persisted, write-only BLE pairing PIN.

This property is the protocol’s sole status-only property write: the value is never echoed. None clears the configured passkey.

Source

pub async fn capabilities(&mut self) -> Result<Vec<u32>, UlcpError>

Fetch and decode PROP_CAPS.

Source

pub async fn sync( &mut self, expected_host_key: Option<&[u8; 32]>, ) -> Result<DeviceSync, UlcpError>

Run the spec’s post-attach synchronization procedure: fetch the retained PROP_LAST_STATUS (detecting a reset since the last contact), the capability list, the configured host identity — yielding an ownership verdict against expected_host_key — and the state each advertised capability grants, all in digest form.

The host must decide ownership before treating queued data as its own: HostOwnership::OtherHost means the queue and provisioning belong to another identity.

Source

pub async fn provision( &mut self, desired: &HostProvisioning, ) -> Result<ProvisionReport, UlcpError>

Establish desired as the device’s complete host domain, writing every part of it unconditionally.

This does not compare and patch, and that is deliberate. Key tables read back in lossy form only: the device reports channel identifiers and peer public keys, never key material. An administrator can replace a peer’s K_enc/K_mic without changing anything observable, so no comparison over the readable surface can detect it — and a digest over the secret state would mean deriving a readable value from key material, which is worse than the problem. The host asserts what it wants; it does not reason about what the device already holds.

Removals still come from comparison, and that is not a contradiction: membership is readable even though key material is not. So this reads the digest lists, removes what desired omits, and writes everything desired contains regardless of what came back.

The host domain is volatile across power cycles, so the usual case is a device that has just rebooted and holds nothing. When it has not rebooted the rewrite is redundant — that is the point. Correctness must not depend on detecting which case this is, because reboot detection would also have to cover partial provisioning, another administrator having intervened, and future device behavior changes.

Each individual write is transactional on the device (spec §Mutation Atomicity); the sequence is not (see the ULCP transition plan, decision 7). An interrupted call leaves a mixture, which the next call repairs by rewriting everything.

Provisioning is per item rather than per table wherever the table can grow: a whole peer table stops fitting in a frame at the fifth entry.

Source

pub async fn ensure_device_identity(&mut self) -> Result<[u8; 32], UlcpError>

The device’s device identity public key, generating one on-device if none is configured (CAP_DEV_IDENTITY; generation requires the transport’s provisioning-security binding).

On-device generation is the spec-recommended form: the private key never exists anywhere but the radio, and only the resulting public key crosses the link.

Source

pub fn pop_prop_event(&mut self) -> Option<PropEvent>

Take the oldest retained unsolicited property notification.

Events accumulate while other calls read from the link (bounded at [PROP_EVENT_DEPTH], oldest dropped first).

Source

pub async fn transmit_raw_with_meta( &mut self, data: &[u8], metadata: &[u8], ) -> Result<(), TxError<UlcpError>>

Transmit a frame with a caller-supplied STR_PHY_RAW metadata block, byte for byte.

[Radio::transmit] composes the metadata from [TxOptions], which is what a MAC wants. A bridge does not: it relays frames whose transmit parameters were decided elsewhere, and must be able to put exactly those bytes on the wire — including fields [TxOptions] has no vocabulary for, such as a power override.

The channel-access retry budget comes from the metadata itself: with TX_FLAG_NOCCA clear the device performs CCA and a busy channel fails immediately with [TxError::CadTimeout], leaving the retry policy to the caller.

Source

pub fn poll_receive_raw( &mut self, cx: &mut Context<'_>, ) -> Poll<Result<RawRxFrame, UlcpError>>

Poll for one inbound frame, preserving its metadata bytes.

[Radio::poll_receive] decodes the metadata into [RxInfo], which cannot represent it faithfully: the “unsupported” sentinels collapse to zero and the buffered-frame extension is discarded entirely. A bridge relays the metadata rather than interpreting it, so it needs the bytes.

Source

pub async fn receive_raw(&mut self) -> Result<RawRxFrame, UlcpError>

Await one inbound frame with its metadata bytes intact.

Cancel-safe: the frame is only removed from the inbound queue once this future is ready to return it.

Source§

impl UlcpDevice<SerialFrameLink<SerialStream>>

Source

pub async fn open_serial( path: impl AsRef<str>, baud_rate: u32, config: UlcpDeviceConfig, ) -> Result<Self, UlcpError>

Attach to a device on a serial port.

Source§

impl UlcpDevice<BleFrameLink>

Source

pub async fn open_ble( selector: Option<&str>, config: UlcpDeviceConfig, ) -> Result<Self, UlcpError>

Discover, connect, attach, and initialize a BLE companion radio.

As Self::open_ble, with an explicit GATT link configuration.

Trait Implementations§

Source§

impl<L> Radio for UlcpDevice<L>
where L: FrameLink,

Source§

async fn transmit( &mut self, data: &[u8], options: TxOptions, ) -> Result<(), TxError<Self::Error>>

Transmit one frame and await the device’s confirmation.

A confirmed transmit blocks the caller for up to response_timeout + 2 × t_frame_ms while the frame goes out on air. This is inherent to the half-duplex [Radio::transmit] contract and a real radio behaves the same way. Frames the device receives during this window are not lost — they are queued (see wait_responseingest) and surface on the next poll_receive. MAC-layer timers (ACK timeouts, retransmit deadlines) cannot advance while this future is pending, but they are only delayed, not missed: the coordinator re-evaluates every deadline against the current clock as soon as transmit returns, so a deadline that came due mid-transmit fires immediately afterward.

Source§

type Error = UlcpError

Source§

fn poll_receive( &mut self, cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll<Result<RxInfo, Self::Error>>

Poll reception of one frame into buf. Read more
Source§

fn max_frame_size(&self) -> usize

Return the largest supported raw frame size.
Source§

fn t_frame_ms(&self) -> u32

Return the approximate airtime for a maximum-length frame.

Auto Trait Implementations§

§

impl<L> Freeze for UlcpDevice<L>
where L: Freeze,

§

impl<L> !RefUnwindSafe for UlcpDevice<L>

§

impl<L> Send for UlcpDevice<L>
where L: Send,

§

impl<L> !Sync for UlcpDevice<L>

§

impl<L> Unpin for UlcpDevice<L>
where L: Unpin,

§

impl<L> !UnwindSafe for UlcpDevice<L>

Blanket Implementations§

§

impl<T> Any for T
where T: 'static + ?Sized,

§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<T> Borrow<T> for T
where T: ?Sized,

§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
§

impl<T> BorrowMut<T> for T
where T: ?Sized,

§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> From<T> for T

§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T, U> Into<U> for T
where U: From<T>,

§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> AutoreleaseSafe for T
where T: ?Sized,