gestura_core_ring/backends/
simulator.rs1use crate::{DeviceStatus, RingBackend};
2use async_trait::async_trait;
3use btleplug::api::{CharPropFlags, Characteristic, Peripheral as _};
4use btleplug::platform::{Adapter, Peripheral};
5use futures::stream::StreamExt;
6use gestura_core_gestures::Gesture;
7use gestura_core_haptics::HapticPattern;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use tokio::sync::{Mutex, broadcast};
11use uuid::Uuid;
12
13const SIMULATOR_SERVICE_UUID: Uuid = Uuid::from_u128(0x12345678_1234_5678_9abc_123456789abc);
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type")]
18pub enum SimulatorRawGesture {
19 Tap {
20 intensity: f32,
21 },
22 DoubleTap,
23 Hold {
24 start_time: u64,
25 },
26 Slide {
27 direction: SlideDirection,
28 distance: u32,
29 },
30 Tilt {
31 angle: f32,
32 },
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum SlideDirection {
37 Up,
38 Down,
39 Left,
40 Right,
41}
42
43impl SimulatorRawGesture {
44 pub fn into_gesture(self) -> Gesture {
63 match self {
64 Self::Tap { intensity } => Gesture {
65 gesture_type: "tap".to_string(),
66 confidence: intensity.clamp(0.0, 1.0),
69 acceleration: None,
70 gyroscope: None,
71 },
72 Self::DoubleTap => Gesture {
73 gesture_type: "double_tap".to_string(),
74 confidence: 1.0,
75 acceleration: None,
76 gyroscope: None,
77 },
78 Self::Hold { .. } => Gesture {
79 gesture_type: "hold".to_string(),
80 confidence: 1.0,
81 acceleration: None,
82 gyroscope: None,
83 },
84 Self::Slide {
85 direction,
86 distance,
87 } => {
88 let tilt_type = match direction {
93 SlideDirection::Up => "tilt_up",
94 SlideDirection::Down => "tilt_down",
95 SlideDirection::Left => "tilt_left",
96 SlideDirection::Right => "tilt_right",
97 };
98 Gesture {
99 gesture_type: tilt_type.to_string(),
100 confidence: 1.0,
101 acceleration: Some([distance as f32, 0.0, 0.0]),
104 gyroscope: None,
105 }
106 }
107 Self::Tilt { angle } => {
108 let tilt_type = if angle >= 0.0 {
111 "tilt_right"
112 } else {
113 "tilt_left"
114 };
115 Gesture {
116 gesture_type: tilt_type.to_string(),
117 confidence: 1.0,
118 acceleration: None,
119 gyroscope: Some([angle, 0.0, 0.0]),
122 }
123 }
124 }
125 }
126}
127
128pub struct SimulatorBackend {
129 tx: broadcast::Sender<Gesture>,
130 peripheral: Arc<Mutex<Option<Peripheral>>>,
131 tx_char: Arc<Mutex<Option<Characteristic>>>,
132 adapter: Arc<Mutex<Option<Adapter>>>,
133}
134
135impl Default for SimulatorBackend {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl SimulatorBackend {
142 pub fn new() -> Self {
143 let (tx, _) = broadcast::channel(100);
144 Self {
145 tx,
146 peripheral: Arc::new(Mutex::new(None)),
147 tx_char: Arc::new(Mutex::new(None)),
148 adapter: Arc::new(Mutex::new(None)),
149 }
150 }
151
152 async fn find_simulator(&self) -> Result<Peripheral, String> {
153 let (adapter, peripheral) = gestura_core_ble::scanner::find_device_by_service_uuid(
154 SIMULATOR_SERVICE_UUID,
155 10,
156 std::time::Duration::from_millis(500),
157 )
158 .await?;
159
160 *self.adapter.lock().await = Some(adapter);
161 Ok(peripheral)
162 }
163
164 fn spawn_event_listener(&self, peripheral: Peripheral, characteristic: Characteristic) {
166 let tx = self.tx.clone();
167 tokio::spawn(async move {
168 if let Err(e) = peripheral.subscribe(&characteristic).await {
169 tracing::error!("Failed to subscribe to gesture characteristics: {}", e);
170 return;
171 }
172
173 let mut notification_stream = match peripheral.notifications().await {
174 Ok(stream) => stream,
175 Err(e) => {
176 tracing::error!("Failed to get notification stream: {}", e);
177 return;
178 }
179 };
180
181 tracing::info!("Started listening for Simulator raw gestures");
182
183 while let Some(data) = notification_stream.next().await {
184 if let Ok(raw_str) = String::from_utf8(data.value)
185 && let Ok(raw_gest) = serde_json::from_str::<SimulatorRawGesture>(&raw_str)
186 {
187 let gesture = raw_gest.into_gesture();
188 let _ = tx.send(gesture);
189 }
190 }
191 });
192 }
193
194 pub async fn _untested_tauri_fallback_trigger(&self, raw: SimulatorRawGesture) {
196 let _ = self.tx.send(raw.into_gesture());
197 }
198}
199
200#[async_trait]
201impl RingBackend for SimulatorBackend {
202 async fn connect(&self) -> Result<(), String> {
203 tracing::info!("SimulatorBackend initializing connection sequence");
204 let peripheral = self.find_simulator().await?;
205
206 peripheral
207 .connect()
208 .await
209 .map_err(|e| format!("Connection failed: {}", e))?;
210 peripheral
211 .discover_services()
212 .await
213 .map_err(|e| format!("Service discovery failed: {}", e))?;
214
215 let chars = peripheral.characteristics();
216 let notify_char = chars
219 .iter()
220 .find(|c| c.properties.contains(CharPropFlags::NOTIFY));
221 let write_char = chars
222 .iter()
223 .find(|c| {
224 c.properties.contains(CharPropFlags::WRITE)
225 || c.properties.contains(CharPropFlags::WRITE_WITHOUT_RESPONSE)
226 })
227 .cloned();
228
229 *self.peripheral.lock().await = Some(peripheral.clone());
230 *self.tx_char.lock().await = write_char;
231
232 if let Some(c) = notify_char {
233 self.spawn_event_listener(peripheral, c.clone());
234 } else {
235 return Err("Simulator connected but no NOTIFY characteristic found; gestures will never be emitted.".to_string());
236 }
237
238 tracing::info!("SimulatorBackend successfully fully bound BLE channel");
239 Ok(())
240 }
241
242 async fn subscribe_to_gestures(&self) -> tokio::sync::broadcast::Receiver<Gesture> {
243 self.tx.subscribe()
244 }
245
246 async fn send_haptic(&self, pattern: HapticPattern, intensity: f32, duration_ms: u32) {
247 tracing::debug!(
248 "SimulatorBackend sending haptic: {:?} (int: {}, dur: {}ms)",
249 pattern,
250 intensity,
251 duration_ms
252 );
253
254 let p_lock = self.peripheral.lock().await;
255 let c_lock = self.tx_char.lock().await;
256
257 if let (Some(peripheral), Some(char)) = (&*p_lock, &*c_lock) {
258 let command_json = serde_json::json!({
259 "command": "trigger_haptic",
260 "pattern": pattern,
261 "intensity": intensity,
262 "duration_ms": duration_ms
263 })
264 .to_string();
265
266 let write_type = if char
270 .properties
271 .contains(CharPropFlags::WRITE_WITHOUT_RESPONSE)
272 {
273 btleplug::api::WriteType::WithoutResponse
274 } else {
275 btleplug::api::WriteType::WithResponse
276 };
277
278 if let Err(e) = peripheral
279 .write(char, command_json.as_bytes(), write_type)
280 .await
281 {
282 tracing::warn!(
283 pattern = ?pattern,
284 "Failed to send haptic feedback via BLE write: {}",
285 e
286 );
287 }
288 }
289 }
290
291 async fn get_status(&self) -> DeviceStatus {
292 let is_connected = self.peripheral.lock().await.is_some();
293 DeviceStatus {
294 battery: 100,
295 is_charging: false,
296 connection_state: if is_connected {
297 "simulator_ble_connected".to_string()
298 } else {
299 "simulator_disconnected".to_string()
300 },
301 }
302 }
303}