feagi_hal/transports/protocol.rs
1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! FEAGI Transport Protocol (Transport-Agnostic)
5//!
6//! This module defines the binary protocol for FEAGI→Embodiment communication.
7//! It works with **any transport layer**: BLE, USB CDC, UART, WiFi, etc.
8//!
9//! The protocol layer is responsible for:
10//! - Parsing binary packets into structured commands
11//! - Buffering incomplete packets
12//! - Formatting responses (sensor data, capabilities)
13//!
14//! The protocol layer does NOT handle:
15//! - Transport connection/disconnection
16//! - Sending/receiving bytes (that's the transport's job)
17//! - Platform-specific hardware access
18//!
19//! ## Binary Protocol Format
20//!
21//! ```text
22//! ┌──────────┬──────────┬────────────────────┐
23//! │ Command │ Length │ Payload │
24//! │ (1 byte) │ (1 byte) │ (variable) │
25//! └──────────┴──────────┴────────────────────┘
26//! ```
27//!
28//! ### Command Types
29//!
30//! | ID | Name | Payload | Description |
31//! |----|------|---------|-------------|
32//! | 0x01 | NeuronFiring | count + (x,y) pairs | Fired neurons for display |
33//! | 0x02 | SetGpio | pin + value | Digital GPIO control |
34//! | 0x03 | SetPwm | pin + duty | PWM control (0-255) |
35//! | 0x04 | SetLedMatrix | 25 bytes | Full 5×5 LED matrix |
36//! | 0x05 | GetCapabilities | none | Request device info |
37//!
38//! ## Example: Neuron Firing Packet
39//!
40//! To light LEDs at (1,2) and (3,4):
41//!
42//! ```text
43//! [0x01] [0x02] [0x01, 0x02, 0x03, 0x04]
44//! │ │ └─ Coordinates: (1,2), (3,4)
45//! │ └─ Count: 2 neurons
46//! └─ Command: NeuronFiring (0x01)
47//! ```
48//!
49//! ## Transport Independence
50//!
51//! This protocol works identically over:
52//! - **BLE**: Bytes arrive via GATT Write characteristic
53//! - **USB CDC**: Bytes arrive via USB bulk OUT endpoint
54//! - **UART**: Bytes arrive via serial RX interrupt
55//! - **WiFi**: Bytes arrive via TCP socket
56//!
57//! The protocol doesn't know or care which transport is used!
58
59// Note: This module is part of a no_std crate
60
61use heapless::Vec;
62
63/// FEAGI commands (parsed from binary packets)
64#[derive(Debug, Clone, PartialEq)]
65#[cfg_attr(feature = "defmt", derive(defmt::Format))]
66pub enum Command {
67 /// Set a GPIO pin to high or low
68 SetGpio {
69 /// GPIO pin number
70 pin: u8,
71 /// Pin value (true = high, false = low)
72 value: bool,
73 },
74 /// Set PWM duty cycle (0-255) on a pin
75 SetPwm {
76 /// GPIO pin number
77 pin: u8,
78 /// PWM duty cycle (0-255)
79 duty: u8,
80 },
81 /// Set full LED matrix (5x5 = 25 bytes, brightness 0-255)
82 SetLedMatrix {
83 /// LED matrix data (5x5 = 25 bytes)
84 data: [u8; 25],
85 },
86 /// Neuron firing coordinates for LED matrix visualization
87 /// Each coordinate is (x, y) where x,y ∈ [0, 4] for a 5×5 matrix
88 NeuronFiring {
89 /// Coordinates of fired neurons
90 coordinates: Vec<(u8, u8), 25>,
91 },
92 /// Request device capabilities JSON
93 GetCapabilities,
94}
95
96/// Binary packet command IDs
97#[repr(u8)]
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PacketCommand {
100 /// Neuron firing command (0x01)
101 NeuronFiring = 0x01,
102 /// Set GPIO command (0x02)
103 SetGpio = 0x02,
104 /// Set PWM command (0x03)
105 SetPwm = 0x03,
106 /// Set LED matrix command (0x04)
107 SetLedMatrix = 0x04,
108 /// Get capabilities command (0x05)
109 GetCapabilities = 0x05,
110}
111
112impl TryFrom<u8> for PacketCommand {
113 type Error = ();
114
115 fn try_from(value: u8) -> Result<Self, Self::Error> {
116 match value {
117 0x01 => Ok(PacketCommand::NeuronFiring),
118 0x02 => Ok(PacketCommand::SetGpio),
119 0x03 => Ok(PacketCommand::SetPwm),
120 0x04 => Ok(PacketCommand::SetLedMatrix),
121 0x05 => Ok(PacketCommand::GetCapabilities),
122 _ => Err(()),
123 }
124 }
125}
126
127/// Transport-agnostic protocol handler
128///
129/// This handles protocol parsing and command buffering.
130/// It does NOT handle transport connection/disconnection.
131pub struct Protocol {
132 device_name: &'static str,
133 /// Receive buffer for incoming packets (from any transport)
134 receive_buffer: Vec<u8, 256>,
135 /// Connection status (managed by application, not protocol)
136 connected: bool,
137}
138
139impl Protocol {
140 /// Create a new protocol handler
141 pub fn new(device_name: &'static str) -> Self {
142 Self {
143 device_name,
144 receive_buffer: Vec::new(),
145 connected: false,
146 }
147 }
148
149 /// Get device name
150 pub fn device_name(&self) -> &str {
151 self.device_name
152 }
153
154 /// Check if connected (application-managed)
155 pub fn is_connected(&self) -> bool {
156 self.connected
157 }
158
159 /// Set connection status (called by application)
160 pub fn set_connected(&mut self, connected: bool) {
161 self.connected = connected;
162 if !connected {
163 // Clear buffer on disconnect
164 self.receive_buffer.clear();
165 }
166 }
167
168 /// Process incoming data from transport layer
169 ///
170 /// This appends data to the internal buffer for parsing.
171 /// Call `receive_command()` to extract parsed commands.
172 ///
173 /// **Transport Independence:** This method accepts bytes from ANY source:
174 /// - BLE notification data
175 /// - USB CDC read buffer
176 /// - UART RX buffer
177 /// - WiFi socket data
178 pub fn process_received_data(&mut self, data: &[u8]) {
179 for &byte in data {
180 if self.receive_buffer.push(byte).is_err() {
181 // Buffer full - clear and restart
182 // This handles malformed packets or overflow
183 self.receive_buffer.clear();
184 break;
185 }
186 }
187 }
188
189 /// Parse and consume the next command from the buffer
190 ///
191 /// Returns `Some(Command)` if a complete, valid command was parsed.
192 /// Returns `None` if buffer is empty or packet is incomplete/invalid.
193 pub fn receive_command(&mut self) -> Option<Command> {
194 // Try neuron firing packet
195 if let Some(coords) = self.parse_neuron_firing_packet() {
196 return Some(Command::NeuronFiring {
197 coordinates: coords,
198 });
199 }
200
201 // TODO: Add other packet types (GPIO, PWM, LED matrix, capabilities)
202
203 None
204 }
205
206 /// Parse neuron firing packet
207 ///
208 /// Format: [0x01] [count] [x1, y1, x2, y2, ...]
209 fn parse_neuron_firing_packet(&mut self) -> Option<Vec<(u8, u8), 25>> {
210 if self.receive_buffer.len() < 2 {
211 return None;
212 }
213
214 if self.receive_buffer[0] != PacketCommand::NeuronFiring as u8 {
215 return None;
216 }
217
218 let count = self.receive_buffer[1] as usize;
219 if count > 25 || self.receive_buffer.len() < 2 + count * 2 {
220 return None;
221 }
222
223 let mut coords = Vec::new();
224 for i in 0..count {
225 let x = self.receive_buffer[2 + i * 2];
226 let y = self.receive_buffer[2 + i * 2 + 1];
227 if coords.push((x, y)).is_err() {
228 break;
229 }
230 }
231
232 // Consume processed bytes
233 let consumed = 2 + count * 2;
234 self.consume_bytes(consumed);
235
236 Some(coords)
237 }
238
239 /// Remove consumed bytes from buffer
240 fn consume_bytes(&mut self, count: usize) {
241 if count >= self.receive_buffer.len() {
242 self.receive_buffer.clear();
243 return;
244 }
245
246 // Shift remaining data to front
247 for i in count..self.receive_buffer.len() {
248 self.receive_buffer[i - count] = self.receive_buffer[i];
249 }
250
251 // Truncate to new length
252 for _ in 0..count {
253 if self.receive_buffer.pop().is_none() {
254 break;
255 }
256 }
257 }
258
259 /// Format capabilities JSON into byte buffer
260 ///
261 /// Example capabilities string:
262 /// ```json
263 /// {
264 /// "sensors": {"accel": true, "mag": true},
265 /// "gpio": {"digital": 8, "pwm": 8},
266 /// "display": {"matrix": true}
267 /// }
268 /// ```
269 pub fn get_capabilities_data(&self, caps: &str) -> Vec<u8, 256> {
270 let mut buffer = Vec::new();
271 for &byte in caps.as_bytes() {
272 if buffer.push(byte).is_err() {
273 break;
274 }
275 }
276 buffer
277 }
278}
279
280// Re-export for backward compatibility
281pub use Protocol as BluetoothProtocol;
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_protocol_creation() {
289 let protocol = Protocol::new("FEAGI-test");
290 assert_eq!(protocol.device_name(), "FEAGI-test");
291 assert!(!protocol.is_connected());
292 }
293
294 #[test]
295 fn test_connection_status() {
296 let mut protocol = Protocol::new("FEAGI-test");
297
298 protocol.set_connected(true);
299 assert!(protocol.is_connected());
300 protocol.set_connected(false);
301 assert!(!protocol.is_connected());
302 }
303
304 #[test]
305 fn test_parse_neuron_firing_valid() {
306 let mut protocol = Protocol::new("FEAGI-test");
307
308 // Valid packet
309 let packet = [0x01, 0x02, 0x01, 0x02, 0x03, 0x04];
310 protocol.process_received_data(&packet);
311
312 let result = protocol.receive_command();
313 assert!(result.is_some());
314
315 if let Some(Command::NeuronFiring { coordinates }) = result {
316 assert_eq!(coordinates.len(), 2);
317 assert_eq!(coordinates[0], (1, 2));
318 assert_eq!(coordinates[1], (3, 4));
319 } else {
320 panic!("Expected NeuronFiring command");
321 }
322 }
323
324 #[test]
325 fn test_buffer_overflow_handling() {
326 let mut protocol = Protocol::new("FEAGI-test");
327
328 // Fill buffer beyond capacity
329 let mut large_data = heapless::Vec::<u8, 300>::new();
330 for i in 0..300 {
331 let _ = large_data.push(i as u8);
332 }
333 protocol.process_received_data(&large_data);
334
335 // Buffer should handle overflow
336 let packet = [0x01, 0x01, 0x05, 0x06];
337 protocol.process_received_data(&packet);
338 let result = protocol.receive_command();
339 assert!(result.is_some());
340 }
341
342 #[test]
343 fn test_disconnect_clears_buffer() {
344 let mut protocol = Protocol::new("FEAGI-test");
345
346 protocol.process_received_data(&[0x01, 0x02, 0x03]);
347 protocol.set_connected(false);
348
349 // Buffer should be cleared
350 let packet = [0x01, 0x01, 0x05, 0x06];
351 protocol.process_received_data(&packet);
352
353 let result = protocol.receive_command();
354 assert!(result.is_some());
355 }
356}