netget/server/usb/keyboard/mod.rs
1//! USB HID Keyboard server implementation
2//!
3//! This module implements a virtual USB HID keyboard using the USB/IP protocol.
4//! The keyboard can be controlled by the LLM to type text, press keys, and handle
5//! key combinations (Ctrl+C, Alt+Tab, etc.).
6//!
7//! ## When the LLM call fails, the keyboard stays silent — deliberately
8//!
9//! Most protocols owe the peer an answer, and going quiet on an LLM failure leaves it hanging
10//! until its own timeout. A HID keyboard owes nothing. The host polls the interrupt IN endpoint
11//! and is NAKed whenever no key is down; a keyboard with nothing to report is the *normal*
12//! state of every keyboard in the world, not an error condition. There is no HID vocabulary for
13//! "the device's brain is unreachable" either: STALLing the interrupt endpoint is a fault
14//! condition that makes a host reset or unbind the device, which is a worse and less truthful
15//! answer than reporting no keystrokes. (`usbip` also has no way to express it — returning
16//! `Err` from `handle_urb` aborts the USB/IP session for the whole device.)
17//!
18//! The fail-closed property that *does* matter here is the one this direction makes free: a
19//! failed LLM call must never put a keystroke on the wire. Nothing below can synthesise a
20//! report; reports exist only where an action produced them.
21//!
22//! So the failure is dual-logged at ERROR and nothing is sent. The log carries a `decision=`
23//! tag so the three outcomes an operator would otherwise see as one quiet keyboard stay apart:
24//! `decision=fail_closed_llm_error` (the backend failed), `decision=model_no_actions` (the model answered
25//! and asked for no keystrokes) and `decision=model_actions` (something was typed).
26//! `tests/server/usb_keyboard/llm_failure_test.rs` pins both halves.
27
28pub mod actions;
29
30#[cfg(all(unix, feature = "libusb"))]
31pub mod handler;
32
33// Re-export protocol struct for registration
34#[cfg(all(unix, feature = "libusb"))]
35pub use actions::UsbKeyboardProtocol;
36
37#[cfg(all(unix, feature = "libusb"))]
38use anyhow::Result;
39#[cfg(all(unix, feature = "libusb"))]
40use std::collections::HashMap;
41#[cfg(all(unix, feature = "libusb"))]
42use std::net::SocketAddr;
43#[cfg(all(unix, feature = "libusb"))]
44use std::sync::Arc;
45#[cfg(all(unix, feature = "libusb"))]
46use tokio::sync::{mpsc, Mutex};
47#[cfg(all(unix, feature = "libusb"))]
48use tracing::{debug, error, info};
49
50#[cfg(all(unix, feature = "libusb"))]
51use crate::console_error;
52#[cfg(all(unix, feature = "libusb"))]
53use crate::llm::action_helper::call_llm;
54#[cfg(all(unix, feature = "libusb"))]
55use crate::llm::ollama_client::OllamaClient;
56#[cfg(all(unix, feature = "libusb"))]
57use crate::protocol::Event;
58#[cfg(all(unix, feature = "libusb"))]
59use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter};
60#[cfg(all(unix, feature = "libusb"))]
61use crate::server::connection::ConnectionId;
62#[cfg(all(unix, feature = "libusb"))]
63use crate::server::usb::guard::{MAX_USBIP_CONNECTIONS, USBIP_NO_REFUSAL};
64#[cfg(all(unix, feature = "libusb"))]
65use crate::state::app_state::AppState;
66#[cfg(all(unix, feature = "libusb"))]
67use actions::{
68 USB_KEYBOARD_ATTACHED_EVENT, USB_KEYBOARD_DETACHED_EVENT, USB_KEYBOARD_LED_STATUS_EVENT,
69};
70#[cfg(all(unix, feature = "libusb"))]
71use handler::{LedState, NetGetKeyboardHandler};
72
73/// Connection state for LLM processing
74#[cfg(all(unix, feature = "libusb"))]
75#[derive(Debug, Clone, PartialEq)]
76#[allow(dead_code)]
77enum ConnectionState {
78 Idle,
79 Processing,
80 Accumulating,
81}
82
83/// Per-connection data for USB keyboard
84#[cfg(all(unix, feature = "libusb"))]
85struct ConnectionData {
86 state: ConnectionState,
87 #[allow(dead_code)]
88 memory: String,
89 /// Last LED byte the host set, kept so the TUI/state view can show it.
90 led_status: u8,
91}
92
93/// USB HID Keyboard server
94#[cfg(all(unix, feature = "libusb"))]
95pub struct UsbKeyboardServer;
96
97#[cfg(all(unix, feature = "libusb"))]
98impl UsbKeyboardServer {
99 /// Spawn the USB keyboard server with LLM integration
100 ///
101 /// This creates a USB/IP server that exports a virtual HID keyboard device.
102 /// The LLM can control the keyboard through actions like type_text and press_key.
103 pub async fn spawn_with_llm_actions(
104 listen_addr: SocketAddr,
105 llm_client: OllamaClient,
106 app_state: Arc<AppState>,
107 status_tx: mpsc::UnboundedSender<String>,
108 server_id: crate::state::ServerId,
109 first_byte_timeout_secs: Option<u64>,
110 idle_timeout_secs: Option<u64>,
111 ) -> Result<SocketAddr> {
112 // Create and bind TCP server for USB/IP protocol
113 let listener =
114 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
115 let local_addr = listener.local_addr()?;
116 info!("USB Keyboard server listening on {}", local_addr);
117 let _ = status_tx.send(format!("USB Keyboard server listening on {}", local_addr));
118
119 let connections = Arc::new(Mutex::new(HashMap::new()));
120 let protocol = Arc::new(crate::server::usb::keyboard::UsbKeyboardProtocol::new());
121
122 // The two read deadlines, argued in `src/server/usb/guard.rs` and overridable per
123 // server. Copied into each connection task; the screen is the only thing that reads
124 // from the socket, so it is the only place they can be applied.
125 let deadlines = crate::server::usb::guard::UsbIpDeadlines::from_secs(
126 first_byte_timeout_secs,
127 idle_timeout_secs,
128 );
129 let task_registrar = app_state.clone();
130 // One emulated device per connection, so the cap is small and lives with the screen.
131 let limiter = ConnectionLimiter::new(MAX_USBIP_CONNECTIONS);
132 // Spawn accept loop for USB/IP connections
133 let accept_handle = tokio::spawn(async move {
134 loop {
135 match accept_bounded(
136 &listener,
137 &limiter,
138 USBIP_NO_REFUSAL,
139 "USB keyboard",
140 Some(&status_tx),
141 )
142 .await
143 {
144 Ok((stream, remote_addr, permit)) => {
145 let connection_id =
146 ConnectionId::new(app_state.get_next_unified_id().await);
147 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
148 info!(
149 "USB/IP connection {} from {} (USB keyboard device)",
150 connection_id, remote_addr
151 );
152
153 // Add connection to ServerInstance
154 use crate::state::server::{
155 ConnectionState as ServerConnectionState, ConnectionStatus,
156 ProtocolConnectionInfo,
157 };
158 let now = crate::utils::clock::Instant::now();
159 let conn_state = ServerConnectionState {
160 id: connection_id,
161 remote_addr,
162 local_addr: local_addr_conn,
163 bytes_sent: 0,
164 bytes_received: 0,
165 packets_sent: 0,
166 packets_received: 0,
167 last_activity: now,
168 status: ConnectionStatus::Active,
169 status_changed_at: now,
170 protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
171 "state": "WaitingForImport",
172 "led_status": 0
173 })),
174 };
175 app_state
176 .add_connection_to_server(server_id, conn_state)
177 .await;
178 let _ = status_tx.send("__UPDATE_UI__".to_string());
179
180 // Handle USB/IP connection
181 let llm_client_clone = llm_client.clone();
182 let app_state_clone = app_state.clone();
183 let status_tx_clone = status_tx.clone();
184 let connections_clone = connections.clone();
185 let protocol_clone = protocol.clone();
186
187 // Tracked, not detached: stop_server must abort this task too.
188 let task_owner = app_state.clone();
189 task_owner
190 .spawn_server_task(server_id, async move {
191 // Held for the life of the connection: dropping it early
192 // releases the slot while the device is still exported.
193 let _permit = permit;
194 if let Err(e) = Self::handle_connection(
195 stream,
196 connection_id,
197 remote_addr,
198 llm_client_clone,
199 app_state_clone,
200 status_tx_clone,
201 connections_clone,
202 protocol_clone,
203 server_id,
204 deadlines,
205 )
206 .await
207 {
208 error!(
209 "USB keyboard connection {} error: {}",
210 connection_id, e
211 );
212 }
213 })
214 .await;
215 }
216 Err(e) => {
217 // A persistent accept error recurs immediately, so continuing spins a
218 // hot loop on an unbounded status channel. Give up the listener.
219 error!("USB keyboard accept failed, stopping accept loop: {}", e);
220 break;
221 }
222 }
223 }
224 });
225
226 // Without this, stop_server has no handle to abort and the listener stays bound.
227 task_registrar
228 .register_server_task(server_id, accept_handle)
229 .await;
230
231 Ok(local_addr)
232 }
233
234 /// Handle USB/IP server lifecycle
235 ///
236 /// This creates a USB/IP server that exports a virtual HID keyboard device.
237 /// The server handles USB/IP protocol operations and integrates with LLM actions.
238 #[allow(clippy::too_many_arguments)]
239 async fn handle_connection(
240 stream: tokio::net::TcpStream,
241 connection_id: ConnectionId,
242 remote_addr: SocketAddr,
243 llm_client: OllamaClient,
244 app_state: Arc<AppState>,
245 status_tx: mpsc::UnboundedSender<String>,
246 connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
247 protocol: Arc<crate::server::usb::keyboard::UsbKeyboardProtocol>,
248 server_id: crate::state::ServerId,
249 deadlines: crate::server::usb::guard::UsbIpDeadlines,
250 ) -> Result<()> {
251 info!(
252 "USB keyboard connection {} from {} - device ready for USB/IP import",
253 connection_id, remote_addr
254 );
255
256 let local_addr = stream.local_addr().unwrap_or(remote_addr);
257
258 // Initialize connection data
259 connections.lock().await.insert(
260 connection_id,
261 ConnectionData {
262 state: ConnectionState::Idle,
263 memory: String::new(),
264 led_status: 0,
265 },
266 );
267
268 // LED reports arrive on a synchronous URB callback, which cannot raise an event by
269 // itself. The channel is the seam between the two, exactly as in usb/msc.
270 let (led_tx, mut led_rx) = mpsc::unbounded_channel::<LedState>();
271
272 // A wrapper around the crate's keyboard handler: it keeps the key-down/key-up state
273 // machine but takes over endpoint 0, so LED SET_REPORTs are seen (the crate discards
274 // them, which is why usb_keyboard_led_status could never fire) and no control request
275 // reaches the crate's `unimplemented!()`.
276 let handler = Arc::new(std::sync::Mutex::new(Box::new(
277 NetGetKeyboardHandler::new().with_led_events(led_tx),
278 )
279 as Box<dyn usbip::UsbInterfaceHandler + Send>));
280
281 // Store handler in protocol for action execution
282 protocol.set_handler(connection_id, handler.clone());
283
284 // Create USB device with HID keyboard interface
285 let device = usbip::UsbDevice::new(0).with_interface(
286 usbip::ClassCode::HID as u8,
287 0x00, // Subclass: no subclass
288 0x00, // Protocol: none
289 Some("NetGet Virtual Keyboard"),
290 vec![usbip::UsbEndpoint {
291 address: 0x81, // EP1 IN (interrupt)
292 attributes: 0x03, // Interrupt transfer
293 max_packet_size: 0x08, // 8 bytes (keyboard report)
294 interval: 10, // 10ms polling interval
295 }],
296 handler.clone(),
297 );
298
299 let usbip_server = Arc::new(usbip::UsbIpServer::new_simulated(vec![device]));
300
301 info!(
302 "USB keyboard device ready for connection {} from {}",
303 connection_id, remote_addr
304 );
305 let _ = status_tx.send(format!(
306 "USB keyboard ready on {} - run: sudo usbip attach -r {} -b 0-0-0",
307 local_addr,
308 local_addr.ip()
309 ));
310
311 // Drive the USB/IP protocol on the socket we already accepted.
312 //
313 // This deliberately does not call `usbip::server()`: that binds a *second*
314 // listener, which forced a hardcoded port (3240) and limited the protocol to one
315 // instance per host. `usbip::handler` speaks the same protocol over an existing
316 // socket, so the netget listener is the USB/IP listener and the port is whatever
317 // the caller asked for.
318 // Not `usbip::handler(&mut stream, ...)` directly: the crate allocates from the peer's
319 // `transfer_buffer_length` and `number_of_packets` without checking either, on a socket
320 // USB/IP never authenticates. `run_guarded_usbip` screens both before a byte reaches it
321 // -- see `src/server/usb/guard.rs`.
322 //
323 // The attach event hangs off `import_rx`, not off the accept: see the note above
324 // `import_pending` below.
325 let guard_status_tx = status_tx.clone();
326 let (import_tx, mut import_rx) = tokio::sync::oneshot::channel();
327 let usbip_task = tokio::spawn(async move {
328 match crate::server::usb::guard::run_guarded_usbip(
329 stream,
330 usbip_server,
331 "USB keyboard",
332 connection_id.to_string(),
333 guard_status_tx,
334 Some(import_tx),
335 deadlines,
336 )
337 .await
338 {
339 Ok(()) => debug!(
340 "USB/IP session ended for keyboard connection {}",
341 connection_id
342 ),
343 Err(e) => debug!(
344 "USB/IP session for keyboard connection {} ended with error: {}",
345 connection_id, e
346 ),
347 }
348 });
349
350 // Serve the host until the USB/IP session ends. This loop is why
351 // usb_keyboard_led_status can fire at all: the previous version awaited the session
352 // handle directly and never looked at the LED channel.
353 //
354 // It is also where the attach event is raised. USB/IP authenticates nothing, so a bare
355 // TCP connect must cost nothing: `import_rx` fires only once the screen has admitted an
356 // `OP_REQ_IMPORT`, which is the first moment the peer has asked for the keyboard.
357 // `import_pending` is the select! guard — a `oneshot::Receiver` must not be polled
358 // again once it has resolved — and `imported` is the record, because the detach event
359 // is the other half of an attachment and must not fire without one.
360 let mut usbip_task = usbip_task;
361 let mut import_pending = true;
362 let mut imported = false;
363 loop {
364 tokio::select! {
365 asked = &mut import_rx, if import_pending => {
366 import_pending = false;
367 if asked.is_ok() {
368 imported = true;
369 if let Err(e) = Self::call_llm_on_attach(
370 connection_id,
371 &llm_client,
372 &app_state,
373 &status_tx,
374 &connections,
375 &protocol,
376 server_id,
377 )
378 .await
379 {
380 error!(
381 "Failed to call LLM on keyboard attach for connection {}: {}",
382 connection_id, e
383 );
384 }
385 }
386 }
387 received = led_rx.recv() => {
388 let Some(mut leds) = received else { break };
389 // A burst of LED writes (Caps then Num within a millisecond) is folded
390 // into the last state, so one keypress does not become several LLM calls.
391 while let Ok(newer) = led_rx.try_recv() {
392 leds = newer;
393 }
394 Self::call_llm_on_led_status(
395 connection_id,
396 leds,
397 &llm_client,
398 &app_state,
399 &status_tx,
400 &connections,
401 &protocol,
402 server_id,
403 )
404 .await;
405 }
406 _ = &mut usbip_task => break,
407 }
408 }
409
410 info!(
411 "USB keyboard host detached on connection {} from {}",
412 connection_id, remote_addr
413 );
414
415 // Call LLM on device detach — only for a host that actually attached. A peer that
416 // opened a socket and closed it again detached nothing.
417 if imported {
418 if let Err(e) = Self::call_llm_on_detach(
419 connection_id,
420 &llm_client,
421 &app_state,
422 &status_tx,
423 &connections,
424 &protocol,
425 server_id,
426 )
427 .await
428 {
429 error!(
430 "Failed to call LLM on keyboard detach for connection {}: {}",
431 connection_id, e
432 );
433 }
434 }
435
436 // The USB/IP session owned this handler; drop it so a later connection with the
437 // same id cannot reach a dead device.
438 protocol.remove_handler(connection_id);
439 connections.lock().await.remove(&connection_id);
440
441 Ok(())
442 }
443
444 /// Raise `usb_keyboard_led_status` after the host changed a lock LED.
445 ///
446 /// This is the event's only emit site, and it did not exist before: the event was declared,
447 /// advertised to the model with a full action vocabulary, and could never fire, because the
448 /// crate's keyboard handler discards HID output reports.
449 #[allow(clippy::too_many_arguments)]
450 async fn call_llm_on_led_status(
451 connection_id: ConnectionId,
452 leds: LedState,
453 llm_client: &OllamaClient,
454 app_state: &Arc<AppState>,
455 status_tx: &mpsc::UnboundedSender<String>,
456 connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
457 protocol: &Arc<crate::server::usb::keyboard::UsbKeyboardProtocol>,
458 server_id: crate::state::ServerId,
459 ) {
460 {
461 let mut conns = connections.lock().await;
462 match conns.get_mut(&connection_id) {
463 Some(conn) if conn.state != ConnectionState::Idle => {
464 debug!(
465 "USB keyboard connection {} already processing, skipping LED event",
466 connection_id
467 );
468 return;
469 }
470 Some(conn) => {
471 conn.state = ConnectionState::Processing;
472 conn.led_status = leds.raw;
473 }
474 None => {}
475 }
476 }
477
478 let event = Event::new(
479 &USB_KEYBOARD_LED_STATUS_EVENT,
480 serde_json::json!({
481 "connection_id": connection_id.to_string(),
482 "num_lock": leds.num_lock(),
483 "caps_lock": leds.caps_lock(),
484 "scroll_lock": leds.scroll_lock(),
485 }),
486 );
487
488 match call_llm(
489 llm_client,
490 app_state,
491 server_id,
492 Some(connection_id),
493 &event,
494 protocol.as_ref(),
495 )
496 .await
497 {
498 Ok(execution_result) => {
499 let acted =
500 execution_result.protocol_results.len() + execution_result.raw_actions.len();
501 info!(
502 "USB keyboard LLM call completed (led_status) for connection {} decision={} actions={}",
503 connection_id,
504 if acted == 0 {
505 "model_no_actions"
506 } else {
507 "model_actions"
508 },
509 acted
510 );
511 }
512 Err(e) => console_error!(
513 status_tx,
514 "LLM call failed for USB keyboard led_status on connection {} decision=fail_closed_llm_error: \
515 {}; the keyboard stays silent (see the module docs for why silence is the \
516 refusal here)",
517 connection_id,
518 e
519 ),
520 }
521
522 let mut conns = connections.lock().await;
523 if let Some(conn) = conns.get_mut(&connection_id) {
524 conn.state = ConnectionState::Idle;
525 }
526 }
527
528 /// Call LLM when the USB/IP host detaches.
529 ///
530 /// `usb_keyboard_detached` is declared `with_no_actions()`, so the model's vocabulary
531 /// here is the common action set (`show_message`, memory operations); there is no
532 /// wire left to write to.
533 async fn call_llm_on_detach(
534 connection_id: ConnectionId,
535 llm_client: &OllamaClient,
536 app_state: &Arc<AppState>,
537 status_tx: &mpsc::UnboundedSender<String>,
538 connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
539 protocol: &Arc<crate::server::usb::keyboard::UsbKeyboardProtocol>,
540 server_id: crate::state::ServerId,
541 ) -> Result<()> {
542 {
543 let mut conns = connections.lock().await;
544 if let Some(conn_data) = conns.get_mut(&connection_id) {
545 conn_data.state = ConnectionState::Processing;
546 }
547 }
548
549 let event = Event::new(
550 &USB_KEYBOARD_DETACHED_EVENT,
551 serde_json::json!({
552 "connection_id": connection_id.to_string(),
553 }),
554 );
555
556 let result = call_llm(
557 llm_client,
558 app_state,
559 server_id,
560 Some(connection_id),
561 &event,
562 protocol.as_ref(),
563 )
564 .await;
565
566 match &result {
567 Err(e) => console_error!(
568 status_tx,
569 "LLM call failed for USB keyboard detach on connection {} decision=fail_closed_llm_error: {}",
570 connection_id,
571 e
572 ),
573 Ok(execution_result) => {
574 let acted =
575 execution_result.protocol_results.len() + execution_result.raw_actions.len();
576 info!(
577 "USB keyboard detach LLM call completed for connection {} decision={} actions={}",
578 connection_id,
579 if acted == 0 {
580 "model_no_actions"
581 } else {
582 "model_actions"
583 },
584 acted
585 );
586 }
587 }
588
589 let mut conns = connections.lock().await;
590 if let Some(conn_data) = conns.get_mut(&connection_id) {
591 conn_data.state = ConnectionState::Idle;
592 }
593
594 Ok(())
595 }
596
597 /// Call LLM when device is attached
598 async fn call_llm_on_attach(
599 connection_id: ConnectionId,
600 llm_client: &OllamaClient,
601 app_state: &Arc<AppState>,
602 status_tx: &mpsc::UnboundedSender<String>,
603 connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
604 protocol: &Arc<crate::server::usb::keyboard::UsbKeyboardProtocol>,
605 server_id: crate::state::ServerId,
606 ) -> Result<()> {
607 // Check if already processing
608 {
609 let conns = connections.lock().await;
610 if let Some(conn_data) = conns.get(&connection_id) {
611 if conn_data.state != ConnectionState::Idle {
612 debug!(
613 "USB keyboard connection {} already processing, skipping LLM call",
614 connection_id
615 );
616 return Ok(());
617 }
618 }
619 }
620
621 // Set state to processing
622 {
623 let mut conns = connections.lock().await;
624 if let Some(conn_data) = conns.get_mut(&connection_id) {
625 conn_data.state = ConnectionState::Processing;
626 }
627 }
628
629 // Get instruction and memory
630 let (_instruction, _memory) = {
631 if let Some(server) = app_state.get_server(server_id).await {
632 (server.instruction.clone(), String::new())
633 } else {
634 return Err(anyhow::anyhow!("Server not found"));
635 }
636 };
637
638 // Create attached event
639 let event = Event::new(
640 &USB_KEYBOARD_ATTACHED_EVENT,
641 serde_json::json!({
642 "connection_id": connection_id.to_string(),
643 }),
644 );
645
646 info!(
647 "Calling LLM for USB keyboard attached event on connection {}",
648 connection_id
649 );
650
651 // Call LLM
652 let result = call_llm(
653 llm_client,
654 app_state,
655 server_id,
656 Some(connection_id),
657 &event,
658 protocol.as_ref(),
659 )
660 .await;
661
662 // Process result
663 match result {
664 Ok(execution_result) => {
665 // Actions have already been executed by call_llm. Whether the model actually
666 // asked for keystrokes is the distinction an operator needs in the log: a
667 // model that answered with nothing looks exactly like a working keyboard
668 // nobody is typing on, and so does a backend outage. Tag all three.
669 let acted =
670 execution_result.protocol_results.len() + execution_result.raw_actions.len();
671 if acted == 0 {
672 info!(
673 "USB keyboard LLM call completed for connection {} decision=model_no_actions (nothing typed)",
674 connection_id
675 );
676 } else {
677 info!(
678 "USB keyboard LLM call completed for connection {} decision=model_actions actions={}",
679 connection_id, acted
680 );
681 }
682
683 // Set state back to idle
684 let mut conns = connections.lock().await;
685 if let Some(conn_data) = conns.get_mut(&connection_id) {
686 conn_data.state = ConnectionState::Idle;
687 }
688 }
689 Err(e) => {
690 // Silence is the correct wire behaviour here, and it is deliberate — see the
691 // module docs. What must not happen is that it goes unrecorded, so this is
692 // dual-logged at ERROR rather than only reaching `netget.log`.
693 console_error!(
694 status_tx,
695 "LLM call failed for USB keyboard connection {} decision=fail_closed_llm_error: {}; no \
696 HID report will be sent, so the host sees a keyboard nobody is typing on",
697 connection_id,
698 e
699 );
700
701 // Set state back to idle
702 let mut conns = connections.lock().await;
703 if let Some(conn_data) = conns.get_mut(&connection_id) {
704 conn_data.state = ConnectionState::Idle;
705 }
706 }
707 }
708
709 Ok(())
710 }
711}