offline_protocol/telemetry/device.rs
1//! Push-based snapshots of local device capability changes.
2//!
3//! Emitted when any of (battery level, charging state, relay role) changes
4//! between consecutive `OfflineProtocol::process()` ticks. Apps that need
5//! "always-on" polling consume this instead of reconstructing the state from
6//! the cumulative `TransportMetrics` stream.
7//!
8//! Advertised services are **not** represented — the SDK does not currently
9//! track an advertised-services list. A later step introduces that surface
10//! with its own dedicated record.
11
12use offline_protocol_router::RelayRole;
13
14/// Bitmask bit indicating `battery_level` changed between ticks.
15pub const CHANGED_BATTERY: u8 = 0b0000_0001;
16/// Bitmask bit indicating `is_charging` changed between ticks.
17pub const CHANGED_CHARGING: u8 = 0b0000_0010;
18/// Bitmask bit indicating `relay_role` changed between ticks.
19pub const CHANGED_RELAY_ROLE: u8 = 0b0000_0100;
20
21/// Snapshot of local device capability at the moment of emission.
22///
23/// The `changed_fields` bitmask identifies which fields triggered emission —
24/// sinks can filter on a specific bit to ignore unrelated churn.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct DeviceCapabilitySnapshot {
27 /// Emission timestamp (milliseconds since Unix epoch).
28 pub timestamp_ms: i64,
29 /// Current battery level (0-100), or `None` if unknown.
30 pub battery_level: Option<u8>,
31 /// Whether the device is charging.
32 pub is_charging: bool,
33 /// Current relay role (regular or relay).
34 pub relay_role: RelayRole,
35 /// Bitmask of the fields that triggered this emission.
36 pub changed_fields: u8,
37}
38
39impl DeviceCapabilitySnapshot {
40 /// Stable OTel-friendly name for this record.
41 pub const NAME: &'static str = "protocol.device.capability_changed";
42
43 /// Returns the stable telemetry name.
44 pub fn telemetry_name(&self) -> &'static str {
45 Self::NAME
46 }
47
48 /// Returns `true` when the given change bit is set.
49 pub fn has_changed(&self, bit: u8) -> bool {
50 self.changed_fields & bit != 0
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn name_is_stable() {
60 let snapshot = DeviceCapabilitySnapshot {
61 timestamp_ms: 0,
62 battery_level: Some(50),
63 is_charging: false,
64 relay_role: RelayRole::Regular,
65 changed_fields: CHANGED_BATTERY,
66 };
67 assert_eq!(
68 DeviceCapabilitySnapshot::NAME,
69 "protocol.device.capability_changed"
70 );
71 assert_eq!(
72 snapshot.telemetry_name(),
73 "protocol.device.capability_changed"
74 );
75 }
76
77 #[test]
78 fn has_changed_respects_bits() {
79 let snapshot = DeviceCapabilitySnapshot {
80 timestamp_ms: 0,
81 battery_level: None,
82 is_charging: true,
83 relay_role: RelayRole::Regular,
84 changed_fields: CHANGED_CHARGING | CHANGED_RELAY_ROLE,
85 };
86 assert!(snapshot.has_changed(CHANGED_CHARGING));
87 assert!(snapshot.has_changed(CHANGED_RELAY_ROLE));
88 assert!(!snapshot.has_changed(CHANGED_BATTERY));
89 }
90}