1use std::future::Future;
4use std::pin::Pin;
5
6use anyhow::{Context, Result};
7use config::Config;
8use map_core::client::MapClient;
9use pbap_core::client::PbapClient;
10use tokio_util::either::Either;
11use transport::iroh::{Endpoint, EndpointId, HubStream, MAP_ALPN, PBAP_ALPN};
12
13pub type Stream = Either<HubStream, bluer::rfcomm::Stream>;
15
16enum Target {
18 Hub(EndpointId),
20 Rfcomm(bluer::Address, u8),
22}
23
24fn target(
30 hub: bool,
31 hub_node_key: Option<&str>,
32 device_override: Option<&str>,
33 device_addr: &str,
34 channel: u8,
35) -> Result<Target> {
36 if hub {
37 Ok(Target::Hub(resolve_hub_id(hub_node_key)?))
38 } else {
39 let addr_str = device_override.unwrap_or(device_addr);
40 let addr = addr_str
41 .parse::<bluer::Address>()
42 .with_context(|| format!("invalid device address: {addr_str}"))?;
43 Ok(Target::Rfcomm(addr, channel))
44 }
45}
46
47pub fn resolve_hub_id(node_key: Option<&str>) -> Result<EndpointId> {
56 let key_str = node_key
57 .ok_or_else(|| anyhow::anyhow!("hub.node_key is not set; run `imsg spoke add <KEY>`"))?;
58 key_str.parse::<EndpointId>().context("invalid hub.node_key")
59}
60
61#[must_use]
73pub fn connect_map<'a>(
74 cfg: &'a Config,
75 endpoint: Option<&'a Endpoint>,
76 device_override: Option<&'a str>,
77) -> Pin<Box<dyn Future<Output = Result<MapClient<Stream>>> + Send + 'a>> {
78 Box::pin(connect_map_inner(cfg, endpoint, device_override))
80}
81
82async fn connect_map_inner(
83 cfg: &Config,
84 endpoint: Option<&Endpoint>,
85 device_override: Option<&str>,
86) -> Result<MapClient<Stream>> {
87 let tgt = target(
88 endpoint.is_some(),
89 cfg.hub.node_key.as_deref(),
90 device_override,
91 cfg.device.address(),
92 cfg.device.map_channel,
93 )?;
94 match tgt {
95 Target::Hub(id) => {
96 let ep = endpoint
97 .ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
98 let conn = ep.connect(id, MAP_ALPN).await.context("iroh connect (MAP)")?;
99 let (send, recv) = conn.open_bi().await.context("iroh open_bi (MAP)")?;
100 let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
101 crate::lifecycle::establish_map_session(stream)
102 .await
103 .context("establishing MAP session over iroh")
104 }
105 Target::Rfcomm(addr, channel) => {
106 let stream = transport::rfcomm::connect(
107 addr,
108 channel,
109 transport::rfcomm::DEFAULT_BT_CONNECTED_GATE,
110 None,
111 )
112 .await
113 .context("RFCOMM connect (MAP)")?;
114 crate::lifecycle::establish_map_session(Either::Right(stream))
115 .await
116 .context("establishing MAP session")
117 }
118 }
119}
120
121#[must_use]
131pub fn connect_pbap<'a>(
132 cfg: &'a Config,
133 endpoint: Option<&'a Endpoint>,
134 device_override: Option<&'a str>,
135) -> Pin<Box<dyn Future<Output = Result<PbapClient<Stream>>> + Send + 'a>> {
136 Box::pin(connect_pbap_inner(cfg, endpoint, device_override))
138}
139
140async fn connect_pbap_inner(
141 cfg: &Config,
142 endpoint: Option<&Endpoint>,
143 device_override: Option<&str>,
144) -> Result<PbapClient<Stream>> {
145 let tgt = target(
146 endpoint.is_some(),
147 cfg.hub.node_key.as_deref(),
148 device_override,
149 cfg.device.address(),
150 cfg.device.pbap_channel,
151 )?;
152 match tgt {
153 Target::Hub(id) => {
154 let ep = endpoint
155 .ok_or_else(|| anyhow::anyhow!("internal: hub target requires an endpoint"))?;
156 let conn = ep.connect(id, PBAP_ALPN).await.context("iroh connect (PBAP)")?;
157 let (send, recv) = conn.open_bi().await.context("iroh open_bi (PBAP)")?;
158 let stream: Stream = Either::Left(HubStream::new(tokio::io::join(recv, send), conn));
159 PbapClient::connect(stream).await.context("establishing PBAP session over iroh")
160 }
161 Target::Rfcomm(addr, channel) => {
162 let stream = transport::rfcomm::connect(
163 addr,
164 channel,
165 transport::rfcomm::DEFAULT_BT_CONNECTED_GATE,
166 None,
167 )
168 .await
169 .context("RFCOMM connect (PBAP)")?;
170 PbapClient::connect(Either::Right(stream)).await.context("establishing PBAP session")
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn target_hub_key_absent_returns_err() {
181 let result = target(true, None, None, "AA:BB:CC:DD:EE:FF", 2);
182 assert!(result.is_err());
183 }
184
185 #[test]
186 fn target_hub_key_invalid_returns_err() {
187 let result = target(true, Some("not-a-valid-key"), None, "AA:BB:CC:DD:EE:FF", 2);
188 assert!(result.is_err());
189 }
190
191 #[test]
192 fn target_hub_valid_key() -> Result<()> {
193 let key = transport::iroh::SecretKey::generate();
194 let id_str = key.public().to_string();
195 let tgt = target(true, Some(&id_str), None, "AA:BB:CC:DD:EE:FF", 2)?;
196 assert!(matches!(tgt, Target::Hub(_)));
197 Ok(())
198 }
199
200 #[test]
201 fn target_rfcomm_valid_addr() -> Result<()> {
202 let tgt = target(false, None, None, "AA:BB:CC:DD:EE:FF", 2)?;
203 assert!(matches!(tgt, Target::Rfcomm(_, 2)));
204 Ok(())
205 }
206
207 #[test]
208 fn target_rfcomm_device_override() -> Result<()> {
209 let tgt = target(false, None, Some("11:22:33:44:55:66"), "AA:BB:CC:DD:EE:FF", 13)?;
210 let Target::Rfcomm(addr, ch) = tgt else {
211 anyhow::bail!("expected Rfcomm");
212 };
213 assert_eq!(addr.to_string(), "11:22:33:44:55:66");
214 assert_eq!(ch, 13);
215 Ok(())
216 }
217
218 #[test]
219 fn target_rfcomm_invalid_addr_returns_err() {
220 let result = target(false, None, None, "not-a-mac", 2);
221 assert!(result.is_err());
222 }
223}