diff --git a/agent/src/protocol.rs b/agent/src/protocol.rs new file mode 100644 index 0000000..0164440 --- /dev/null +++ b/agent/src/protocol.rs @@ -0,0 +1,162 @@ +//! Message types for communication between the agent and the Butterfly server. +//! +//! These mirror the server's `WsMessage` enum (serde-tagged with `msg_type`). +//! The agent sends **Agent → Server** messages and receives **Server → Agent** messages. + +use serde::{Deserialize, Serialize}; + +// ── Agent → Server ──────────────────────────────────────────────────────────── + +/// A single display frame, base64-encoded JPEG. +#[derive(Debug, Clone, Serialize)] +pub struct DisplayFrame { + pub session_id: String, + pub data: String, + pub timestamp: Option, +} + +/// An audio chunk (future: PCM/Opus encoded). +#[derive(Debug, Clone, Serialize)] +pub struct AudioFrame { + pub session_id: String, + pub data: String, + pub timestamp: Option, +} + +/// Agent announces its capabilities on connect. +#[derive(Debug, Clone, Serialize)] +pub struct AgentInfo { + pub session_id: String, + pub agent_id: String, + pub resolution: Option, + pub hostname: String, +} + +/// Keep-alive ping. +#[derive(Debug, Clone, Serialize)] +pub struct Heartbeat; + +// ── Server → Agent ──────────────────────────────────────────────────────────── + +/// A HUD command forwarded from a viewer (mouse, keyboard, etc.). +#[derive(Debug, Clone, Deserialize)] +pub struct ForwardHudCommand { + pub command: String, + pub params: serde_json::Value, +} + +/// A resize request forwarded from a viewer. +#[derive(Debug, Clone, Deserialize)] +pub struct ForwardResize { + pub width: u32, + pub height: u32, +} + +/// Server tells the agent to start/stop streaming. +#[derive(Debug, Clone, Deserialize)] +pub struct StreamControl { + pub action: String, +} + +/// Generic acknowledgment from server. +#[derive(Debug, Clone, Deserialize)] +pub struct Ack { + pub message: String, +} + +/// Error from server. +#[derive(Debug, Clone, Deserialize)] +pub struct ErrorMsg { + pub message: String, +} + +// ── Unified message envelope ────────────────────────────────────────────────── + +/// All messages use a `msg_type` discriminator for routing. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "msg_type", rename_all = "snake_case")] +pub enum AgentWsMessage { + // Outgoing (Agent → Server) + #[serde(rename = "display_frame")] + DisplayFrame { + session_id: String, + data: String, + #[serde(skip_serializing_if = "Option::is_none")] + timestamp: Option, + }, + #[serde(rename = "audio_frame")] + AudioFrame { + session_id: String, + data: String, + #[serde(skip_serializing_if = "Option::is_none")] + timestamp: Option, + }, + #[serde(rename = "agent_info")] + AgentInfo { + session_id: String, + agent_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + resolution: Option, + #[serde(skip_serializing_if = "Option::is_none")] + hostname: Option, + }, + #[serde(rename = "heartbeat")] + Heartbeat, + + // Incoming (Server → Agent) — included for deserialization + #[serde(rename = "forward_hud_command")] + ForwardHudCommand { + command: String, + params: serde_json::Value, + }, + #[serde(rename = "forward_resize")] + ForwardResize { + width: u32, + height: u32, + }, + #[serde(rename = "stream_control")] + StreamControl { action: String }, + #[serde(rename = "ack")] + Ack { message: String }, + #[serde(rename = "error")] + Error { message: String }, +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +/// Build a display frame message. +pub fn display_frame_msg(session_id: &str, data: String) -> String { + let msg = AgentWsMessage::DisplayFrame { + session_id: session_id.to_string(), + data, + timestamp: None, + }; + serde_json::to_string(&msg).unwrap_or_default() +} + +/// Build an audio frame message. +pub fn audio_frame_msg(session_id: &str, data: String) -> String { + let msg = AgentWsMessage::AudioFrame { + session_id: session_id.to_string(), + data, + timestamp: None, + }; + serde_json::to_string(&msg).unwrap_or_default() +} + +/// Build an agent info message. +pub fn agent_info_msg(session_id: &str, agent_id: &str, resolution: Option<&str>, hostname: Option<&str>) -> String { + let msg = AgentWsMessage::AgentInfo { + session_id: session_id.to_string(), + agent_id: agent_id.to_string(), + resolution: resolution.map(String::from), + hostname: hostname.map(String::from), + }; + serde_json::to_string(&msg).unwrap_or_default() +} + +/// Build a heartbeat message. +pub fn heartbeat_msg() -> String { + let msg = AgentWsMessage::Heartbeat; + serde_json::to_string(&msg).unwrap_or_default() +}