pub mod actions;
#[cfg(all(unix, feature = "libusb"))]
pub mod handler;
#[cfg(all(unix, feature = "libusb"))]
pub use actions::UsbMouseProtocol;
#[cfg(all(unix, feature = "libusb"))]
use anyhow::Result;
#[cfg(all(unix, feature = "libusb"))]
use std::collections::HashMap;
#[cfg(all(unix, feature = "libusb"))]
use std::net::SocketAddr;
#[cfg(all(unix, feature = "libusb"))]
use std::sync::Arc;
#[cfg(all(unix, feature = "libusb"))]
use tokio::sync::{mpsc, Mutex};
#[cfg(all(unix, feature = "libusb"))]
use tracing::{debug, error, info};
#[cfg(all(unix, feature = "libusb"))]
use crate::console_error;
#[cfg(all(unix, feature = "libusb"))]
use crate::llm::action_helper::call_llm;
#[cfg(all(unix, feature = "libusb"))]
use crate::llm::ollama_client::OllamaClient;
#[cfg(all(unix, feature = "libusb"))]
use crate::protocol::Event;
#[cfg(all(unix, feature = "libusb"))]
use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter};
#[cfg(all(unix, feature = "libusb"))]
use crate::server::connection::ConnectionId;
#[cfg(all(unix, feature = "libusb"))]
use crate::server::usb::guard::{MAX_USBIP_CONNECTIONS, USBIP_NO_REFUSAL};
#[cfg(all(unix, feature = "libusb"))]
use crate::state::app_state::AppState;
#[cfg(all(unix, feature = "libusb"))]
use actions::{USB_MOUSE_ATTACHED_EVENT, USB_MOUSE_DETACHED_EVENT};
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
enum ConnectionState {
Idle,
Processing,
Accumulating,
}
#[cfg(all(unix, feature = "libusb"))]
struct ConnectionData {
state: ConnectionState,
#[allow(dead_code)]
memory: String,
}
#[cfg(all(unix, feature = "libusb"))]
pub struct UsbMouseServer;
#[cfg(all(unix, feature = "libusb"))]
impl UsbMouseServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
info!("USB Mouse server listening on {}", local_addr);
let _ = status_tx.send(format!("USB Mouse server listening on {}", local_addr));
let connections = Arc::new(Mutex::new(HashMap::new()));
let protocol = Arc::new(crate::server::usb::mouse::UsbMouseProtocol::new());
let deadlines = crate::server::usb::guard::UsbIpDeadlines::from_secs(
first_byte_timeout_secs,
idle_timeout_secs,
);
let task_registrar = app_state.clone();
let limiter = ConnectionLimiter::new(MAX_USBIP_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match accept_bounded(
&listener,
&limiter,
USBIP_NO_REFUSAL,
"USB mouse",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
info!(
"USB/IP connection {} from {} (USB mouse device)",
connection_id, remote_addr
);
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
"state": "WaitingForImport"
})),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let connections_clone = connections.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
connection_id,
remote_addr,
llm_client_clone,
app_state_clone,
status_tx_clone,
connections_clone,
protocol_clone,
server_id,
deadlines,
)
.await
{
error!("USB mouse connection {} error: {}", connection_id, e);
}
})
.await;
}
Err(e) => {
error!("USB mouse accept failed, stopping accept loop: {}", e);
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: tokio::net::TcpStream,
connection_id: ConnectionId,
remote_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<crate::server::usb::mouse::UsbMouseProtocol>,
server_id: crate::state::ServerId,
deadlines: crate::server::usb::guard::UsbIpDeadlines,
) -> Result<()> {
info!(
"USB mouse connection {} from {} - device ready for USB/IP import",
connection_id, remote_addr
);
connections.lock().await.insert(
connection_id,
ConnectionData {
state: ConnectionState::Idle,
memory: String::new(),
},
);
let local_addr = stream.local_addr().unwrap_or(remote_addr);
let handler = Arc::new(std::sync::Mutex::new(
Box::new(handler::UsbHidMouseHandler::new())
as Box<dyn usbip::UsbInterfaceHandler + Send>,
));
protocol.set_handler(connection_id, handler.clone()).await;
let device = usbip::UsbDevice::new(0).with_interface(
usbip::ClassCode::HID as u8,
0x01, 0x02, Some("NetGet Virtual Mouse"),
handler::UsbHidMouseHandler::endpoints(),
handler.clone(),
);
let usbip_server = Arc::new(usbip::UsbIpServer::new_simulated(vec![device]));
let _ = status_tx.send(format!(
"USB mouse ready on {} - run: sudo usbip attach -r {} -b 0-0-0",
local_addr,
local_addr.ip()
));
let guard_status_tx = status_tx.clone();
let (import_tx, mut import_rx) = tokio::sync::oneshot::channel();
let usbip_task = tokio::spawn(async move {
match crate::server::usb::guard::run_guarded_usbip(
stream,
usbip_server,
"USB mouse",
connection_id.to_string(),
guard_status_tx,
Some(import_tx),
deadlines,
)
.await
{
Ok(()) => debug!(
"USB/IP session ended for mouse connection {}",
connection_id
),
Err(e) => debug!(
"USB/IP session for mouse connection {} ended with error: {}",
connection_id, e
),
}
});
let mut usbip_task = usbip_task;
let mut import_pending = true;
let mut imported = false;
loop {
tokio::select! {
asked = &mut import_rx, if import_pending => {
import_pending = false;
if asked.is_ok() {
imported = true;
if let Err(e) = Self::call_llm_on_attach(
connection_id,
&llm_client,
&app_state,
&status_tx,
&connections,
&protocol,
server_id,
)
.await
{
error!(
"Failed to call LLM on mouse attach for connection {}: {}",
connection_id, e
);
}
}
}
_ = &mut usbip_task => break,
}
}
info!(
"USB mouse host detached on connection {} from {}",
connection_id, remote_addr
);
if imported {
Self::call_llm_on_detach(
connection_id,
&llm_client,
&app_state,
&status_tx,
&connections,
&protocol,
server_id,
)
.await;
}
protocol.remove_handler(connection_id).await;
connections.lock().await.remove(&connection_id);
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
Ok(())
}
async fn call_llm_on_detach(
connection_id: ConnectionId,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: &Arc<crate::server::usb::mouse::UsbMouseProtocol>,
server_id: crate::state::ServerId,
) {
{
let mut conns = connections.lock().await;
if let Some(conn) = conns.get_mut(&connection_id) {
conn.state = ConnectionState::Processing;
}
}
let event = Event::new(
&USB_MOUSE_DETACHED_EVENT,
serde_json::json!({ "connection_id": connection_id.to_string() }),
);
match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
let acted = !execution_result.protocol_results.is_empty()
|| !execution_result.raw_actions.is_empty();
info!(
"USB mouse LLM call completed (detach) for connection {} decision={}",
connection_id,
if acted { "model_action" } else { "no_action" }
)
}
Err(e) => console_error!(
status_tx,
"LLM call failed for USB mouse detach on connection {} \
decision=fail_closed_llm_error: {}",
connection_id,
e
),
}
let mut conns = connections.lock().await;
if let Some(conn) = conns.get_mut(&connection_id) {
conn.state = ConnectionState::Idle;
}
}
async fn call_llm_on_attach(
connection_id: ConnectionId,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: &Arc<crate::server::usb::mouse::UsbMouseProtocol>,
server_id: crate::state::ServerId,
) -> Result<()> {
{
let conns = connections.lock().await;
if let Some(conn_data) = conns.get(&connection_id) {
if conn_data.state != ConnectionState::Idle {
debug!(
"USB mouse connection {} already processing, skipping LLM call",
connection_id
);
return Ok(());
}
}
}
{
let mut conns = connections.lock().await;
if let Some(conn_data) = conns.get_mut(&connection_id) {
conn_data.state = ConnectionState::Processing;
}
}
let (_instruction, _memory) = {
if let Some(server) = app_state.get_server(server_id).await {
(server.instruction.clone(), String::new())
} else {
return Err(anyhow::anyhow!("Server not found"));
}
};
let event = Event::new(
&USB_MOUSE_ATTACHED_EVENT,
serde_json::json!({
"connection_id": connection_id.to_string(),
}),
);
info!(
"Calling LLM for USB mouse attached event on connection {}",
connection_id
);
let result = call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await;
match result {
Ok(execution_result) => {
let acted = !execution_result.protocol_results.is_empty()
|| !execution_result.raw_actions.is_empty();
info!(
"USB mouse LLM call completed for connection {} decision={} ({} action \
results)",
connection_id,
if acted { "model_action" } else { "no_action" },
execution_result.protocol_results.len()
);
let mut conns = connections.lock().await;
if let Some(conn_data) = conns.get_mut(&connection_id) {
conn_data.state = ConnectionState::Idle;
}
}
Err(e) => {
console_error!(
status_tx,
"LLM call failed for USB mouse connection {} decision=fail_closed_llm_error: \
{}; no HID report will be sent, so the host's pointer does not move",
connection_id,
e
);
let mut conns = connections.lock().await;
if let Some(conn_data) = conns.get_mut(&connection_id) {
conn_data.state = ConnectionState::Idle;
}
}
}
Ok(())
}
}