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][has_ttl: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][has_ttl: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 Default for Session {
1196        fn default() -> Self {
1197            Self::new()
1198        }
1199    }
1200
1201    impl Session {
1202        pub const fn new() -> Self {
1203            Self {
1204                active: false,
1205                id: 0,
1206                subscriptions: [const { Subscription::empty() }; MAX_SUBS_PER_CLIENT],
1207                sub_count: 0,
1208            }
1209        }
1210
1211        /// Add a subscription
1212        pub fn subscribe(&mut self, id: u32, pattern: &str) -> bool {
1213            if self.sub_count as usize >= MAX_SUBS_PER_CLIENT {
1214                return false;
1215            }
1216            if pattern.len() > MAX_PATTERN_LEN {
1217                return false;
1218            }
1219
1220            // Find empty slot
1221            for sub in &mut self.subscriptions {
1222                if !sub.active {
1223                    sub.active = true;
1224                    sub.id = id;
1225                    sub.pattern[..pattern.len()].copy_from_slice(pattern.as_bytes());
1226                    sub.pattern_len = pattern.len();
1227                    self.sub_count += 1;
1228                    return true;
1229                }
1230            }
1231            false
1232        }
1233
1234        /// Remove a subscription by ID
1235        pub fn unsubscribe(&mut self, id: u32) -> bool {
1236            for sub in &mut self.subscriptions {
1237                if sub.active && sub.id == id {
1238                    sub.active = false;
1239                    sub.pattern_len = 0;
1240                    self.sub_count = self.sub_count.saturating_sub(1);
1241                    return true;
1242                }
1243            }
1244            false
1245        }
1246
1247        /// Check if any subscription matches the address
1248        pub fn has_match(&self, address: &str) -> bool {
1249            self.subscriptions.iter().any(|s| s.matches(address))
1250        }
1251    }
1252
1253    /// Broadcast result - which clients should receive a message
1254    pub struct BroadcastList {
1255        pub clients: [bool; MAX_CLIENTS],
1256        pub count: u8,
1257    }
1258
1259    impl BroadcastList {
1260        pub const fn empty() -> Self {
1261            Self {
1262                clients: [false; MAX_CLIENTS],
1263                count: 0,
1264            }
1265        }
1266    }
1267
1268    /// Minimal embedded router with subscription support
1269    ///
1270    /// Can act as a local hub for sensors/actuators, forwarding to a main router.
1271    pub struct MiniRouter {
1272        pub state: StateCache,
1273        sessions: [Session; MAX_CLIENTS],
1274        session_count: u8,
1275        tx_buf: [u8; TX_BUF_SIZE],
1276    }
1277
1278    impl MiniRouter {
1279        pub const fn new() -> Self {
1280            Self {
1281                state: StateCache::new(),
1282                sessions: [const { Session::new() }; MAX_CLIENTS],
1283                session_count: 0,
1284                tx_buf: [0; TX_BUF_SIZE],
1285            }
1286        }
1287
1288        /// Process incoming message from a client
1289        ///
1290        /// Returns a response frame to send back to the client (if any)
1291        pub fn process(&mut self, client_id: u8, data: &[u8]) -> Option<&[u8]> {
1292            let (_, payload_len) = decode_header(data)?;
1293            let payload = &data[HEADER_SIZE..HEADER_SIZE + payload_len];
1294            let msg = decode_message(payload)?;
1295
1296            match msg {
1297                Message::Hello { name: _, .. } => {
1298                    self.create_session(client_id);
1299                    Some(self.prepare_welcome(client_id))
1300                }
1301                Message::Subscribe { id, pattern } => {
1302                    self.handle_subscribe(client_id, id, pattern);
1303                    None // ACK could be sent
1304                }
1305                Message::Unsubscribe { id } => {
1306                    self.handle_unsubscribe(client_id, id);
1307                    None
1308                }
1309                Message::Set { address, value } => {
1310                    self.state.set(address, value);
1311                    None // Broadcast handled separately via get_broadcast_targets
1312                }
1313                Message::Ping => Some(self.prepare_pong()),
1314                _ => None,
1315            }
1316        }
1317
1318        /// Get list of clients that should receive a broadcast for an address
1319        ///
1320        /// Call this after processing a SET to get which clients need the update
1321        pub fn get_broadcast_targets(&self, address: &str, sender_id: u8) -> BroadcastList {
1322            let mut result = BroadcastList::empty();
1323
1324            for (i, session) in self.sessions.iter().enumerate() {
1325                // Don't send back to sender, only to other active sessions with matching subs
1326                if session.active && i as u8 != sender_id && session.has_match(address) {
1327                    result.clients[i] = true;
1328                    result.count += 1;
1329                }
1330            }
1331
1332            result
1333        }
1334
1335        /// Prepare a SET frame for broadcasting to subscribers
1336        ///
1337        /// Returns the frame bytes to send to each matching client
1338        pub fn prepare_broadcast(&mut self, address: &str, value: Value) -> &[u8] {
1339            let n = encode_set_frame(&mut self.tx_buf, address, &value);
1340            &self.tx_buf[..n]
1341        }
1342
1343        fn handle_subscribe(&mut self, client_id: u8, id: u32, pattern: &str) {
1344            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1345                if session.active {
1346                    session.subscribe(id, pattern);
1347                }
1348            }
1349        }
1350
1351        fn handle_unsubscribe(&mut self, client_id: u8, id: u32) {
1352            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1353                if session.active {
1354                    session.unsubscribe(id);
1355                }
1356            }
1357        }
1358
1359        fn create_session(&mut self, client_id: u8) {
1360            if (client_id as usize) < MAX_CLIENTS {
1361                self.sessions[client_id as usize] = Session {
1362                    active: true,
1363                    id: client_id,
1364                    subscriptions: [const { Subscription::empty() }; MAX_SUBS_PER_CLIENT],
1365                    sub_count: 0,
1366                };
1367                self.session_count += 1;
1368            }
1369        }
1370
1371        /// Remove a client session
1372        pub fn disconnect(&mut self, client_id: u8) {
1373            if let Some(session) = self.sessions.get_mut(client_id as usize) {
1374                if session.active {
1375                    session.active = false;
1376                    session.sub_count = 0;
1377                    self.session_count = self.session_count.saturating_sub(1);
1378                }
1379            }
1380        }
1381
1382        fn prepare_welcome(&mut self, _client_id: u8) -> &[u8] {
1383            let payload_start = HEADER_SIZE;
1384            let mut offset = payload_start;
1385
1386            self.tx_buf[offset] = msg::WELCOME;
1387            offset += 1;
1388
1389            self.tx_buf[offset] = VERSION;
1390            offset += 1;
1391
1392            self.tx_buf[offset] = 0xF8; // param|event|stream|gesture|timeline
1393            offset += 1;
1394
1395            self.tx_buf[offset..offset + 8].copy_from_slice(&0u64.to_be_bytes());
1396            offset += 8;
1397
1398            offset += encode_string(&mut self.tx_buf[offset..], "embedded");
1399            offset += encode_string(&mut self.tx_buf[offset..], "MiniRouter");
1400
1401            let payload_len = offset - payload_start;
1402            encode_header(&mut self.tx_buf, 0, payload_len);
1403
1404            &self.tx_buf[..offset]
1405        }
1406
1407        fn prepare_pong(&mut self) -> &[u8] {
1408            let n = encode_pong_frame(&mut self.tx_buf);
1409            &self.tx_buf[..n]
1410        }
1411
1412        pub fn get(&self, address: &str) -> Option<Value> {
1413            self.state.get(address)
1414        }
1415
1416        pub fn set(&mut self, address: &str, value: Value) {
1417            self.state.set(address, value);
1418        }
1419
1420        /// Get number of active sessions
1421        pub fn session_count(&self) -> u8 {
1422            self.session_count
1423        }
1424
1425        /// Get mutable access to a session (for testing/setup)
1426        pub fn session_mut(&mut self, client_id: u8) -> Option<&mut Session> {
1427            self.sessions.get_mut(client_id as usize)
1428        }
1429    }
1430
1431    impl Default for MiniRouter {
1432        fn default() -> Self {
1433            Self::new()
1434        }
1435    }
1436}
1437
1438// ============================================================================
1439// Tests
1440// ============================================================================
1441
1442#[cfg(test)]
1443mod tests {
1444    use super::*;
1445
1446    #[test]
1447    fn test_encode_decode_value() {
1448        let mut buf = [0u8; 16];
1449
1450        // Float
1451        let n = encode_value(&mut buf, &Value::Float(1.25));
1452        assert_eq!(n, 9);
1453        let (v, consumed) = decode_value(&buf).unwrap();
1454        assert_eq!(consumed, 9);
1455        assert!((v.as_float().unwrap() - 1.25).abs() < 0.001);
1456
1457        // Int
1458        let _n = encode_value(&mut buf, &Value::Int(-42));
1459        let (v, _) = decode_value(&buf).unwrap();
1460        assert_eq!(v.as_int(), Some(-42));
1461    }
1462
1463    #[test]
1464    fn test_encode_decode_set() {
1465        let mut buf = [0u8; 64];
1466        let n = encode_set_frame(&mut buf, "/test/value", &Value::Float(1.5));
1467        assert!(n > HEADER_SIZE);
1468
1469        let (_, payload_len) = decode_header(&buf).unwrap();
1470        let payload = &buf[HEADER_SIZE..HEADER_SIZE + payload_len];
1471        let msg = decode_message(payload).unwrap();
1472
1473        match msg {
1474            Message::Set { address, value } => {
1475                assert_eq!(address, "/test/value");
1476                assert!((value.as_float().unwrap() - 1.5).abs() < 0.001);
1477            }
1478            _ => panic!("Expected Set message"),
1479        }
1480    }
1481
1482    #[test]
1483    fn test_client_flow() {
1484        let mut client = Client::new();
1485        assert_eq!(client.state, ClientState::Disconnected);
1486
1487        // Prepare hello
1488        let hello = client.prepare_hello("ESP32");
1489        assert!(hello.len() > HEADER_SIZE);
1490
1491        // Simulate welcome response (binary format: type + version + features + time + session + name)
1492        let mut welcome_buf = [0u8; 64];
1493        let payload_start = HEADER_SIZE;
1494        let mut offset = payload_start;
1495
1496        // Message type
1497        welcome_buf[offset] = msg::WELCOME;
1498        offset += 1;
1499
1500        // Version
1501        welcome_buf[offset] = VERSION;
1502        offset += 1;
1503
1504        // Features flags
1505        welcome_buf[offset] = 0xF8;
1506        offset += 1;
1507
1508        // Server time (u64)
1509        welcome_buf[offset..offset + 8].copy_from_slice(&0u64.to_be_bytes());
1510        offset += 8;
1511
1512        // Session ID
1513        offset += encode_string(&mut welcome_buf[offset..], "session123");
1514
1515        // Server name
1516        offset += encode_string(&mut welcome_buf[offset..], "TestRouter");
1517
1518        encode_header(&mut welcome_buf, 0, offset - payload_start);
1519
1520        client.process(&welcome_buf[..offset]);
1521        assert_eq!(client.state, ClientState::Connected);
1522    }
1523
1524    #[test]
1525    fn test_state_cache() {
1526        let mut cache = StateCache::new();
1527
1528        cache.set("/sensor/temp", Value::Float(25.5));
1529        cache.set("/sensor/humidity", Value::Float(60.0));
1530
1531        assert_eq!(cache.get("/sensor/temp").unwrap().as_float(), Some(25.5));
1532        assert_eq!(
1533            cache.get("/sensor/humidity").unwrap().as_float(),
1534            Some(60.0)
1535        );
1536        assert!(cache.get("/unknown").is_none());
1537    }
1538
1539    #[test]
1540    fn test_memory_size() {
1541        let client_size = core::mem::size_of::<Client>();
1542        let _cache_size = core::mem::size_of::<StateCache>();
1543
1544        // Client should be under 4KB
1545        assert!(
1546            client_size < 4096,
1547            "Client too large: {} bytes",
1548            client_size
1549        );
1550
1551        // Total memory budget check
1552        let total = client_size + 1024; // + some working memory
1553        assert!(total < 8192, "Total too large: {} bytes", total);
1554    }
1555
1556    #[cfg(feature = "server")]
1557    #[test]
1558    fn test_mini_router() {
1559        use server::MiniRouter;
1560
1561        let mut router = MiniRouter::new();
1562        router.set("/light/brightness", Value::Float(0.8));
1563
1564        assert_eq!(
1565            router.get("/light/brightness").unwrap().as_float(),
1566            Some(0.8)
1567        );
1568    }
1569
1570    #[cfg(feature = "server")]
1571    #[test]
1572    fn test_mini_router_subscriptions() {
1573        use server::{Session, Subscription};
1574
1575        // Test subscription pattern matching
1576        let mut sub = Subscription::empty();
1577        sub.active = true;
1578        sub.pattern_len = "/light/*".len();
1579        sub.pattern[..sub.pattern_len].copy_from_slice(b"/light/*");
1580
1581        assert!(sub.matches("/light/brightness"));
1582        assert!(sub.matches("/light/color"));
1583        assert!(!sub.matches("/audio/volume"));
1584        assert!(!sub.matches("/light/zone/1"));
1585
1586        // Test ** wildcard
1587        sub.pattern_len = "/light/**".len();
1588        sub.pattern[..sub.pattern_len].copy_from_slice(b"/light/**");
1589        assert!(sub.matches("/light/brightness"));
1590        assert!(sub.matches("/light/zone/1/brightness"));
1591        assert!(!sub.matches("/audio/volume"));
1592
1593        // Test session subscriptions
1594        let mut session = Session::new();
1595        session.active = true;
1596        assert!(session.subscribe(1, "/light/*"));
1597        assert!(session.subscribe(2, "/audio/**"));
1598        assert_eq!(session.sub_count, 2);
1599
1600        assert!(session.has_match("/light/brightness"));
1601        assert!(session.has_match("/audio/master/volume"));
1602        assert!(!session.has_match("/midi/cc/1"));
1603
1604        // Unsubscribe
1605        assert!(session.unsubscribe(1));
1606        assert_eq!(session.sub_count, 1);
1607        assert!(!session.has_match("/light/brightness"));
1608        assert!(session.has_match("/audio/master/volume"));
1609    }
1610
1611    #[cfg(feature = "server")]
1612    #[test]
1613    fn test_mini_router_broadcast() {
1614        use server::MiniRouter;
1615
1616        let mut router = MiniRouter::new();
1617
1618        // Simulate two clients connecting
1619        // Client 0 subscribes to /light/**
1620        // Client 1 subscribes to /audio/**
1621
1622        // Create sessions manually (normally done via HELLO message)
1623        {
1624            let session = router.session_mut(0).unwrap();
1625            session.active = true;
1626            session.id = 0;
1627            session.subscribe(1, "/light/**");
1628        }
1629
1630        {
1631            let session = router.session_mut(1).unwrap();
1632            session.active = true;
1633            session.id = 1;
1634            session.subscribe(1, "/audio/**");
1635        }
1636
1637        // Client 2 sends a SET to /light/brightness
1638        router.set("/light/brightness", Value::Float(0.75));
1639
1640        // Get broadcast targets (excluding sender 2)
1641        let targets = router.get_broadcast_targets("/light/brightness", 2);
1642        assert_eq!(targets.count, 1);
1643        assert!(targets.clients[0]); // Client 0 should receive
1644        assert!(!targets.clients[1]); // Client 1 should NOT receive
1645
1646        // Test audio broadcast
1647        let targets = router.get_broadcast_targets("/audio/volume", 2);
1648        assert_eq!(targets.count, 1);
1649        assert!(!targets.clients[0]); // Client 0 should NOT receive
1650        assert!(targets.clients[1]); // Client 1 should receive
1651
1652        // Test that sender doesn't receive their own broadcast
1653        let targets = router.get_broadcast_targets("/light/brightness", 0);
1654        assert_eq!(targets.count, 0); // Client 0 sent it, so no one else matches
1655    }
1656
1657    #[test]
1658    fn test_new_message_types() {
1659        // Test ANNOUNCE message decoding
1660        let announce_payload = [msg::ANNOUNCE, 0x00, 0x03]; // 3 signals
1661        let msg = decode_message(&announce_payload).unwrap();
1662        match msg {
1663            Message::Announce { signal_count } => assert_eq!(signal_count, 3),
1664            _ => panic!("Expected Announce message"),
1665        }
1666
1667        // Test SYNC message decoding
1668        let sync_payload = [
1669            msg::SYNC,
1670            0x00,
1671            0x00,
1672            0x01,
1673            0x90,
1674            0x9F,
1675            0x8D,
1676            0x80,
1677            0x00, // timestamp
1678        ];
1679        let msg = decode_message(&sync_payload).unwrap();
1680        match msg {
1681            Message::Sync { timestamp } => assert!(timestamp > 0),
1682            _ => panic!("Expected Sync message"),
1683        }
1684
1685        // Test BUNDLE message decoding
1686        let bundle_payload = [msg::BUNDLE, 0x00, 0x00, 0x05]; // flags + 5 messages
1687        let msg = decode_message(&bundle_payload).unwrap();
1688        match msg {
1689            Message::Bundle { message_count } => assert_eq!(message_count, 5),
1690            _ => panic!("Expected Bundle message"),
1691        }
1692
1693        // Test QUERY message decoding
1694        let mut query_payload = [0u8; 32];
1695        query_payload[0] = msg::QUERY;
1696        let pattern = "/test/**";
1697        let len = (pattern.len() as u16).to_be_bytes();
1698        query_payload[1] = len[0];
1699        query_payload[2] = len[1];
1700        query_payload[3..3 + pattern.len()].copy_from_slice(pattern.as_bytes());
1701
1702        let msg = decode_message(&query_payload[..3 + pattern.len()]).unwrap();
1703        match msg {
1704            Message::Query { pattern: p } => assert_eq!(p, "/test/**"),
1705            _ => panic!("Expected Query message"),
1706        }
1707
1708        // Test RESULT message decoding
1709        let result_payload = [msg::RESULT, 0x00, 0x0A]; // 10 signals
1710        let msg = decode_message(&result_payload).unwrap();
1711        match msg {
1712            Message::Result { signal_count } => assert_eq!(signal_count, 10),
1713            _ => panic!("Expected Result message"),
1714        }
1715    }
1716
1717    #[cfg(feature = "alloc")]
1718    #[test]
1719    fn test_value_ext_string() {
1720        let mut buf = [0u8; 64];
1721
1722        // Encode string
1723        let value = ValueExt::String("hello world".into());
1724        let n = encode_value_ext(&mut buf, &value);
1725        assert!(n > 0);
1726
1727        // Decode string
1728        let (decoded, consumed) = decode_value_ext(&buf).unwrap();
1729        assert_eq!(consumed, n);
1730        assert_eq!(decoded.as_str(), Some("hello world"));
1731    }
1732
1733    #[cfg(feature = "alloc")]
1734    #[test]
1735    fn test_value_ext_bytes() {
1736        use alloc::vec;
1737
1738        let mut buf = [0u8; 64];
1739
1740        // Encode bytes
1741        let data = vec![0x01, 0x02, 0x03, 0x04];
1742        let value = ValueExt::Bytes(data.clone());
1743        let n = encode_value_ext(&mut buf, &value);
1744        assert!(n > 0);
1745
1746        // Decode bytes
1747        let (decoded, consumed) = decode_value_ext(&buf).unwrap();
1748        assert_eq!(consumed, n);
1749        assert_eq!(decoded.as_bytes(), Some(data.as_slice()));
1750    }
1751
1752    #[cfg(feature = "alloc")]
1753    #[test]
1754    fn test_value_ext_conversion() {
1755        // Convert Value to ValueExt
1756        let v = Value::Float(1.25);
1757        let ve = ValueExt::from_value(v);
1758        assert!((ve.as_float().unwrap() - 1.25).abs() < 0.001);
1759
1760        // Convert ValueExt to Value (for non-heap types)
1761        let ve = ValueExt::Int(42);
1762        let v = ve.to_value().unwrap();
1763        assert_eq!(v.as_int(), Some(42));
1764
1765        // String/Bytes can't convert to Value
1766        let ve = ValueExt::String("test".into());
1767        assert!(ve.to_value().is_none());
1768    }
1769}