Skip to main content

rmk_types/
connection.rs

1//! Shared connection type definitions used across RMK crates.
2
3use postcard::experimental::max_size::MaxSize;
4use serde::{Deserialize, Serialize};
5
6use crate::ble::{BleState, BleStatus};
7
8/// Connection type for the keyboard.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
10#[cfg_attr(feature = "defmt", derive(defmt::Format))]
11#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
12#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
13pub enum ConnectionType {
14    Usb,
15    Ble,
16}
17
18/// USB device lifecycle. `Suspended` is distinct from `Configured` because
19/// the bus is enumerated but transmission is gated on remote wakeup — the
20/// first key still needs to reach the USB writer to trigger that wakeup.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
22#[cfg_attr(feature = "defmt", derive(defmt::Format))]
23#[derive(Default)]
24#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
25#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
26pub enum UsbState {
27    #[default]
28    Disabled,
29    Enabled,
30    Configured,
31    Suspended,
32}
33
34/// Unified connection status: the single source of truth for transport
35/// availability and routing. The active transport is derived on demand via
36/// [`Self::decide_active`] from the input fields below.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
40#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
41pub struct ConnectionStatus {
42    pub usb: UsbState,
43    pub ble: BleStatus,
44    /// Tiebreaker when both transports are ready.
45    pub preferred: ConnectionType,
46}
47
48impl ConnectionStatus {
49    pub const fn new() -> Self {
50        Self {
51            usb: UsbState::Disabled,
52            ble: BleStatus {
53                profile: 0,
54                state: BleState::Inactive,
55            },
56            preferred: ConnectionType::Usb,
57        }
58    }
59}
60
61impl Default for ConnectionStatus {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl ConnectionStatus {
68    fn usb_ready(&self) -> bool {
69        matches!(self.usb, UsbState::Configured | UsbState::Suspended)
70    }
71
72    fn ble_ready(&self) -> bool {
73        matches!(self.ble.state, BleState::Connected)
74    }
75
76    /// Pick the active transport from current readiness + preference. Suspended
77    /// USB remains routable for remote wakeup, so it participates in the same
78    /// preference tie-break as configured USB.
79    pub fn decide_active(&self) -> Option<ConnectionType> {
80        match (self.usb_ready(), self.ble_ready()) {
81            (true, false) => Some(ConnectionType::Usb),
82            (false, true) => Some(ConnectionType::Ble),
83            (true, true) => Some(self.preferred),
84            (false, false) => None,
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::{ConnectionStatus, ConnectionType, UsbState};
92    use crate::ble::{BleState, BleStatus};
93
94    fn status(usb: UsbState, ble_state: BleState, preferred: ConnectionType) -> ConnectionStatus {
95        ConnectionStatus {
96            usb,
97            ble: BleStatus {
98                profile: 0,
99                state: ble_state,
100            },
101            preferred,
102        }
103    }
104
105    #[test]
106    fn decide_active_only_usb_ready() {
107        let s = status(UsbState::Configured, BleState::Inactive, ConnectionType::Ble);
108        assert_eq!(s.decide_active(), Some(ConnectionType::Usb));
109    }
110
111    #[test]
112    fn decide_active_only_ble_ready() {
113        let s = status(UsbState::Disabled, BleState::Connected, ConnectionType::Usb);
114        assert_eq!(s.decide_active(), Some(ConnectionType::Ble));
115    }
116
117    #[test]
118    fn decide_active_both_ready_prefers_preference() {
119        let s = status(UsbState::Configured, BleState::Connected, ConnectionType::Ble);
120        assert_eq!(s.decide_active(), Some(ConnectionType::Ble));
121        let s = status(UsbState::Configured, BleState::Connected, ConnectionType::Usb);
122        assert_eq!(s.decide_active(), Some(ConnectionType::Usb));
123    }
124
125    #[test]
126    fn decide_active_neither_ready_is_none() {
127        let s = status(UsbState::Disabled, BleState::Inactive, ConnectionType::Usb);
128        assert_eq!(s.decide_active(), None);
129    }
130
131    #[test]
132    fn suspended_usb_stays_routable_for_remote_wakeup() {
133        let s = status(UsbState::Suspended, BleState::Advertising, ConnectionType::Ble);
134        assert_eq!(s.decide_active(), Some(ConnectionType::Usb));
135        assert!(s.usb_ready());
136    }
137
138    #[test]
139    fn suspended_usb_with_connected_ble_prefers_preference() {
140        let s = status(UsbState::Suspended, BleState::Connected, ConnectionType::Usb);
141        assert_eq!(s.decide_active(), Some(ConnectionType::Usb));
142        assert!(s.usb_ready());
143        assert!(s.ble_ready());
144
145        let s = status(UsbState::Suspended, BleState::Connected, ConnectionType::Ble);
146        assert_eq!(s.decide_active(), Some(ConnectionType::Ble));
147        assert!(s.usb_ready());
148        assert!(s.ble_ready());
149    }
150}