Skip to main content

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}