1use std::sync::atomic::{AtomicU8, Ordering};
15
16use dbus::channel::MatchingReceiver;
17use dbus::message::MatchRule;
18use dbus_crossroads::{Crossroads, IfaceBuilder};
19use tracing::{debug, info, warn};
20
21const STATE_IDLE: u8 = 0;
22const STATE_STARTING: u8 = 1;
23const STATE_REGISTERED: u8 = 2;
24const STATE_FAILED_PERMANENTLY: u8 = 3;
25
26const MAX_AGENT_ATTEMPTS: u8 = 3;
30
31static AGENT_STATE: AtomicU8 = AtomicU8::new(STATE_IDLE);
32static AGENT_ATTEMPTS: AtomicU8 = AtomicU8::new(0);
33static AGENT_PATH: &str = "/dev/rye/aranet/agent";
34const AGENT_CAPABILITY: &str = "NoInputNoOutput";
35
36pub fn ensure_agent() {
42 if AGENT_STATE
46 .compare_exchange(
47 STATE_IDLE,
48 STATE_STARTING,
49 Ordering::SeqCst,
50 Ordering::SeqCst,
51 )
52 .is_err()
53 {
54 return;
55 }
56 tokio::spawn(async {
57 match run_agent().await {
58 Ok(()) => {
59 AGENT_STATE.store(STATE_REGISTERED, Ordering::SeqCst);
60 }
61 Err(e) => {
62 let attempt = AGENT_ATTEMPTS.fetch_add(1, Ordering::SeqCst) + 1;
63 if attempt >= MAX_AGENT_ATTEMPTS {
64 warn!(
65 "Failed to register BlueZ agent after {attempt} attempts: {e} — \
66 giving up (BLE scans may hang if pairing is required)"
67 );
68 AGENT_STATE.store(STATE_FAILED_PERMANENTLY, Ordering::SeqCst);
69 } else {
70 warn!(
71 "Failed to register BlueZ agent (attempt {attempt}/{MAX_AGENT_ATTEMPTS}): \
72 {e} — will retry on next BLE operation"
73 );
74 AGENT_STATE.store(STATE_IDLE, Ordering::SeqCst);
76 }
77 }
78 }
79 });
80}
81
82async fn run_agent() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
83 let (resource, conn) = dbus_tokio::connection::new_system_sync()?;
85
86 let _handle = tokio::spawn(async move {
88 let err = resource.await;
89 warn!("BlueZ agent D-Bus connection lost: {err}");
90 });
91
92 let mut cr = Crossroads::new();
94
95 let iface_token = cr.register("org.bluez.Agent1", |b: &mut IfaceBuilder<()>| {
96 b.method("Release", (), (), |_, _, ()| {
97 debug!("BlueZ agent: Release");
98 Ok(())
99 });
100
101 b.method(
102 "RequestPasskey",
103 ("device",),
104 ("passkey",),
105 |_, _, (device,): (dbus::Path,)| {
106 debug!("BlueZ agent: RequestPasskey for {device}");
107 Ok((0u32,))
109 },
110 );
111
112 b.method(
113 "RequestConfirmation",
114 ("device", "passkey"),
115 (),
116 |_, _, (device, passkey): (dbus::Path, u32)| {
117 debug!("BlueZ agent: RequestConfirmation for {device}, passkey {passkey}");
118 Ok(())
119 },
120 );
121
122 b.method(
123 "RequestAuthorization",
124 ("device",),
125 (),
126 |_, _, (device,): (dbus::Path,)| {
127 debug!("BlueZ agent: RequestAuthorization for {device}");
128 Ok(())
129 },
130 );
131
132 b.method(
133 "AuthorizeService",
134 ("device", "uuid"),
135 (),
136 |_, _, (device, uuid): (dbus::Path, String)| {
137 debug!("BlueZ agent: AuthorizeService {uuid} for {device}");
138 Ok(())
139 },
140 );
141
142 b.method("Cancel", (), (), |_, _, ()| {
143 debug!("BlueZ agent: Cancel");
144 Ok(())
145 });
146 });
147
148 cr.insert(AGENT_PATH, &[iface_token], ());
149
150 conn.start_receive(
152 MatchRule::new_method_call(),
153 Box::new(move |msg, conn| {
154 if let Err(()) = cr.handle_message(msg, conn) {
155 warn!("BlueZ agent: failed to handle D-Bus message");
156 }
157 true
158 }),
159 );
160
161 let proxy = dbus::nonblock::Proxy::new(
166 "org.bluez",
167 "/org/bluez",
168 std::time::Duration::from_secs(5),
169 conn.clone(),
170 );
171
172 let () = proxy
173 .method_call(
174 "org.bluez.AgentManager1",
175 "RegisterAgent",
176 (dbus::Path::from(AGENT_PATH), AGENT_CAPABILITY),
177 )
178 .await?;
179
180 let () = proxy
181 .method_call(
182 "org.bluez.AgentManager1",
183 "RequestDefaultAgent",
184 (dbus::Path::from(AGENT_PATH),),
185 )
186 .await?;
187
188 info!("BlueZ agent registered as default ({AGENT_CAPABILITY})");
189
190 std::future::pending::<()>().await;
194 Ok(())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_agent_state_constants_are_distinct() {
203 let states = [
204 STATE_IDLE,
205 STATE_STARTING,
206 STATE_REGISTERED,
207 STATE_FAILED_PERMANENTLY,
208 ];
209 for (i, a) in states.iter().enumerate() {
210 for (j, b) in states.iter().enumerate() {
211 if i != j {
212 assert_ne!(a, b, "States at index {i} and {j} must differ");
213 }
214 }
215 }
216 }
217}