Skip to main content

clasp_embedded/
lib.rs

1//! CLASP Embedded
2//!
3//! Minimal `no_std` implementation of the **standard CLASP binary protocol**.
4//!
5//! This crate provides both client AND server (mini-router) capabilities
6//! for embedded devices like ESP32, Raspberry Pi Pico, etc.
7//!
8//! # Protocol Compatibility
9//!
10//! **Uses the same compact binary protocol as the full CLASP implementation.**
11//! Messages from embedded devices are fully compatible with desktop/cloud routers.
12//!
13//! # Memory Budget
14//!
15//! | Component | ESP32 (320KB) | RP2040 (264KB) | Notes |
16//! |-----------|---------------|----------------|-------|
17//! | Client | ~2KB | ~2KB | State cache, subscriptions |
18//! | Server | ~4KB | ~4KB | + session management |
19//! | Buffers | ~1KB | ~1KB | TX/RX configurable |
20//!
21//! # Features
22//!
23//! - `alloc` - Enable heap allocation for dynamic strings (recommended for ESP32)
24//! - `server` - Enable mini-router/server mode
25//! - `client` - Enable client mode (default)
26
27#![no_std]
28#![allow(dead_code)]
29
30#[cfg(feature = "alloc")]
31extern crate alloc;
32
33#[cfg(feature = "alloc")]
34use alloc::{string::String, vec::Vec};
35
36// ============================================================================
37// CLASP Protocol Constants (same as clasp-core)
38// ============================================================================
39
40/// Protocol magic byte
41pub const MAGIC: u8 = 0x53; // 'S' for Stream
42
43/// Protocol version (used in HELLO messages)
44pub const VERSION: u8 = 1;
45
46/// Message type codes (standard CLASP binary format)
47pub mod msg {
48    pub const HELLO: u8 = 0x01;
49    pub const WELCOME: u8 = 0x02;
50    pub const ANNOUNCE: u8 = 0x03;
51    pub const SUBSCRIBE: u8 = 0x10;
52    pub const UNSUBSCRIBE: u8 = 0x11;
53    pub const PUBLISH: u8 = 0x20;
54    pub const SET: u8 = 0x21;
55    pub const GET: u8 = 0x22;
56    pub const SNAPSHOT: u8 = 0x23;
57    pub const BUNDLE: u8 = 0x30;
58    pub const SYNC: u8 = 0x40;
59    pub const PING: u8 = 0x41;
60    pub const PONG: u8 = 0x42;
61    pub const ACK: u8 = 0x50;
62    pub const ERROR: u8 = 0x51;
63    pub const QUERY: u8 = 0x60;
64    pub const RESULT: u8 = 0x61;
65}
66
67/// Value type codes (standard CLASP binary format)
68pub mod val {
69    pub const NULL: u8 = 0x00;
70    pub const BOOL: u8 = 0x01;
71    pub const I32: u8 = 0x04;
72    pub const I64: u8 = 0x05;
73    pub const F32: u8 = 0x06;
74    pub const F64: u8 = 0x07;
75    pub const STRING: u8 = 0x08;
76    pub const BYTES: u8 = 0x09;
77    pub const ARRAY: u8 = 0x0A;
78    pub const MAP: u8 = 0x0B;
79}
80
81// ============================================================================
82// Frame Format (standard CLASP binary format)
83// ============================================================================
84
85/// Frame header size (without timestamp)
86pub const HEADER_SIZE: usize = 4;
87
88/// Maximum payload for embedded (configurable, smaller than full 65535)
89pub const MAX_PAYLOAD: usize = 1024;
90
91/// Decode frame header, returns (flags, payload_len) or None
92pub fn decode_header(buf: &[u8]) -> Option<(u8, usize)> {
93    if buf.len() < HEADER_SIZE || buf[0] != MAGIC {
94        return None;
95    }
96    let flags = buf[1];
97    let len = u16::from_be_bytes([buf[2], buf[3]]) as usize;
98    Some((flags, len))
99}
100
101/// Frame flags for compact binary encoding
102/// Bits: [qos:2][has_ts:1][enc:1][cmp:1][rsv:1][version:2]
103pub const FLAGS_BINARY: u8 = 0x01; // version=1 (compact binary), rest default
104
105/// Encode frame header with binary encoding flags
106pub fn encode_header(buf: &mut [u8], _flags: u8, payload_len: usize) -> usize {
107    if buf.len() < HEADER_SIZE {
108        return 0;
109    }
110    buf[0] = MAGIC;
111    buf[1] = FLAGS_BINARY; // Always use compact binary encoding
112    let len = (payload_len as u16).to_be_bytes();
113    buf[2] = len[0];
114    buf[3] = len[1];
115    HEADER_SIZE
116}
117
118// ============================================================================
119// Value Encoding/Decoding (compact binary format)
120// ============================================================================
121
122/// Simple value type for embedded (core types, no allocation needed)
123#[derive(Clone, Copy, Debug, PartialEq)]
124pub enum Value {
125    Null,
126    Bool(bool),
127    Int(i64),
128    Float(f64),
129}
130
131/// Extended value type with heap-allocated String and Bytes (requires alloc feature)
132#[cfg(feature = "alloc")]
133#[derive(Clone, Debug, PartialEq)]
134pub enum ValueExt {
135    Null,
136    Bool(bool),
137    Int(i64),
138    Float(f64),
139    String(String),
140    Bytes(Vec<u8>),
141}
142
143#[cfg(feature = "alloc")]
144impl ValueExt {
145    pub fn as_int(&self) -> Option<i64> {
146        match self {
147            ValueExt::Int(i) => Some(*i),
148            ValueExt::Float(f) => Some(*f as i64),
149            _ => None,
150        }
151    }
152
153    pub fn as_float(&self) -> Option<f64> {
154        match self {
155            ValueExt::Float(f) => Some(*f),
156            ValueExt::Int(i) => Some(*i as f64),
157            _ => None,
158        }
159    }
160
161    pub fn as_bool(&self) -> Option<bool> {
162        match self {
163            ValueExt::Bool(b) => Some(*b),
164            _ => None,
165        }
166    }
167
168    pub fn as_str(&self) -> Option<&str> {
169        match self {
170            ValueExt::String(s) => Some(s.as_str()),
171            _ => None,
172        }
173    }
174
175    pub fn as_bytes(&self) -> Option<&[u8]> {
176        match self {
177            ValueExt::Bytes(b) => Some(b.as_slice()),
178            _ => None,
179        }
180    }
181
182    /// Convert from core Value to extended Value
183    pub fn from_value(v: Value) -> Self {
184        match v {
185            Value::Null => ValueExt::Null,
186            Value::Bool(b) => ValueExt::Bool(b),
187            Value::Int(i) => ValueExt::Int(i),
188            Value::Float(f) => ValueExt::Float(f),
189        }
190    }
191
192    /// Try to convert to core Value (fails for String/Bytes)
193    pub fn to_value(&self) -> Option<Value> {
194        match self {
195            ValueExt::Null => Some(Value::Null),
196            ValueExt::Bool(b) => Some(Value::Bool(*b)),
197            ValueExt::Int(i) => Some(Value::Int(*i)),
198            ValueExt::Float(f) => Some(Value::Float(*f)),
199            ValueExt::String(_) | ValueExt::Bytes(_) => None,
200        }
201    }
202}
203
204impl Value {
205    pub fn as_int(&self) -> Option<i64> {
206        match self {
207            Value::Int(i) => Some(*i),
208            Value::Float(f) => Some(*f as i64),
209            _ => None,
210        }
211    }
212
213    pub fn as_float(&self) -> Option<f64> {
214        match self {
215            Value::Float(f) => Some(*f),
216            Value::Int(i) => Some(*i as f64),
217            _ => None,
218        }
219    }
220
221    pub fn as_bool(&self) -> Option<bool> {
222        match self {
223            Value::Bool(b) => Some(*b),
224            _ => None,
225        }
226    }
227}
228
229/// Encode a value, returns bytes written
230pub fn encode_value(buf: &mut [u8], value: &Value) -> usize {
231    match value {
232        Value::Null => {
233            if buf.is_empty() {
234                return 0;
235            }
236            buf[0] = val::NULL;
237            1
238        }
239        Value::Bool(b) => {
240            if buf.len() < 2 {
241                return 0;
242            }
243            buf[0] = val::BOOL;
244            buf[1] = if *b { 1 } else { 0 };
245            2
246        }
247        Value::Int(i) => {
248            if buf.len() < 9 {
249                return 0;
250            }
251            buf[0] = val::I64;
252            buf[1..9].copy_from_slice(&i.to_be_bytes());
253            9
254        }
255        Value::Float(f) => {
256            if buf.len() < 9 {
257                return 0;
258            }
259            buf[0] = val::F64;
260            buf[1..9].copy_from_slice(&f.to_be_bytes());
261            9
262        }
263    }
264}
265
266/// Decode a value, returns (value, bytes_consumed)
267/// Note: For String/Bytes types, use decode_value_ext with alloc feature
268pub fn decode_value(buf: &[u8]) -> Option<(Value, usize)> {
269    if buf.is_empty() {
270        return None;
271    }
272    match buf[0] {
273        val::NULL => Some((Value::Null, 1)),
274        val::BOOL => {
275            if buf.len() < 2 {
276                return None;
277            }
278            Some((Value::Bool(buf[1] != 0), 2))
279        }
280        val::I32 => {
281            if buf.len() < 5 {
282                return None;
283            }
284            let i = i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
285            Some((Value::Int(i as i64), 5))
286        }
287        val::I64 => {
288            if buf.len() < 9 {
289                return None;
290            }
291            let i = i64::from_be_bytes([
292                buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8],
293            ]);
294            Some((Value::Int(i), 9))
295        }
296        val::F32 => {
297            if buf.len() < 5 {
298                return None;
299            }
300            let f = f32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
301            Some((Value::Float(f as f64), 5))
302        }
303        val::F64 => {
304            if buf.len() < 9 {
305                return None;
306            }
307            let f = f64::from_be_bytes([
308                buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8],
309            ]);
310            Some((Value::Float(f), 9))
311        }
312        // Skip over string/bytes (return None for non-alloc since we can't store them)
313        val::STRING | val::BYTES => {
314            // Calculate size but return None since we can't represent these without alloc
315            if buf.len() < 3 {
316                return None;
317            }
318            let len = u16::from_be_bytes([buf[1], buf[2]]) as usize;
319            if buf.len() < 3 + len {
320                return None;
321            }
322            None // Can't represent String/Bytes in Value without alloc
323        }
324        // Skip over array/map (return None since we don't support them)
325        val::ARRAY | val::MAP => None,
326        _ => None,
327    }
328}
329
330/// Decode extended value (with alloc support for String/Bytes)
331#[cfg(feature = "alloc")]
332pub fn decode_value_ext(buf: &[u8]) -> Option<(ValueExt, usize)> {
333    if buf.is_empty() {
334        return None;
335    }
336    match buf[0] {
337        val::NULL => Some((ValueExt::Null, 1)),
338        val::BOOL => {
339            if buf.len() < 2 {
340                return None;
341            }
342            Some((ValueExt::Bool(buf[1] != 0), 2))
343        }
344        val::I32 => {
345            if buf.len() < 5 {
346                return None;
347            }
348            let i = i32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
349            Some((ValueExt::Int(i as i64), 5))
350        }
351        val::I64 => {
352            if buf.len() < 9 {
353                return None;
354            }
355            let i = i64::from_be_bytes([
356                buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8],
357            ]);
358            Some((ValueExt::Int(i), 9))
359        }
360        val::F32 => {
361            if buf.len() < 5 {
362                return None;
363            }
364            let f = f32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
365            Some((ValueExt::Float(f as f64), 5))
366        }
367        val::F64 => {
368            if buf.len() < 9 {
369                return None;
370            }
371            let f = f64::from_be_bytes([
372                buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7], buf[8],
373            ]);
374            Some((ValueExt::Float(f), 9))
375        }
376        val::STRING => {
377            if buf.len() < 3 {
378                return None;
379            }
380            let len = u16::from_be_bytes([buf[1], buf[2]]) as usize;
381            if buf.len() < 3 + len {
382                return None;
383            }
384            let s = core::str::from_utf8(&buf[3..3 + len]).ok()?;
385            Some((ValueExt::String(String::from(s)), 3 + len))
386        }
387        val::BYTES => {
388            if buf.len() < 3 {
389                return None;
390            }
391            let len = u16::from_be_bytes([buf[1], buf[2]]) as usize;
392            if buf.len() < 3 + len {
393                return None;
394            }
395            let bytes = buf[3..3 + len].to_vec();
396            Some((ValueExt::Bytes(bytes), 3 + len))
397        }
398        _ => None,
399    }
400}
401
402/// Encode extended value (with alloc support for String/Bytes)
403#[cfg(feature = "alloc")]
404pub fn encode_value_ext(buf: &mut [u8], value: &ValueExt) -> usize {
405    match value {
406        ValueExt::Null => {
407            if buf.is_empty() {
408                return 0;
409            }
410            buf[0] = val::NULL;
411            1
412        }
413        ValueExt::Bool(b) => {
414            if buf.len() < 2 {
415                return 0;
416            }
417            buf[0] = val::BOOL;
418            buf[1] = if *b { 1 } else { 0 };
419            2
420        }
421        ValueExt::Int(i) => {
422            if buf.len() < 9 {
423                return 0;
424            }
425            buf[0] = val::I64;
426            buf[1..9].copy_from_slice(&i.to_be_bytes());
427            9
428        }
429        ValueExt::Float(f) => {
430            if buf.len() < 9 {
431                return 0;
432            }
433            buf[0] = val::F64;
434            buf[1..9].copy_from_slice(&f.to_be_bytes());
435            9
436        }
437        ValueExt::String(s) => {
438            let bytes = s.as_bytes();
439            if buf.len() < 3 + bytes.len() {
440                return 0;
441            }
442            buf[0] = val::STRING;
443            let len = (bytes.len() as u16).to_be_bytes();
444            buf[1] = len[0];
445            buf[2] = len[1];
446            buf[3..3 + bytes.len()].copy_from_slice(bytes);
447            3 + bytes.len()
448        }
449        ValueExt::Bytes(b) => {
450            if buf.len() < 3 + b.len() {
451                return 0;
452            }
453            buf[0] = val::BYTES;
454            let len = (b.len() as u16).to_be_bytes();
455            buf[1] = len[0];
456            buf[2] = len[1];
457            buf[3..3 + b.len()].copy_from_slice(b);
458            3 + b.len()
459        }
460    }
461}
462
463// ============================================================================
464// String Encoding (length-prefixed, standard CLASP format)
465// ============================================================================
466
467/// Encode a string (u16 length prefix)
468pub fn encode_string(buf: &mut [u8], s: &str) -> usize {
469    let bytes = s.as_bytes();
470    if buf.len() < 2 + bytes.len() {
471        return 0;
472    }
473    let len = (bytes.len() as u16).to_be_bytes();
474    buf[0] = len[0];
475    buf[1] = len[1];
476    buf[2..2 + bytes.len()].copy_from_slice(bytes);
477    2 + bytes.len()
478}
479
480/// Decode a string, returns (str slice, bytes consumed)
481pub fn decode_string(buf: &[u8]) -> Option<(&str, usize)> {
482    if buf.len() < 2 {
483        return None;
484    }
485    let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
486    if buf.len() < 2 + len {
487        return None;
488    }
489    let s = core::str::from_utf8(&buf[2..2 + len]).ok()?;
490    Some((s, 2 + len))
491}
492
493// ============================================================================
494// Message Encoding (compact binary format)
495// ============================================================================
496
497/// Get value type code for flags byte
498fn value_type_code(value: &Value) -> u8 {
499    match value {
500        Value::Null => val::NULL,
501        Value::Bool(_) => val::BOOL,
502        Value::Int(_) => val::I64,
503        Value::Float(_) => val::F64,
504    }
505}
506
507/// Encode value data only (without type byte, for SET messages)
508fn encode_value_data(buf: &mut [u8], value: &Value) -> usize {
509    match value {
510        Value::Null => 0,
511        Value::Bool(b) => {
512            if buf.is_empty() {
513                return 0;
514            }
515            buf[0] = if *b { 1 } else { 0 };
516            1
517        }
518        Value::Int(i) => {
519            if buf.len() < 8 {
520                return 0;
521            }
522            buf[..8].copy_from_slice(&i.to_be_bytes());
523            8
524        }
525        Value::Float(f) => {
526            if buf.len() < 8 {
527                return 0;
528            }
529            buf[..8].copy_from_slice(&f.to_be_bytes());
530            8
531        }
532    }
533}
534
535/// Encode a SET message payload (without frame header)
536/// Format: msg_type(1) + flags(1) + addr_len(2) + addr + value_data
537/// Flags: [has_rev:1][lock:1][unlock:1][rsv:1][vtype:4]
538pub fn encode_set(buf: &mut [u8], address: &str, value: &Value) -> usize {
539    if buf.len() < 2 {
540        return 0;
541    }
542
543    // Message type
544    buf[0] = msg::SET;
545
546    // Flags: value type in lower 4 bits, no revision/lock/unlock
547    let vtype = value_type_code(value);
548    buf[1] = vtype & 0x0F;
549
550    let mut offset = 2;
551
552    // Address (length-prefixed)
553    offset += encode_string(&mut buf[offset..], address);
554
555    // Value data only (type is in flags)
556    offset += encode_value_data(&mut buf[offset..], value);
557
558    offset
559}
560
561/// Encode a complete SET frame (header + payload)
562pub fn encode_set_frame(buf: &mut [u8], address: &str, value: &Value) -> usize {
563    let header_size = HEADER_SIZE;
564    let payload_start = header_size;
565
566    let payload_len = encode_set(&mut buf[payload_start..], address, value);
567    if payload_len == 0 {
568        return 0;
569    }
570
571    encode_header(buf, 0, payload_len);
572    header_size + payload_len
573}
574
575/// Encode a SUBSCRIBE message
576/// Format: msg_type(1) + id(4) + pattern + type_mask(1) + opt_flags(1)
577pub fn encode_subscribe(buf: &mut [u8], pattern: &str) -> usize {
578    if buf.is_empty() {
579        return 0;
580    }
581    buf[0] = msg::SUBSCRIBE;
582    let mut offset = 1;
583
584    // subscription id (u32)
585    if buf.len() < offset + 4 {
586        return 0;
587    }
588    buf[offset..offset + 4].copy_from_slice(&0u32.to_be_bytes());
589    offset += 4;
590
591    // pattern
592    offset += encode_string(&mut buf[offset..], pattern);
593
594    // Type mask (0xFF = all types)
595    if buf.len() > offset {
596        buf[offset] = 0xFF;
597        offset += 1;
598    }
599
600    // Option flags (0 = no options)
601    if buf.len() > offset {
602        buf[offset] = 0;
603        offset += 1;
604    }
605
606    offset
607}
608
609/// Encode a SUBSCRIBE frame
610pub fn encode_subscribe_frame(buf: &mut [u8], pattern: &str) -> usize {
611    let header_size = HEADER_SIZE;
612    let payload_len = encode_subscribe(&mut buf[header_size..], pattern);
613    if payload_len == 0 {
614        return 0;
615    }
616    encode_header(buf, 0, payload_len);
617    header_size + payload_len
618}
619
620/// Encode a HELLO message (binary format)
621/// Format: msg_type(1) + version(1) + features(1) + name + token
622pub fn encode_hello(buf: &mut [u8], name: &str) -> usize {
623    if buf.len() < 6 {
624        return 0;
625    }
626
627    // Message type
628    buf[0] = msg::HELLO;
629
630    // Protocol version
631    buf[1] = VERSION;
632
633    // Feature flags (all features supported)
634    buf[2] = 0xF8; // param|event|stream|gesture|timeline
635
636    let mut offset = 3;
637
638    // Name
639    offset += encode_string(&mut buf[offset..], name);
640
641    // Token (none)
642    if buf.len() >= offset + 2 {
643        buf[offset] = 0;
644        buf[offset + 1] = 0;
645        offset += 2;
646    }
647
648    offset
649}
650
651/// Encode a HELLO frame
652pub fn encode_hello_frame(buf: &mut [u8], name: &str) -> usize {
653    let header_size = HEADER_SIZE;
654    let payload_len = encode_hello(&mut buf[header_size..], name);
655    if payload_len == 0 {
656        return 0;
657    }
658    encode_header(buf, 0, payload_len);
659    header_size + payload_len
660}
661
662/// Encode a PING frame
663pub fn encode_ping_frame(buf: &mut [u8]) -> usize {
664    if buf.len() < HEADER_SIZE + 1 {
665        return 0;
666    }
667    encode_header(buf, 0, 1);
668    buf[HEADER_SIZE] = msg::PING;
669    HEADER_SIZE + 1
670}
671
672/// Encode a PONG frame
673pub fn encode_pong_frame(buf: &mut [u8]) -> usize {
674    if buf.len() < HEADER_SIZE + 1 {
675        return 0;
676    }
677    encode_header(buf, 0, 1);
678    buf[HEADER_SIZE] = msg::PONG;
679    HEADER_SIZE + 1
680}
681
682// ============================================================================
683// Message Decoding
684// ============================================================================
685
686/// Decoded message (zero-copy where possible)
687#[derive(Debug)]
688pub enum Message<'a> {
689    Hello { name: &'a str, version: u8 },
690    Welcome { session: &'a str },
691    Announce { signal_count: u16 },
692    Set { address: &'a str, value: Value },
693    Subscribe { id: u32, pattern: &'a str },
694    Unsubscribe { id: u32 },
695    Publish { address: &'a str },
696    Bundle { message_count: u16 },
697    Sync { timestamp: u64 },
698    Ping,
699    Pong,
700    Query { pattern: &'a str },
701    Result { signal_count: u16 },
702    Error { code: u16, message: &'a str },
703    Unknown(u8),
704}
705
706/// Decode a message from a frame payload
707pub fn decode_message(payload: &[u8]) -> Option<Message<'_>> {
708    if payload.is_empty() {
709        return None;
710    }
711
712    let msg_type = payload[0];
713    let data = &payload[1..];
714
715    match msg_type {
716        msg::HELLO => {
717            // HELLO format: version(1) + features(1) + name + token
718            if data.len() < 2 {
719                return None;
720            }
721            let version = data[0];
722            let _features = data[1];
723            let (name, _) = decode_string(&data[2..])?;
724            Some(Message::Hello { name, version })
725        }
726        msg::WELCOME => {
727            // WELCOME format: version(1) + features(1) + time(8) + session + name
728            if data.len() < 10 {
729                return None;
730            }
731            let _version = data[0];
732            let _features = data[1];
733            let _time = u64::from_be_bytes([
734                data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9],
735            ]);
736            let (session, _) = decode_string(&data[10..])?;
737            Some(Message::Welcome { session })
738        }
739        msg::SET => {
740            // SET format: flags(1) + address + value_data
741            // Flags: [has_rev:1][lock:1][unlock:1][rsv:1][vtype:4]
742            if data.is_empty() {
743                return None;
744            }
745            let flags = data[0];
746            let vtype = flags & 0x0F;
747            let _has_rev = (flags & 0x80) != 0;
748
749            let (address, offset) = decode_string(&data[1..])?;
750            let value_data = &data[1 + offset..];
751
752            let value = match vtype {
753                val::NULL => Value::Null,
754                val::BOOL => {
755                    if value_data.is_empty() {
756                        return None;
757                    }
758                    Value::Bool(value_data[0] != 0)
759                }
760                val::I64 => {
761                    if value_data.len() < 8 {
762                        return None;
763                    }
764                    let i = i64::from_be_bytes([
765                        value_data[0],
766                        value_data[1],
767                        value_data[2],
768                        value_data[3],
769                        value_data[4],
770                        value_data[5],
771                        value_data[6],
772                        value_data[7],
773                    ]);
774                    Value::Int(i)
775                }
776                val::F64 => {
777                    if value_data.len() < 8 {
778                        return None;
779                    }
780                    let f = f64::from_be_bytes([
781                        value_data[0],
782                        value_data[1],
783                        value_data[2],
784                        value_data[3],
785                        value_data[4],
786                        value_data[5],
787                        value_data[6],
788                        value_data[7],
789                    ]);
790                    Value::Float(f)
791                }
792                _ => return None, // Unsupported type
793            };
794
795            Some(Message::Set { address, value })
796        }
797        msg::SUBSCRIBE => {
798            // SUBSCRIBE format: id(4) + pattern
799            if data.len() < 4 {
800                return None;
801            }
802            let id = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
803            let (pattern, _) = decode_string(&data[4..])?;
804            Some(Message::Subscribe { id, pattern })
805        }
806        msg::UNSUBSCRIBE => {
807            // UNSUBSCRIBE format: id(4)
808            if data.len() < 4 {
809                return None;
810            }
811            let id = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
812            Some(Message::Unsubscribe { id })
813        }
814        msg::PING => Some(Message::Ping),
815        msg::PONG => Some(Message::Pong),
816        msg::ERROR => {
817            if data.len() < 2 {
818                return None;
819            }
820            let code = u16::from_be_bytes([data[0], data[1]]);
821            let (message, _) = decode_string(&data[2..]).unwrap_or(("", 0));
822            Some(Message::Error { code, message })
823        }
824        msg::ANNOUNCE => {
825            // ANNOUNCE format: signal_count(2) + signals...
826            // We just extract the count for minimal embedded support
827            if data.len() < 2 {
828                return None;
829            }
830            let signal_count = u16::from_be_bytes([data[0], data[1]]);
831            Some(Message::Announce { signal_count })
832        }
833        msg::PUBLISH => {
834            // PUBLISH format: flags(1) + address + payload
835            // Extract just the address for minimal embedded support
836            if data.is_empty() {
837                return None;
838            }
839            let _flags = data[0];
840            let (address, _) = decode_string(&data[1..])?;
841            Some(Message::Publish { address })
842        }
843        msg::BUNDLE => {
844            // BUNDLE format: flags(1) + message_count(2) + messages...
845            if data.len() < 3 {
846                return None;
847            }
848            let _flags = data[0];
849            let message_count = u16::from_be_bytes([data[1], data[2]]);
850            Some(Message::Bundle { message_count })
851        }
852        msg::SYNC => {
853            // SYNC format: timestamp(8) + optional snapshot
854            if data.len() < 8 {
855                return None;
856            }
857            let timestamp = u64::from_be_bytes([
858                data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
859            ]);
860            Some(Message::Sync { timestamp })
861        }
862        msg::QUERY => {
863            // QUERY format: pattern
864            let (pattern, _) = decode_string(data)?;
865            Some(Message::Query { pattern })
866        }
867        msg::RESULT => {
868            // RESULT format: signal_count(2) + signals...
869            if data.len() < 2 {
870                return None;
871            }
872            let signal_count = u16::from_be_bytes([data[0], data[1]]);
873            Some(Message::Result { signal_count })
874        }
875        _ => Some(Message::Unknown(msg_type)),
876    }
877}
878
879// ============================================================================
880// State Cache (Fixed Size, No Heap)
881// ============================================================================
882
883/// Maximum cached parameters
884pub const MAX_CACHE_ENTRIES: usize = 32;
885
886/// Maximum address length
887pub const MAX_ADDRESS_LEN: usize = 64;
888
889/// A cached parameter entry
890#[derive(Clone)]
891pub struct CacheEntry {
892    address: [u8; MAX_ADDRESS_LEN],
893    address_len: u8,
894    value: Value,
895    valid: bool,
896}
897
898impl Default for CacheEntry {
899    fn default() -> Self {
900        Self {
901            address: [0; MAX_ADDRESS_LEN],
902            address_len: 0,
903            value: Value::Null,
904            valid: false,
905        }
906    }
907}
908
909impl CacheEntry {
910    fn address(&self) -> &str {
911        core::str::from_utf8(&self.address[..self.address_len as usize]).unwrap_or("")
912    }
913
914    fn set_address(&mut self, addr: &str) {
915        let bytes = addr.as_bytes();
916        let len = bytes.len().min(MAX_ADDRESS_LEN);
917        self.address[..len].copy_from_slice(&bytes[..len]);
918        self.address_len = len as u8;
919    }
920}
921
922/// Fixed-size parameter cache
923pub struct StateCache {
924    entries: [CacheEntry; MAX_CACHE_ENTRIES],
925    count: usize,
926}
927
928impl StateCache {
929    pub const fn new() -> Self {
930        Self {
931            entries: [const {
932                CacheEntry {
933                    address: [0; MAX_ADDRESS_LEN],
934                    address_len: 0,
935                    value: Value::Null,
936                    valid: false,
937                }
938            }; MAX_CACHE_ENTRIES],
939            count: 0,
940        }
941    }
942
943    /// Get a cached value
944    pub fn get(&self, address: &str) -> Option<Value> {
945        for entry in &self.entries[..self.count] {
946            if entry.valid && entry.address() == address {
947                return Some(entry.value);
948            }
949        }
950        None
951    }
952
953    /// Set a cached value
954    pub fn set(&mut self, address: &str, value: Value) -> bool {
955        // Update existing
956        for entry in &mut self.entries[..self.count] {
957            if entry.valid && entry.address() == address {
958                entry.value = value;
959                return true;
960            }
961        }
962
963        // Add new
964        if self.count < MAX_CACHE_ENTRIES {
965            self.entries[self.count].set_address(address);
966            self.entries[self.count].value = value;
967            self.entries[self.count].valid = true;
968            self.count += 1;
969            return true;
970        }
971
972        false
973    }
974
975    pub fn len(&self) -> usize {
976        self.count
977    }
978
979    pub fn is_empty(&self) -> bool {
980        self.count == 0
981    }
982
983    pub fn clear(&mut self) {
984        for entry in &mut self.entries {
985            entry.valid = false;
986        }
987        self.count = 0;
988    }
989}
990
991impl Default for StateCache {
992    fn default() -> Self {
993        Self::new()
994    }
995}
996
997// ============================================================================
998// Client (Compact Binary Protocol)
999// ============================================================================
1000
1001/// Client state
1002#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1003pub enum ClientState {
1004    Disconnected,
1005    Connecting,
1006    Connected,
1007}
1008
1009/// Buffer size for messages
1010pub const TX_BUF_SIZE: usize = 256;
1011pub const RX_BUF_SIZE: usize = 512;
1012
1013/// Embedded CLASP client (compact binary protocol)
1014///
1015/// # Memory Usage
1016/// ~3KB total (cache + buffers + state)
1017pub struct Client {
1018    pub state: ClientState,
1019    pub cache: StateCache,
1020    tx_buf: [u8; TX_BUF_SIZE],
1021    rx_buf: [u8; RX_BUF_SIZE],
1022}
1023
1024impl Client {
1025    pub const fn new() -> Self {
1026        Self {
1027            state: ClientState::Disconnected,
1028            cache: StateCache::new(),
1029            tx_buf: [0; TX_BUF_SIZE],
1030            rx_buf: [0; RX_BUF_SIZE],
1031        }
1032    }
1033
1034    /// Prepare HELLO frame
1035    pub fn prepare_hello(&mut self, name: &str) -> &[u8] {
1036        let n = encode_hello_frame(&mut self.tx_buf, name);
1037        &self.tx_buf[..n]
1038    }
1039
1040    /// Prepare SET frame
1041    pub fn prepare_set(&mut self, address: &str, value: Value) -> &[u8] {
1042        let n = encode_set_frame(&mut self.tx_buf, address, &value);
1043        &self.tx_buf[..n]
1044    }
1045
1046    /// Prepare SUBSCRIBE frame
1047    pub fn prepare_subscribe(&mut self, pattern: &str) -> &[u8] {
1048        let n = encode_subscribe_frame(&mut self.tx_buf, pattern);
1049        &self.tx_buf[..n]
1050    }
1051
1052    /// Prepare PING frame
1053    pub fn prepare_ping(&mut self) -> &[u8] {
1054        let n = encode_ping_frame(&mut self.tx_buf);
1055        &self.tx_buf[..n]
1056    }
1057
1058    /// Process received frame data
1059    pub fn process<'a>(&mut self, data: &'a [u8]) -> Option<Message<'a>> {
1060        let (_, payload_len) = decode_header(data)?;
1061        let payload = &data[HEADER_SIZE..HEADER_SIZE + payload_len];
1062        let msg = decode_message(payload)?;
1063
1064        match &msg {
1065            Message::Welcome { .. } => {
1066                self.state = ClientState::Connected;
1067            }
1068            Message::Set { address, value } => {
1069                self.cache.set(address, *value);
1070            }
1071            _ => {}
1072        }
1073
1074        Some(msg)
1075    }
1076
1077    pub fn is_connected(&self) -> bool {
1078        self.state == ClientState::Connected
1079    }
1080
1081    pub fn get_cached(&self, address: &str) -> Option<Value> {
1082        self.cache.get(address)
1083    }
1084}
1085
1086impl Default for Client {
1087    fn default() -> Self {
1088        Self::new()
1089    }
1090}
1091
1092// ============================================================================
1093// Mini-Router/Server (Compact Binary Protocol)
1094// ============================================================================
1095
1096#[cfg(feature = "server")]
1097pub mod server {
1098    use super::*;
1099
1100    /// Maximum clients for embedded router
1101    pub const MAX_CLIENTS: usize = 4;
1102
1103    /// Maximum subscriptions per client
1104    pub const MAX_SUBS_PER_CLIENT: usize = 8;
1105
1106    /// Maximum pattern length for subscriptions
1107    pub const MAX_PATTERN_LEN: usize = 64;
1108
1109    /// Subscription entry
1110    #[derive(Clone)]
1111    pub struct Subscription {
1112        pub active: bool,
1113        pub id: u32,
1114        pub pattern: [u8; MAX_PATTERN_LEN],
1115        pub pattern_len: usize,
1116    }
1117
1118    impl Subscription {
1119        pub const fn empty() -> Self {
1120            Self {
1121                active: false,
1122                id: 0,
1123                pattern: [0; MAX_PATTERN_LEN],
1124                pattern_len: 0,
1125            }
1126        }
1127
1128        /// Check if address matches this subscription pattern
1129        pub fn matches(&self, address: &str) -> bool {
1130            if !self.active || self.pattern_len == 0 {
1131                return false;
1132            }
1133
1134            // Get pattern as &str
1135            let pattern = match core::str::from_utf8(&self.pattern[..self.pattern_len]) {
1136                Ok(s) => s,
1137                Err(_) => return false,
1138            };
1139
1140            // Simple wildcard matching
1141            // * matches one segment, ** matches any number of segments
1142            Self::match_pattern(pattern, address)
1143        }
1144
1145        fn match_pattern(pattern: &str, address: &str) -> bool {
1146            // Simple iterative matching without heap allocation
1147            // Split into segments manually
1148            let mut pattern_iter = pattern.split('/').filter(|s| !s.is_empty());
1149            let mut address_iter = address.split('/').filter(|s| !s.is_empty());
1150
1151            loop {
1152                match (pattern_iter.next(), address_iter.next()) {
1153                    (None, None) => return true,
1154                    (Some("**"), _) => {
1155                        // ** matches zero or more segments
1156                        // Check if there's more pattern after **
1157                        if let Some(next_pattern) = pattern_iter.next() {
1158                            // Must find next_pattern in remaining address
1159                            loop {
1160                                match address_iter.next() {
1161                                    None => return next_pattern == "**",
1162                                    Some(seg) if seg == next_pattern || next_pattern == "*" => {
1163                                        // Continue matching rest
1164                                        break;
1165                                    }
1166                                    Some(_) => continue,
1167                                }
1168                            }
1169                        } else {
1170                            // ** at end matches everything
1171                            return true;
1172                        }
1173                    }
1174                    (Some("*"), Some(_)) => continue,
1175                    (Some(p), Some(a)) if p == a => continue,
1176                    (None, Some(_)) => return false,
1177                    (Some(_), None) => {
1178                        // Check if remaining pattern is just **
1179                        return pattern_iter.all(|p| p == "**");
1180                    }
1181                    _ => return false,
1182                }
1183            }
1184        }
1185    }
1186
1187    /// Client session with subscriptions
1188    pub struct Session {
1189        pub active: bool,
1190        pub id: u8,
1191        pub subscriptions: [Subscription; MAX_SUBS_PER_CLIENT],
1192        pub sub_count: u8,
1193    }
1194
1195    impl Session {
1196        pub const fn new() -> Self {
1197            Self {
1198                active: false,
1199                id: 0,
1200                subscriptions: [const { Subscription::empty() }; MAX_SUBS_PER_CLIENT],
1201                sub_count: 0,
1202            }
1203        }
1204
1205        /// Add a subscription
1206        pub fn subscribe(&mut self, id: u32, pattern: &str) -> bool {
1207            if self.sub_count as usize >= MAX_SUBS_PER_CLIENT {
1208                return false;
1209            }
1210            if pattern.len() > MAX_PATTERN_LEN {
1211                return false;
1212            }
1213
1214            // Find empty slot
1215            for sub in &mut self.subscriptions {
1216                if !sub.active {
1217                    sub.active = true;
1218                    sub.id = id;
1219                    sub.pattern[..pattern.len()].copy_from_slice(pattern.as_bytes());
1220                    sub.pattern_len = pattern.len();
1221                    self.sub_count += 1;
1222                    return true;
1223                }
1224            }
1225            false
1226        }
1227
1228        /// Remove a subscription by ID
1229        pub fn unsubscribe(&mut self, id: u32) -> bool {
1230            for sub in &mut self.subscriptions {
1231                if sub.active && sub.id == id {
1232                    sub.active = false;
1233                    sub.pattern_len = 0;
1234                    self.sub_count = self.sub_count.saturating_sub(1);
1235                    return true;
1236                }
1237            }
1238            false
1239        }
1240
1241        /// Check if any subscription matches the address
1242        pub fn has_match(&self, address: &str) -> bool {
1243            self.subscriptions.iter().any(|s| s.matches(address))
1244        }
1245    }
1246
1247    /// Broadcast result - which clients should receive a message
1248    pub struct BroadcastList {
1249        pub clients: [bool; MAX_CLIENTS],
1250        pub count: u8,
1251    }
1252
1253    impl BroadcastList {
1254        pub const fn empty() -> Self {
1255            Self {
1256                clients: [false; MAX_CLIENTS],
1257                count: 0,
1258            }
1259        }
1260    }
1261
1262    /// Minimal embedded router with subscription support
1263    ///
1264    /// Can act as a local hub for sensors/actuators, forwarding to a main router.
1265    pub struct MiniRouter {
1266        pub state: StateCache,
1267        sessions: [Session; MAX_CLIENTS],
1268        session_count: u8,
1269        tx_buf: [u8; TX_BUF_SIZE],
1270    }
1271
1272    impl MiniRouter {
1273        pub const fn new() -> Self {
1274            Self {
1275                state: StateCache::new(),
1276                sessions: [const { Session::new() }; MAX_CLIENTS],
1277                session_count: 0,
1278                tx_buf: [0; TX_BUF_SIZE],
1279            }
1280        }
1281
1282        /// Process incoming message from a client
1283        ///
1284        /// Returns a response frame to send back to the client (if any)
1285        pub fn process(&mut self, client_id: u8, data: &[u8]) -> Option<&[u8]> {
1286            let (_, payload_len) = decode_header(data)?;
1287            let payload = &data[HEADER_SIZE..HEADER_SIZE + payload_len];
1288            let msg = decode_message(payload)?;
1289
1290            match msg {
1291                Message::Hello { name, .. } => {
1292                    self.create_session(client_id);
1293                    Some(self.prepare_welcome(client_id))
1294                }
1295                Message::Subscribe { id, pattern } => {
1296                    self.handle_subscribe(client_id, id, pattern);
1297                    None // ACK could be sent
1298                }
1299                Message::Unsubscribe { id } => {
1300                    self.handle_unsubscribe(client_id, id);
1301                    None
1302                }
1303                Message::Set { address, value } => {
1304                    self.state.set(address, value);
1305                    None // Broadcast handled separately via get_broadcast_targets
1306                }
1307                Message::Ping => Some(self.prepare_pong()),
1308                _ => None,
1309            }
1310        }
1311
1312        /// Get list of clients that should receive a broadcast for an address
1313        ///
1314        /// Call this after processing a SET to get which clients need the update
1315        pub fn get_broadcast_targets(&self, address: &str, sender_id: u8) -> BroadcastList {
1316            let mut result = BroadcastList::empty();
1317
1318            for (i, session) in self.sessions.iter().enumerate() {
1319                // Don't send back to sender, only to other active sessions with matching subs
1320                if session.active && i as u8 != sender_id && session.has_match(address) {
1321                    result.clients[i] = true;
1322                    result.count += 1;
1323                }
1324            }
1325
1326            result
1327        }
1328
1329        /// Prepare a SET frame for broadcasting to subscribers
1330        ///
1331        /// Returns the frame bytes to send to each matching client
1332        pub fn prepare_broadcast(&mut self, address: &str, value: Value) -> &[u8] {
1333            let n = encode_set_frame(&mut self.tx_buf, address, &value);
1334            &self.tx_buf[..n]
1335        }
1336
1337        fn handle_subscribe(&mut self, client_id: u8, id: u32, pattern: &str) {
1338            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1339                if session.active {
1340                    session.subscribe(id, pattern);
1341                }
1342            }
1343        }
1344
1345        fn handle_unsubscribe(&mut self, client_id: u8, id: u32) {
1346            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1347                if session.active {
1348                    session.unsubscribe(id);
1349                }
1350            }
1351        }
1352
1353        fn create_session(&mut self, client_id: u8) {
1354            if (client_id as usize) < MAX_CLIENTS {
1355                self.sessions[client_id as usize] = Session {
1356                    active: true,
1357                    id: client_id,
1358                    subscriptions: [const { Subscription::empty() }; MAX_SUBS_PER_CLIENT],
1359                    sub_count: 0,
1360                };
1361                self.session_count += 1;
1362            }
1363        }
1364
1365        /// Remove a client session
1366        pub fn disconnect(&mut self, client_id: u8) {
1367            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1368                if session.active {
1369                    session.active = false;
1370                    session.sub_count = 0;
1371                    self.session_count = self.session_count.saturating_sub(1);
1372                }
1373            }
1374        }
1375
1376        fn prepare_welcome(&mut self, _client_id: u8) -> &[u8] {
1377            let payload_start = HEADER_SIZE;
1378            let mut offset = payload_start;
1379
1380            self.tx_buf[offset] = msg::WELCOME;
1381            offset += 1;
1382
1383            self.tx_buf[offset] = VERSION;
1384            offset += 1;
1385
1386            self.tx_buf[offset] = 0xF8; // param|event|stream|gesture|timeline
1387            offset += 1;
1388
1389            self.tx_buf[offset..offset + 8].copy_from_slice(&0u64.to_be_bytes());
1390            offset += 8;
1391
1392            offset += encode_string(&mut self.tx_buf[offset..], "embedded");
1393            offset += encode_string(&mut self.tx_buf[offset..], "MiniRouter");
1394
1395            let payload_len = offset - payload_start;
1396            encode_header(&mut self.tx_buf, 0, payload_len);
1397
1398            &self.tx_buf[..offset]
1399        }
1400
1401        fn prepare_pong(&mut self) -> &[u8] {
1402            let n = encode_pong_frame(&mut self.tx_buf);
1403            &self.tx_buf[..n]
1404        }
1405
1406        pub fn get(&self, address: &str) -> Option<Value> {
1407            self.state.get(address)
1408        }
1409
1410        pub fn set(&mut self, address: &str, value: Value) {
1411            self.state.set(address, value);
1412        }
1413
1414        /// Get number of active sessions
1415        pub fn session_count(&self) -> u8 {
1416            self.session_count
1417        }
1418
1419        /// Get mutable access to a session (for testing/setup)
1420        pub fn session_mut(&mut self, client_id: u8) -> Option<&mut Session> {
1421            self.sessions.get_mut(client_id as usize)
1422        }
1423    }
1424
1425    impl Default for MiniRouter {
1426        fn default() -> Self {
1427            Self::new()
1428        }
1429    }
1430}
1431
1432// ============================================================================
1433// Tests
1434// ============================================================================
1435
1436#[cfg(test)]
1437mod tests {
1438    use super::*;
1439
1440    #[test]
1441    fn test_encode_decode_value() {
1442        let mut buf = [0u8; 16];
1443
1444        // Float
1445        let n = encode_value(&mut buf, &Value::Float(1.25));
1446        assert_eq!(n, 9);
1447        let (v, consumed) = decode_value(&buf).unwrap();
1448        assert_eq!(consumed, 9);
1449        assert!((v.as_float().unwrap() - 1.25).abs() < 0.001);
1450
1451        // Int
1452        let n = encode_value(&mut buf, &Value::Int(-42));
1453        let (v, _) = decode_value(&buf).unwrap();
1454        assert_eq!(v.as_int(), Some(-42));
1455    }
1456
1457    #[test]
1458    fn test_encode_decode_set() {
1459        let mut buf = [0u8; 64];
1460        let n = encode_set_frame(&mut buf, "/test/value", &Value::Float(1.5));
1461        assert!(n > HEADER_SIZE);
1462
1463        let (_, payload_len) = decode_header(&buf).unwrap();
1464        let payload = &buf[HEADER_SIZE..HEADER_SIZE + payload_len];
1465        let msg = decode_message(payload).unwrap();
1466
1467        match msg {
1468            Message::Set { address, value } => {
1469                assert_eq!(address, "/test/value");
1470                assert!((value.as_float().unwrap() - 1.5).abs() < 0.001);
1471            }
1472            _ => panic!("Expected Set message"),
1473        }
1474    }
1475
1476    #[test]
1477    fn test_client_flow() {
1478        let mut client = Client::new();
1479        assert_eq!(client.state, ClientState::Disconnected);
1480
1481        // Prepare hello
1482        let hello = client.prepare_hello("ESP32");
1483        assert!(hello.len() > HEADER_SIZE);
1484
1485        // Simulate welcome response (binary format: type + version + features + time + session + name)
1486        let mut welcome_buf = [0u8; 64];
1487        let payload_start = HEADER_SIZE;
1488        let mut offset = payload_start;
1489
1490        // Message type
1491        welcome_buf[offset] = msg::WELCOME;
1492        offset += 1;
1493
1494        // Version
1495        welcome_buf[offset] = VERSION;
1496        offset += 1;
1497
1498        // Features flags
1499        welcome_buf[offset] = 0xF8;
1500        offset += 1;
1501
1502        // Server time (u64)
1503        welcome_buf[offset..offset + 8].copy_from_slice(&0u64.to_be_bytes());
1504        offset += 8;
1505
1506        // Session ID
1507        offset += encode_string(&mut welcome_buf[offset..], "session123");
1508
1509        // Server name
1510        offset += encode_string(&mut welcome_buf[offset..], "TestRouter");
1511
1512        encode_header(&mut welcome_buf, 0, offset - payload_start);
1513
1514        client.process(&welcome_buf[..offset]);
1515        assert_eq!(client.state, ClientState::Connected);
1516    }
1517
1518    #[test]
1519    fn test_state_cache() {
1520        let mut cache = StateCache::new();
1521
1522        cache.set("/sensor/temp", Value::Float(25.5));
1523        cache.set("/sensor/humidity", Value::Float(60.0));
1524
1525        assert_eq!(cache.get("/sensor/temp").unwrap().as_float(), Some(25.5));
1526        assert_eq!(
1527            cache.get("/sensor/humidity").unwrap().as_float(),
1528            Some(60.0)
1529        );
1530        assert!(cache.get("/unknown").is_none());
1531    }
1532
1533    #[test]
1534    fn test_memory_size() {
1535        let client_size = core::mem::size_of::<Client>();
1536        let cache_size = core::mem::size_of::<StateCache>();
1537
1538        // Client should be under 4KB
1539        assert!(
1540            client_size < 4096,
1541            "Client too large: {} bytes",
1542            client_size
1543        );
1544
1545        // Total memory budget check
1546        let total = client_size + 1024; // + some working memory
1547        assert!(total < 8192, "Total too large: {} bytes", total);
1548    }
1549
1550    #[cfg(feature = "server")]
1551    #[test]
1552    fn test_mini_router() {
1553        use server::MiniRouter;
1554
1555        let mut router = MiniRouter::new();
1556        router.set("/light/brightness", Value::Float(0.8));
1557
1558        assert_eq!(
1559            router.get("/light/brightness").unwrap().as_float(),
1560            Some(0.8)
1561        );
1562    }
1563
1564    #[cfg(feature = "server")]
1565    #[test]
1566    fn test_mini_router_subscriptions() {
1567        use server::{MiniRouter, Session, Subscription};
1568
1569        // Test subscription pattern matching
1570        let mut sub = Subscription::empty();
1571        sub.active = true;
1572        sub.pattern_len = "/light/*".len();
1573        sub.pattern[..sub.pattern_len].copy_from_slice(b"/light/*");
1574
1575        assert!(sub.matches("/light/brightness"));
1576        assert!(sub.matches("/light/color"));
1577        assert!(!sub.matches("/audio/volume"));
1578        assert!(!sub.matches("/light/zone/1"));
1579
1580        // Test ** wildcard
1581        sub.pattern_len = "/light/**".len();
1582        sub.pattern[..sub.pattern_len].copy_from_slice(b"/light/**");
1583        assert!(sub.matches("/light/brightness"));
1584        assert!(sub.matches("/light/zone/1/brightness"));
1585        assert!(!sub.matches("/audio/volume"));
1586
1587        // Test session subscriptions
1588        let mut session = Session::new();
1589        session.active = true;
1590        assert!(session.subscribe(1, "/light/*"));
1591        assert!(session.subscribe(2, "/audio/**"));
1592        assert_eq!(session.sub_count, 2);
1593
1594        assert!(session.has_match("/light/brightness"));
1595        assert!(session.has_match("/audio/master/volume"));
1596        assert!(!session.has_match("/midi/cc/1"));
1597
1598        // Unsubscribe
1599        assert!(session.unsubscribe(1));
1600        assert_eq!(session.sub_count, 1);
1601        assert!(!session.has_match("/light/brightness"));
1602        assert!(session.has_match("/audio/master/volume"));
1603    }
1604
1605    #[cfg(feature = "server")]
1606    #[test]
1607    fn test_mini_router_broadcast() {
1608        use server::MiniRouter;
1609
1610        let mut router = MiniRouter::new();
1611
1612        // Simulate two clients connecting
1613        // Client 0 subscribes to /light/**
1614        // Client 1 subscribes to /audio/**
1615
1616        // Create sessions manually (normally done via HELLO message)
1617        {
1618            let session = router.session_mut(0).unwrap();
1619            session.active = true;
1620            session.id = 0;
1621            session.subscribe(1, "/light/**");
1622        }
1623
1624        {
1625            let session = router.session_mut(1).unwrap();
1626            session.active = true;
1627            session.id = 1;
1628            session.subscribe(1, "/audio/**");
1629        }
1630
1631        // Client 2 sends a SET to /light/brightness
1632        router.set("/light/brightness", Value::Float(0.75));
1633
1634        // Get broadcast targets (excluding sender 2)
1635        let targets = router.get_broadcast_targets("/light/brightness", 2);
1636        assert_eq!(targets.count, 1);
1637        assert!(targets.clients[0]); // Client 0 should receive
1638        assert!(!targets.clients[1]); // Client 1 should NOT receive
1639
1640        // Test audio broadcast
1641        let targets = router.get_broadcast_targets("/audio/volume", 2);
1642        assert_eq!(targets.count, 1);
1643        assert!(!targets.clients[0]); // Client 0 should NOT receive
1644        assert!(targets.clients[1]); // Client 1 should receive
1645
1646        // Test that sender doesn't receive their own broadcast
1647        let targets = router.get_broadcast_targets("/light/brightness", 0);
1648        assert_eq!(targets.count, 0); // Client 0 sent it, so no one else matches
1649    }
1650
1651    #[test]
1652    fn test_new_message_types() {
1653        // Test ANNOUNCE message decoding
1654        let announce_payload = [msg::ANNOUNCE, 0x00, 0x03]; // 3 signals
1655        let msg = decode_message(&announce_payload).unwrap();
1656        match msg {
1657            Message::Announce { signal_count } => assert_eq!(signal_count, 3),
1658            _ => panic!("Expected Announce message"),
1659        }
1660
1661        // Test SYNC message decoding
1662        let sync_payload = [
1663            msg::SYNC,
1664            0x00,
1665            0x00,
1666            0x01,
1667            0x90,
1668            0x9F,
1669            0x8D,
1670            0x80,
1671            0x00, // timestamp
1672        ];
1673        let msg = decode_message(&sync_payload).unwrap();
1674        match msg {
1675            Message::Sync { timestamp } => assert!(timestamp > 0),
1676            _ => panic!("Expected Sync message"),
1677        }
1678
1679        // Test BUNDLE message decoding
1680        let bundle_payload = [msg::BUNDLE, 0x00, 0x00, 0x05]; // flags + 5 messages
1681        let msg = decode_message(&bundle_payload).unwrap();
1682        match msg {
1683            Message::Bundle { message_count } => assert_eq!(message_count, 5),
1684            _ => panic!("Expected Bundle message"),
1685        }
1686
1687        // Test QUERY message decoding
1688        let mut query_payload = [0u8; 32];
1689        query_payload[0] = msg::QUERY;
1690        let pattern = "/test/**";
1691        let len = (pattern.len() as u16).to_be_bytes();
1692        query_payload[1] = len[0];
1693        query_payload[2] = len[1];
1694        query_payload[3..3 + pattern.len()].copy_from_slice(pattern.as_bytes());
1695
1696        let msg = decode_message(&query_payload[..3 + pattern.len()]).unwrap();
1697        match msg {
1698            Message::Query { pattern: p } => assert_eq!(p, "/test/**"),
1699            _ => panic!("Expected Query message"),
1700        }
1701
1702        // Test RESULT message decoding
1703        let result_payload = [msg::RESULT, 0x00, 0x0A]; // 10 signals
1704        let msg = decode_message(&result_payload).unwrap();
1705        match msg {
1706            Message::Result { signal_count } => assert_eq!(signal_count, 10),
1707            _ => panic!("Expected Result message"),
1708        }
1709    }
1710
1711    #[cfg(feature = "alloc")]
1712    #[test]
1713    fn test_value_ext_string() {
1714        let mut buf = [0u8; 64];
1715
1716        // Encode string
1717        let value = ValueExt::String("hello world".into());
1718        let n = encode_value_ext(&mut buf, &value);
1719        assert!(n > 0);
1720
1721        // Decode string
1722        let (decoded, consumed) = decode_value_ext(&buf).unwrap();
1723        assert_eq!(consumed, n);
1724        assert_eq!(decoded.as_str(), Some("hello world"));
1725    }
1726
1727    #[cfg(feature = "alloc")]
1728    #[test]
1729    fn test_value_ext_bytes() {
1730        use alloc::vec;
1731
1732        let mut buf = [0u8; 64];
1733
1734        // Encode bytes
1735        let data = vec![0x01, 0x02, 0x03, 0x04];
1736        let value = ValueExt::Bytes(data.clone());
1737        let n = encode_value_ext(&mut buf, &value);
1738        assert!(n > 0);
1739
1740        // Decode bytes
1741        let (decoded, consumed) = decode_value_ext(&buf).unwrap();
1742        assert_eq!(consumed, n);
1743        assert_eq!(decoded.as_bytes(), Some(data.as_slice()));
1744    }
1745
1746    #[cfg(feature = "alloc")]
1747    #[test]
1748    fn test_value_ext_conversion() {
1749        // Convert Value to ValueExt
1750        let v = Value::Float(1.25);
1751        let ve = ValueExt::from_value(v);
1752        assert!((ve.as_float().unwrap() - 1.25).abs() < 0.001);
1753
1754        // Convert ValueExt to Value (for non-heap types)
1755        let ve = ValueExt::Int(42);
1756        let v = ve.to_value().unwrap();
1757        assert_eq!(v.as_int(), Some(42));
1758
1759        // String/Bytes can't convert to Value
1760        let ve = ValueExt::String("test".into());
1761        assert!(ve.to_value().is_none());
1762    }
1763}