bbx_net/websocket/
protocol.rs

1//! WebSocket JSON message protocol.
2
3use serde::{Deserialize, Serialize};
4
5/// Incoming message from client (phone).
6#[derive(Debug, Clone, Deserialize)]
7#[serde(tag = "type")]
8pub enum ClientMessage {
9    /// Join a room with room code.
10    #[serde(rename = "join")]
11    Join {
12        room_code: String,
13        client_name: Option<String>,
14    },
15
16    /// Parameter value change.
17    #[serde(rename = "param")]
18    Parameter {
19        param: String,
20        value: f32,
21        /// Optional scheduled time (microseconds since session start).
22        #[serde(default)]
23        at: Option<u64>,
24    },
25
26    /// Trigger event (momentary).
27    #[serde(rename = "trigger")]
28    Trigger {
29        name: String,
30        #[serde(default)]
31        at: Option<u64>,
32    },
33
34    /// Request current state.
35    #[serde(rename = "sync")]
36    Sync,
37
38    /// Ping for latency measurement.
39    #[serde(rename = "ping")]
40    Ping { client_time: u64 },
41
42    /// Leave current room.
43    #[serde(rename = "leave")]
44    Leave,
45}
46
47/// Outgoing message to client.
48#[derive(Debug, Clone, Serialize)]
49#[serde(tag = "type")]
50pub enum ServerMessage {
51    /// Welcome message after joining.
52    #[serde(rename = "welcome")]
53    Welcome { node_id: String, server_time: u64 },
54
55    /// Current parameter state.
56    #[serde(rename = "state")]
57    State { params: Vec<ParamState> },
58
59    /// Parameter update notification (bidirectional sync).
60    #[serde(rename = "update")]
61    Update { param: String, value: f32 },
62
63    /// Pong response.
64    #[serde(rename = "pong")]
65    Pong { client_time: u64, server_time: u64 },
66
67    /// Error notification.
68    #[serde(rename = "error")]
69    Error { code: String, message: String },
70
71    /// Room closed notification.
72    #[serde(rename = "closed")]
73    RoomClosed,
74}
75
76/// Parameter state for sync responses.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ParamState {
79    pub name: String,
80    pub value: f32,
81    #[serde(default)]
82    pub min: f32,
83    #[serde(default = "default_max")]
84    pub max: f32,
85}
86
87fn default_max() -> f32 {
88    1.0
89}
90
91impl ParamState {
92    pub fn new(name: impl Into<String>, value: f32) -> Self {
93        Self {
94            name: name.into(),
95            value,
96            min: 0.0,
97            max: 1.0,
98        }
99    }
100
101    pub fn with_range(mut self, min: f32, max: f32) -> Self {
102        self.min = min;
103        self.max = max;
104        self
105    }
106}
107
108impl ServerMessage {
109    /// Create an error message.
110    pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
111        Self::Error {
112            code: code.into(),
113            message: message.into(),
114        }
115    }
116
117    /// Create an invalid room code error.
118    pub fn invalid_room_code() -> Self {
119        Self::error("INVALID_ROOM", "Invalid room code")
120    }
121
122    /// Create a room full error.
123    pub fn room_full() -> Self {
124        Self::error("ROOM_FULL", "Room is at capacity")
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_client_message_join() {
134        let json = r#"{"type": "join", "room_code": "123456"}"#;
135        let msg: ClientMessage = serde_json::from_str(json).unwrap();
136
137        match msg {
138            ClientMessage::Join { room_code, .. } => {
139                assert_eq!(room_code, "123456");
140            }
141            _ => panic!("Expected Join message"),
142        }
143    }
144
145    #[test]
146    fn test_client_message_param() {
147        let json = r#"{"type": "param", "param": "gain", "value": 0.5}"#;
148        let msg: ClientMessage = serde_json::from_str(json).unwrap();
149
150        match msg {
151            ClientMessage::Parameter { param, value, at } => {
152                assert_eq!(param, "gain");
153                assert!((value - 0.5).abs() < f32::EPSILON);
154                assert!(at.is_none());
155            }
156            _ => panic!("Expected Parameter message"),
157        }
158    }
159
160    #[test]
161    fn test_client_message_param_with_time() {
162        let json = r#"{"type": "param", "param": "freq", "value": 440.0, "at": 1000000}"#;
163        let msg: ClientMessage = serde_json::from_str(json).unwrap();
164
165        match msg {
166            ClientMessage::Parameter { at, .. } => {
167                assert_eq!(at, Some(1000000));
168            }
169            _ => panic!("Expected Parameter message"),
170        }
171    }
172
173    #[test]
174    fn test_client_message_trigger() {
175        let json = r#"{"type": "trigger", "name": "note_on"}"#;
176        let msg: ClientMessage = serde_json::from_str(json).unwrap();
177
178        match msg {
179            ClientMessage::Trigger { name, .. } => {
180                assert_eq!(name, "note_on");
181            }
182            _ => panic!("Expected Trigger message"),
183        }
184    }
185
186    #[test]
187    fn test_client_message_ping() {
188        let json = r#"{"type": "ping", "client_time": 12345}"#;
189        let msg: ClientMessage = serde_json::from_str(json).unwrap();
190
191        match msg {
192            ClientMessage::Ping { client_time } => {
193                assert_eq!(client_time, 12345);
194            }
195            _ => panic!("Expected Ping message"),
196        }
197    }
198
199    #[test]
200    fn test_server_message_welcome() {
201        let msg = ServerMessage::Welcome {
202            node_id: "abc-123".to_string(),
203            server_time: 1000,
204        };
205        let json = serde_json::to_string(&msg).unwrap();
206
207        assert!(json.contains("\"type\":\"welcome\""));
208        assert!(json.contains("\"node_id\":\"abc-123\""));
209    }
210
211    #[test]
212    fn test_server_message_state() {
213        let msg = ServerMessage::State {
214            params: vec![
215                ParamState::new("gain", 0.5),
216                ParamState::new("freq", 440.0).with_range(20.0, 20000.0),
217            ],
218        };
219        let json = serde_json::to_string(&msg).unwrap();
220
221        assert!(json.contains("\"type\":\"state\""));
222        assert!(json.contains("\"name\":\"gain\""));
223    }
224
225    #[test]
226    fn test_server_message_error() {
227        let msg = ServerMessage::error("TEST_ERROR", "Test error message");
228        let json = serde_json::to_string(&msg).unwrap();
229
230        assert!(json.contains("\"type\":\"error\""));
231        assert!(json.contains("\"code\":\"TEST_ERROR\""));
232    }
233
234    #[test]
235    fn test_param_state_defaults() {
236        let state = ParamState::new("test", 0.5);
237        assert!((state.min - 0.0).abs() < f32::EPSILON);
238        assert!((state.max - 1.0).abs() < f32::EPSILON);
239    }
240}