Skip to main content

laser_dac/protocols/idn/
protocol.rs

1//! Types and constants that precisely match the IDN protocol specification.
2//!
3//! The ILDA Digital Network (IDN) protocol uses UDP on port 7255 for all communication.
4//! Unlike Ether Dream, IDN uses big-endian byte order for point data.
5
6use byteorder::{ReadBytesExt, WriteBytesExt, BE};
7use std::io;
8
9use crate::point::LaserPoint;
10
11// -------------------------------------------------------------------------------------------------
12//  Constants
13// -------------------------------------------------------------------------------------------------
14
15/// IDN protocol UDP port.
16pub const IDN_PORT: u16 = 7255;
17
18/// Maximum UDP payload size to avoid IP fragmentation.
19pub const MAX_UDP_PAYLOAD: usize = 1454;
20
21// Hello protocol commands
22pub const IDNCMD_VOID: u8 = 0x00;
23pub const IDNCMD_PING_REQUEST: u8 = 0x08;
24pub const IDNCMD_PING_RESPONSE: u8 = 0x09;
25pub const IDNCMD_GROUP_REQUEST: u8 = 0x0C;
26pub const IDNCMD_GROUP_RESPONSE: u8 = 0x0D;
27pub const IDNCMD_SCAN_REQUEST: u8 = 0x10;
28pub const IDNCMD_SCAN_RESPONSE: u8 = 0x11;
29pub const IDNCMD_SERVICEMAP_REQUEST: u8 = 0x12;
30pub const IDNCMD_SERVICEMAP_RESPONSE: u8 = 0x13;
31
32// Parameter commands
33pub const IDNCMD_SERVICE_PARAMS_REQUEST: u8 = 0x20;
34pub const IDNCMD_SERVICE_PARAMS_RESPONSE: u8 = 0x21;
35pub const IDNCMD_UNIT_PARAMS_REQUEST: u8 = 0x22;
36pub const IDNCMD_UNIT_PARAMS_RESPONSE: u8 = 0x23;
37pub const IDNCMD_LINK_PARAMS_REQUEST: u8 = 0x28;
38pub const IDNCMD_LINK_PARAMS_RESPONSE: u8 = 0x29;
39
40// Realtime stream commands
41pub const IDNCMD_RT_CNLMSG: u8 = 0x40;
42pub const IDNCMD_RT_CNLMSG_ACKREQ: u8 = 0x41;
43pub const IDNCMD_RT_CNLMSG_CLOSE: u8 = 0x44;
44pub const IDNCMD_RT_CNLMSG_CLOSE_ACKREQ: u8 = 0x45;
45pub const IDNCMD_RT_ABORT: u8 = 0x46;
46pub const IDNCMD_RT_ACKNOWLEDGE: u8 = 0x47;
47
48// Packet flags masks
49pub const IDNMSK_PKTFLAGS_GROUP: u8 = 0x0F;
50
51// Scan response status flags
52pub const IDNFLG_SCAN_STATUS_MALFUNCTION: u8 = 0x80;
53pub const IDNFLG_SCAN_STATUS_OFFLINE: u8 = 0x40;
54pub const IDNFLG_SCAN_STATUS_EXCLUDED: u8 = 0x20;
55pub const IDNFLG_SCAN_STATUS_OCCUPIED: u8 = 0x10;
56pub const IDNFLG_SCAN_STATUS_REALTIME: u8 = 0x01;
57
58// Group operation codes (idn-hello.h:84-89)
59pub const IDNVAL_GROUPOP_SUCCESS: i8 = 0x00;
60pub const IDNVAL_GROUPOP_GETMASK: i8 = 0x01;
61pub const IDNVAL_GROUPOP_SETMASK: i8 = 0x02;
62pub const IDNVAL_GROUPOP_ERR_AUTH: i8 = -3; // 0xFD
63pub const IDNVAL_GROUPOP_ERR_OPERATION: i8 = -2; // 0xFE
64pub const IDNVAL_GROUPOP_ERR_REQUEST: i8 = -1; // 0xFF
65
66// Service types
67pub const IDNVAL_STYPE_RELAY: u8 = 0x00;
68pub const IDNVAL_STYPE_UART: u8 = 0x04;
69pub const IDNVAL_STYPE_DMX512: u8 = 0x05;
70pub const IDNVAL_STYPE_LAPRO: u8 = 0x80; // Standard laser projector
71
72// RT-ACK result codes (spec section 6.3.3)
73/// RT-ACK result code: empty close.
74pub const IDNVAL_RTACK_ERR_EMPTY_CLOSE: u8 = 0xEB;
75/// RT-ACK result code: sessions occupied (another client is already connected).
76pub const IDNVAL_RTACK_ERR_OCCUPIED: u8 = 0xEC;
77/// RT-ACK result code: group excluded (server is excluded from this group).
78pub const IDNVAL_RTACK_ERR_EXCLUDED: u8 = 0xED;
79/// RT-ACK result code: invalid payload.
80pub const IDNVAL_RTACK_ERR_INVALID_PAYLOAD: u8 = 0xEE;
81/// RT-ACK result code: processing error.
82pub const IDNVAL_RTACK_ERR_PROCESSING_ERROR: u8 = 0xEF;
83
84// Service map entry flags
85/// Default Service ID flag - indicates this is the default service for its type
86pub const IDNFLG_SERVICEMAP_DSID: u8 = 0x01;
87
88// Channel message content IDs
89pub const IDNFLG_CONTENTID_CHANNELMSG: u16 = 0x8000;
90pub const IDNFLG_CONTENTID_CONFIG_LSTFRG: u16 = 0x4000;
91pub const IDNMSK_CONTENTID_CHANNELID: u16 = 0x3F00;
92pub const IDNMSK_CONTENTID_CNKTYPE: u16 = 0x00FF;
93
94// Data chunk types
95pub const IDNVAL_CNKTYPE_VOID: u8 = 0x00;
96pub const IDNVAL_CNKTYPE_LPGRF_WAVE: u8 = 0x01;
97pub const IDNVAL_CNKTYPE_LPGRF_FRAME: u8 = 0x02;
98pub const IDNVAL_CNKTYPE_LPGRF_FRAME_FIRST: u8 = 0x03;
99pub const IDNVAL_CNKTYPE_LPGRF_FRAME_SEQUEL: u8 = 0xC0;
100
101// Channel configuration flags
102pub const IDNFLG_CHNCFG_ROUTING: u8 = 0x01;
103pub const IDNFLG_CHNCFG_CLOSE: u8 = 0x02;
104
105// Service modes
106pub const IDNVAL_SMOD_VOID: u8 = 0x00;
107pub const IDNVAL_SMOD_LPGRF_CONTINUOUS: u8 = 0x01;
108pub const IDNVAL_SMOD_LPGRF_DISCRETE: u8 = 0x02;
109
110// Sample sizes
111pub const XYRGBI_SAMPLE_SIZE: usize = 8;
112pub const XYRGB_HIGHRES_SAMPLE_SIZE: usize = 10;
113pub const EXTENDED_SAMPLE_SIZE: usize = 20;
114
115// -------------------------------------------------------------------------------------------------
116//  Traits
117// -------------------------------------------------------------------------------------------------
118
119/// Parse a null-terminated byte array as a UTF-8 string.
120fn null_terminated_str(bytes: &[u8]) -> &str {
121    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
122    std::str::from_utf8(&bytes[..end]).unwrap_or("")
123}
124
125/// A trait for writing any of the IDN protocol types to bytes.
126pub trait WriteBytes {
127    fn write_bytes<P: WriteToBytes>(&mut self, protocol: P) -> io::Result<()>;
128}
129
130/// A trait for reading any of the IDN protocol types from bytes.
131pub trait ReadBytes {
132    fn read_bytes<P: ReadFromBytes>(&mut self) -> io::Result<P>;
133}
134
135/// Protocol types that may be written to bytes.
136pub trait WriteToBytes {
137    fn write_to_bytes<W: WriteBytesExt>(&self, writer: W) -> io::Result<()>;
138}
139
140/// Protocol types that may be read from bytes.
141pub trait ReadFromBytes: Sized {
142    fn read_from_bytes<R: ReadBytesExt>(reader: R) -> io::Result<Self>;
143}
144
145/// Types that have a constant size when written to or read from bytes.
146pub trait SizeBytes {
147    const SIZE_BYTES: usize;
148}
149
150// -------------------------------------------------------------------------------------------------
151//  Packet Header (4 bytes)
152// -------------------------------------------------------------------------------------------------
153
154/// IDN packet header - present in all IDN packets.
155#[repr(C)]
156#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
157pub struct PacketHeader {
158    /// The command code (IDNCMD_*)
159    pub command: u8,
160    /// Upper 4 bits: Flags; Lower 4 bits: Client group
161    pub flags: u8,
162    /// Sequence counter, must count up
163    pub sequence: u16,
164}
165
166impl WriteToBytes for PacketHeader {
167    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
168        writer.write_u8(self.command)?;
169        writer.write_u8(self.flags)?;
170        writer.write_u16::<BE>(self.sequence)?;
171        Ok(())
172    }
173}
174
175impl ReadFromBytes for PacketHeader {
176    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
177        let command = reader.read_u8()?;
178        let flags = reader.read_u8()?;
179        let sequence = reader.read_u16::<BE>()?;
180        Ok(PacketHeader {
181            command,
182            flags,
183            sequence,
184        })
185    }
186}
187
188impl SizeBytes for PacketHeader {
189    const SIZE_BYTES: usize = 4;
190}
191
192// -------------------------------------------------------------------------------------------------
193//  Scan Response
194// -------------------------------------------------------------------------------------------------
195
196/// Response to a scan request, containing unit identification and status.
197#[repr(C)]
198#[derive(Clone, Debug, PartialEq, Eq, Hash)]
199pub struct ScanResponse {
200    /// Size of the struct (for versioning)
201    pub struct_size: u8,
202    /// Protocol version: Upper 4 bits = Major, Lower 4 bits = Minor
203    pub protocol_version: u8,
204    /// Unit and link status flags
205    pub status: u8,
206    /// Reserved byte
207    pub reserved: u8,
208    /// Unit ID: \[0\] = Len, \[1\] = Cat, \[2..Len\] = ID, padded with '\0'
209    pub unit_id: [u8; 16],
210    /// Hostname, not null-terminated, padded with '\0'
211    pub hostname: [u8; 20],
212}
213
214impl WriteToBytes for ScanResponse {
215    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
216        writer.write_u8(self.struct_size)?;
217        writer.write_u8(self.protocol_version)?;
218        writer.write_u8(self.status)?;
219        writer.write_u8(self.reserved)?;
220        for &byte in &self.unit_id {
221            writer.write_u8(byte)?;
222        }
223        for &byte in &self.hostname {
224            writer.write_u8(byte)?;
225        }
226        Ok(())
227    }
228}
229
230impl ReadFromBytes for ScanResponse {
231    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
232        let struct_size = reader.read_u8()?;
233        let protocol_version = reader.read_u8()?;
234        let status = reader.read_u8()?;
235        let reserved = reader.read_u8()?;
236        let mut unit_id = [0u8; 16];
237        for byte in &mut unit_id {
238            *byte = reader.read_u8()?;
239        }
240        let mut hostname = [0u8; 20];
241        for byte in &mut hostname {
242            *byte = reader.read_u8()?;
243        }
244        Ok(ScanResponse {
245            struct_size,
246            protocol_version,
247            status,
248            reserved,
249            unit_id,
250            hostname,
251        })
252    }
253}
254
255impl SizeBytes for ScanResponse {
256    const SIZE_BYTES: usize = 40;
257}
258
259impl ScanResponse {
260    /// Parse the hostname as a string, trimming null bytes.
261    pub fn hostname_str(&self) -> &str {
262        null_terminated_str(&self.hostname)
263    }
264}
265
266// -------------------------------------------------------------------------------------------------
267//  Service Map Response
268// -------------------------------------------------------------------------------------------------
269
270/// Header for service map response.
271#[repr(C)]
272#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
273pub struct ServiceMapResponseHeader {
274    /// Size of this struct
275    pub struct_size: u8,
276    /// Size of an entry
277    pub entry_size: u8,
278    /// Number of relay entries
279    pub relay_entry_count: u8,
280    /// Number of service entries
281    pub service_entry_count: u8,
282}
283
284impl WriteToBytes for ServiceMapResponseHeader {
285    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
286        writer.write_u8(self.struct_size)?;
287        writer.write_u8(self.entry_size)?;
288        writer.write_u8(self.relay_entry_count)?;
289        writer.write_u8(self.service_entry_count)?;
290        Ok(())
291    }
292}
293
294impl ReadFromBytes for ServiceMapResponseHeader {
295    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
296        let struct_size = reader.read_u8()?;
297        let entry_size = reader.read_u8()?;
298        let relay_entry_count = reader.read_u8()?;
299        let service_entry_count = reader.read_u8()?;
300        Ok(ServiceMapResponseHeader {
301            struct_size,
302            entry_size,
303            relay_entry_count,
304            service_entry_count,
305        })
306    }
307}
308
309impl SizeBytes for ServiceMapResponseHeader {
310    const SIZE_BYTES: usize = 4;
311}
312
313/// An entry in the service map (used for both relays and services).
314#[repr(C)]
315#[derive(Clone, Debug, PartialEq, Eq, Hash)]
316pub struct ServiceMapEntry {
317    /// Service: The ID (!=0); Relay: Must be 0
318    pub service_id: u8,
319    /// The type of the service; Relay: Must be 0
320    pub service_type: u8,
321    /// Status flags and options
322    pub flags: u8,
323    /// Service: Root(0)/Relay(>0); Relay: Number (!=0)
324    pub relay_number: u8,
325    /// Name, not null-terminated, padded with '\0'
326    pub name: [u8; 20],
327}
328
329impl WriteToBytes for ServiceMapEntry {
330    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
331        writer.write_u8(self.service_id)?;
332        writer.write_u8(self.service_type)?;
333        writer.write_u8(self.flags)?;
334        writer.write_u8(self.relay_number)?;
335        for &byte in &self.name {
336            writer.write_u8(byte)?;
337        }
338        Ok(())
339    }
340}
341
342impl ReadFromBytes for ServiceMapEntry {
343    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
344        let service_id = reader.read_u8()?;
345        let service_type = reader.read_u8()?;
346        let flags = reader.read_u8()?;
347        let relay_number = reader.read_u8()?;
348        let mut name = [0u8; 20];
349        for byte in &mut name {
350            *byte = reader.read_u8()?;
351        }
352        Ok(ServiceMapEntry {
353            service_id,
354            service_type,
355            flags,
356            relay_number,
357            name,
358        })
359    }
360}
361
362impl SizeBytes for ServiceMapEntry {
363    const SIZE_BYTES: usize = 24;
364}
365
366impl ServiceMapEntry {
367    /// Parse the name as a string, trimming null bytes.
368    pub fn name_str(&self) -> &str {
369        null_terminated_str(&self.name)
370    }
371
372    /// Check if this entry is a relay (service_id == 0).
373    pub fn is_relay(&self) -> bool {
374        self.service_id == 0
375    }
376}
377
378// -------------------------------------------------------------------------------------------------
379//  Channel Message Header (8 bytes)
380// -------------------------------------------------------------------------------------------------
381
382/// Channel message header for real-time streaming.
383#[repr(C)]
384#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
385pub struct ChannelMessageHeader {
386    /// Total size of the channel message payload
387    pub total_size: u16,
388    /// Content ID with flags and chunk type
389    pub content_id: u16,
390    /// Timestamp in microseconds
391    pub timestamp: u32,
392}
393
394impl WriteToBytes for ChannelMessageHeader {
395    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
396        writer.write_u16::<BE>(self.total_size)?;
397        writer.write_u16::<BE>(self.content_id)?;
398        writer.write_u32::<BE>(self.timestamp)?;
399        Ok(())
400    }
401}
402
403impl ReadFromBytes for ChannelMessageHeader {
404    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
405        let total_size = reader.read_u16::<BE>()?;
406        let content_id = reader.read_u16::<BE>()?;
407        let timestamp = reader.read_u32::<BE>()?;
408        Ok(ChannelMessageHeader {
409            total_size,
410            content_id,
411            timestamp,
412        })
413    }
414}
415
416impl SizeBytes for ChannelMessageHeader {
417    const SIZE_BYTES: usize = 8;
418}
419
420// -------------------------------------------------------------------------------------------------
421//  Channel Configuration Header (4 bytes)
422// -------------------------------------------------------------------------------------------------
423
424/// Channel configuration header, sent when format changes or periodically.
425#[repr(C)]
426#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
427pub struct ChannelConfigHeader {
428    /// Number of descriptor pairs (32-bit words) in the descriptor array
429    pub word_count: u8,
430    /// Upper 4 bits: Decoder flags; Lower 4 bits: Config flags
431    pub flags: u8,
432    /// Service ID to route to
433    pub service_id: u8,
434    /// Service mode (continuous/discrete)
435    pub service_mode: u8,
436}
437
438impl WriteToBytes for ChannelConfigHeader {
439    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
440        writer.write_u8(self.word_count)?;
441        writer.write_u8(self.flags)?;
442        writer.write_u8(self.service_id)?;
443        writer.write_u8(self.service_mode)?;
444        Ok(())
445    }
446}
447
448impl ReadFromBytes for ChannelConfigHeader {
449    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
450        let word_count = reader.read_u8()?;
451        let flags = reader.read_u8()?;
452        let service_id = reader.read_u8()?;
453        let service_mode = reader.read_u8()?;
454        Ok(ChannelConfigHeader {
455            word_count,
456            flags,
457            service_id,
458            service_mode,
459        })
460    }
461}
462
463impl SizeBytes for ChannelConfigHeader {
464    const SIZE_BYTES: usize = 4;
465}
466
467// -------------------------------------------------------------------------------------------------
468//  Sample Chunk Header (4 bytes)
469// -------------------------------------------------------------------------------------------------
470
471/// Sample chunk header containing flags and duration.
472#[repr(C)]
473#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
474pub struct SampleChunkHeader {
475    /// Upper 8 bits: Flags; Lower 24 bits: Duration in microseconds
476    pub flags_duration: u32,
477}
478
479impl SampleChunkHeader {
480    /// Create a new sample chunk header with the given flags and duration.
481    pub fn new(flags: u8, duration_us: u32) -> Self {
482        let flags_duration = ((flags as u32) << 24) | (duration_us & 0x00FF_FFFF);
483        Self { flags_duration }
484    }
485
486    /// Get the flags from the header.
487    pub fn flags(&self) -> u8 {
488        (self.flags_duration >> 24) as u8
489    }
490
491    /// Get the duration in microseconds from the header.
492    pub fn duration_us(&self) -> u32 {
493        self.flags_duration & 0x00FF_FFFF
494    }
495}
496
497impl WriteToBytes for SampleChunkHeader {
498    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
499        writer.write_u32::<BE>(self.flags_duration)?;
500        Ok(())
501    }
502}
503
504impl ReadFromBytes for SampleChunkHeader {
505    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
506        let flags_duration = reader.read_u32::<BE>()?;
507        Ok(SampleChunkHeader { flags_duration })
508    }
509}
510
511impl SizeBytes for SampleChunkHeader {
512    const SIZE_BYTES: usize = 4;
513}
514
515// -------------------------------------------------------------------------------------------------
516//  Acknowledgment Response (10 bytes)
517// -------------------------------------------------------------------------------------------------
518
519/// Acknowledgment response from the server (spec section 6.3.3).
520#[repr(C)]
521#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
522pub struct AcknowledgeResponse {
523    /// Size of this struct (always 10)
524    pub struct_size: u8,
525    /// Result code: 0 = success, negative = error (see IDNVAL_RTACK_ERR_*)
526    pub result_code: i8,
527    /// Input event flags (e.g., interlock triggered)
528    pub input_event_flags: u16,
529    /// Pipeline event flags (e.g., buffer underflow)
530    pub pipeline_event_flags: u16,
531    /// Status flags (e.g., projector ready)
532    pub status_flags: u8,
533    /// Link quality (0-255, higher is better)
534    pub link_quality: u8,
535    /// Latency in microseconds (server processing time)
536    pub latency_us: u16,
537}
538
539impl WriteToBytes for AcknowledgeResponse {
540    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
541        writer.write_u8(self.struct_size)?;
542        writer.write_i8(self.result_code)?;
543        writer.write_u16::<BE>(self.input_event_flags)?;
544        writer.write_u16::<BE>(self.pipeline_event_flags)?;
545        writer.write_u8(self.status_flags)?;
546        writer.write_u8(self.link_quality)?;
547        writer.write_u16::<BE>(self.latency_us)?;
548        Ok(())
549    }
550}
551
552impl ReadFromBytes for AcknowledgeResponse {
553    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
554        let struct_size = reader.read_u8()?;
555        let result_code = reader.read_i8()?;
556        let input_event_flags = reader.read_u16::<BE>()?;
557        let pipeline_event_flags = reader.read_u16::<BE>()?;
558        let status_flags = reader.read_u8()?;
559        let link_quality = reader.read_u8()?;
560        let latency_us = reader.read_u16::<BE>()?;
561        Ok(AcknowledgeResponse {
562            struct_size,
563            result_code,
564            input_event_flags,
565            pipeline_event_flags,
566            status_flags,
567            link_quality,
568            latency_us,
569        })
570    }
571}
572
573impl SizeBytes for AcknowledgeResponse {
574    const SIZE_BYTES: usize = 10;
575}
576
577impl AcknowledgeResponse {
578    /// Check if the acknowledgment indicates success.
579    pub fn is_success(&self) -> bool {
580        self.result_code >= 0
581    }
582
583    /// Get the latency as a Duration.
584    pub fn latency(&self) -> std::time::Duration {
585        std::time::Duration::from_micros(self.latency_us as u64)
586    }
587}
588
589// -------------------------------------------------------------------------------------------------
590//  Client Group Request/Response (idn-hello.h:124-140)
591// -------------------------------------------------------------------------------------------------
592
593/// Client group request payload for get/set operations (16 bytes).
594/// Sent with IDNCMD_GROUP_REQUEST, response via IDNCMD_GROUP_RESPONSE.
595#[repr(C)]
596#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
597pub struct GroupRequest {
598    /// Size of this struct (always 16)
599    pub struct_size: u8,
600    /// Operation code: GETMASK (0x01) or SETMASK (0x02)
601    pub op_code: i8,
602    /// Mask for the client groups
603    pub group_mask: u16,
604    /// Authentication code, padded with '\0'
605    pub auth_code: [u8; 12],
606}
607
608impl GroupRequest {
609    /// Create a GET request to retrieve the current group mask.
610    pub fn get() -> Self {
611        Self {
612            struct_size: 16,
613            op_code: IDNVAL_GROUPOP_GETMASK,
614            group_mask: 0,
615            auth_code: [0u8; 12],
616        }
617    }
618
619    /// Create a SET request to change the group mask.
620    pub fn set(mask: u16) -> Self {
621        Self {
622            struct_size: 16,
623            op_code: IDNVAL_GROUPOP_SETMASK,
624            group_mask: mask,
625            auth_code: [0u8; 12],
626        }
627    }
628
629    /// Create a SET request with authentication.
630    pub fn set_with_auth(mask: u16, auth: &[u8]) -> Self {
631        let mut auth_code = [0u8; 12];
632        let len = auth.len().min(12);
633        auth_code[..len].copy_from_slice(&auth[..len]);
634        Self {
635            struct_size: 16,
636            op_code: IDNVAL_GROUPOP_SETMASK,
637            group_mask: mask,
638            auth_code,
639        }
640    }
641}
642
643impl WriteToBytes for GroupRequest {
644    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
645        writer.write_u8(self.struct_size)?;
646        writer.write_i8(self.op_code)?;
647        writer.write_u16::<BE>(self.group_mask)?;
648        for &byte in &self.auth_code {
649            writer.write_u8(byte)?;
650        }
651        Ok(())
652    }
653}
654
655impl ReadFromBytes for GroupRequest {
656    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
657        let struct_size = reader.read_u8()?;
658        let op_code = reader.read_i8()?;
659        let group_mask = reader.read_u16::<BE>()?;
660        let mut auth_code = [0u8; 12];
661        for byte in &mut auth_code {
662            *byte = reader.read_u8()?;
663        }
664        Ok(GroupRequest {
665            struct_size,
666            op_code,
667            group_mask,
668            auth_code,
669        })
670    }
671}
672
673impl SizeBytes for GroupRequest {
674    const SIZE_BYTES: usize = 16;
675}
676
677/// Client group response payload (4 bytes).
678#[repr(C)]
679#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
680pub struct GroupResponse {
681    /// Size of this struct (always 4)
682    pub struct_size: u8,
683    /// Operation result: success (>=0) or error (<0)
684    pub op_code: i8,
685    /// Current client group mask (16 bits, one per group 0-15)
686    pub group_mask: u16,
687}
688
689impl WriteToBytes for GroupResponse {
690    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
691        writer.write_u8(self.struct_size)?;
692        writer.write_i8(self.op_code)?;
693        writer.write_u16::<BE>(self.group_mask)?;
694        Ok(())
695    }
696}
697
698impl ReadFromBytes for GroupResponse {
699    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
700        let struct_size = reader.read_u8()?;
701        let op_code = reader.read_i8()?;
702        let group_mask = reader.read_u16::<BE>()?;
703        Ok(GroupResponse {
704            struct_size,
705            op_code,
706            group_mask,
707        })
708    }
709}
710
711impl SizeBytes for GroupResponse {
712    const SIZE_BYTES: usize = 4;
713}
714
715impl GroupResponse {
716    /// Check if the operation was successful.
717    pub fn is_success(&self) -> bool {
718        self.op_code >= 0
719    }
720
721    /// Check if a specific group (0-15) is enabled.
722    pub fn is_group_enabled(&self, group: u8) -> bool {
723        if group > 15 {
724            return false;
725        }
726        (self.group_mask & (1 << group)) != 0
727    }
728
729    /// Get list of enabled groups.
730    pub fn enabled_groups(&self) -> Vec<u8> {
731        (0..16).filter(|&g| self.is_group_enabled(g)).collect()
732    }
733}
734
735// -------------------------------------------------------------------------------------------------
736//  Parameter Request/Response
737// -------------------------------------------------------------------------------------------------
738
739/// Parameter get request payload.
740/// The service_id specifies which service's parameter to get.
741#[repr(C)]
742#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
743pub struct ParameterGetRequest {
744    /// Service ID (0 for unit-level parameters)
745    pub service_id: u8,
746    /// Reserved byte
747    pub reserved: u8,
748    /// Parameter ID
749    pub param_id: u16,
750}
751
752impl WriteToBytes for ParameterGetRequest {
753    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
754        writer.write_u8(self.service_id)?;
755        writer.write_u8(self.reserved)?;
756        writer.write_u16::<BE>(self.param_id)?;
757        Ok(())
758    }
759}
760
761impl ReadFromBytes for ParameterGetRequest {
762    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
763        let service_id = reader.read_u8()?;
764        let reserved = reader.read_u8()?;
765        let param_id = reader.read_u16::<BE>()?;
766        Ok(ParameterGetRequest {
767            service_id,
768            reserved,
769            param_id,
770        })
771    }
772}
773
774impl SizeBytes for ParameterGetRequest {
775    const SIZE_BYTES: usize = 4;
776}
777
778/// Parameter response payload (for both get and set responses).
779#[repr(C)]
780#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
781pub struct ParameterResponse {
782    /// Service ID (0 for unit-level parameters)
783    pub service_id: u8,
784    /// Result code: 0 = success, negative = error
785    pub result_code: i8,
786    /// Parameter ID
787    pub param_id: u16,
788    /// Parameter value (interpretation depends on param_id)
789    pub value: u32,
790}
791
792impl WriteToBytes for ParameterResponse {
793    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
794        writer.write_u8(self.service_id)?;
795        writer.write_i8(self.result_code)?;
796        writer.write_u16::<BE>(self.param_id)?;
797        writer.write_u32::<BE>(self.value)?;
798        Ok(())
799    }
800}
801
802impl ReadFromBytes for ParameterResponse {
803    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
804        let service_id = reader.read_u8()?;
805        let result_code = reader.read_i8()?;
806        let param_id = reader.read_u16::<BE>()?;
807        let value = reader.read_u32::<BE>()?;
808        Ok(ParameterResponse {
809            service_id,
810            result_code,
811            param_id,
812            value,
813        })
814    }
815}
816
817impl SizeBytes for ParameterResponse {
818    const SIZE_BYTES: usize = 8;
819}
820
821impl ParameterResponse {
822    /// Check if the response indicates success.
823    pub fn is_success(&self) -> bool {
824        self.result_code >= 0
825    }
826}
827
828/// Parameter set request payload.
829#[repr(C)]
830#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
831pub struct ParameterSetRequest {
832    /// Service ID (0 for unit-level parameters)
833    pub service_id: u8,
834    /// Reserved byte
835    pub reserved: u8,
836    /// Parameter ID
837    pub param_id: u16,
838    /// New parameter value
839    pub value: u32,
840}
841
842impl WriteToBytes for ParameterSetRequest {
843    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
844        writer.write_u8(self.service_id)?;
845        writer.write_u8(self.reserved)?;
846        writer.write_u16::<BE>(self.param_id)?;
847        writer.write_u32::<BE>(self.value)?;
848        Ok(())
849    }
850}
851
852impl ReadFromBytes for ParameterSetRequest {
853    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
854        let service_id = reader.read_u8()?;
855        let reserved = reader.read_u8()?;
856        let param_id = reader.read_u16::<BE>()?;
857        let value = reader.read_u32::<BE>()?;
858        Ok(ParameterSetRequest {
859            service_id,
860            reserved,
861            param_id,
862            value,
863        })
864    }
865}
866
867impl SizeBytes for ParameterSetRequest {
868    const SIZE_BYTES: usize = 8;
869}
870
871// -------------------------------------------------------------------------------------------------
872//  Point Types
873// -------------------------------------------------------------------------------------------------
874
875/// Standard XYRGBI point format (8 bytes).
876/// Coordinates are signed 16-bit, colors are unsigned 8-bit.
877#[repr(C)]
878#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
879pub struct PointXyrgbi {
880    /// X coordinate (-32768 to 32767)
881    pub x: i16,
882    /// Y coordinate (-32768 to 32767)
883    pub y: i16,
884    /// Red channel (0-255)
885    pub r: u8,
886    /// Green channel (0-255)
887    pub g: u8,
888    /// Blue channel (0-255)
889    pub b: u8,
890    /// Intensity channel (0-255)
891    pub i: u8,
892}
893
894impl PointXyrgbi {
895    pub fn new(x: i16, y: i16, r: u8, g: u8, b: u8, i: u8) -> Self {
896        Self { x, y, r, g, b, i }
897    }
898}
899
900impl WriteToBytes for PointXyrgbi {
901    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
902        writer.write_i16::<BE>(self.x)?;
903        writer.write_i16::<BE>(self.y)?;
904        writer.write_u8(self.r)?;
905        writer.write_u8(self.g)?;
906        writer.write_u8(self.b)?;
907        writer.write_u8(self.i)?;
908        Ok(())
909    }
910}
911
912impl PointXyrgbi {
913    /// Batch-encode a slice of points directly into a byte buffer.
914    ///
915    /// This bypasses the per-point `WriteToBytes` trait dispatch and
916    /// `write_all` overhead by doing a single `reserve` upfront and
917    /// extending in a tight loop. Use this in the packet-building hot
918    /// path instead of looping `buf.write_bytes(point)`.
919    #[inline]
920    pub fn encode_batch(points: &[Self], buf: &mut Vec<u8>) {
921        buf.reserve(points.len() * XYRGBI_SAMPLE_SIZE);
922        for p in points {
923            let xb = p.x.to_be_bytes();
924            let yb = p.y.to_be_bytes();
925            buf.extend_from_slice(&[xb[0], xb[1], yb[0], yb[1], p.r, p.g, p.b, p.i]);
926        }
927    }
928}
929
930impl ReadFromBytes for PointXyrgbi {
931    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
932        let x = reader.read_i16::<BE>()?;
933        let y = reader.read_i16::<BE>()?;
934        let r = reader.read_u8()?;
935        let g = reader.read_u8()?;
936        let b = reader.read_u8()?;
937        let i = reader.read_u8()?;
938        Ok(PointXyrgbi { x, y, r, g, b, i })
939    }
940}
941
942impl SizeBytes for PointXyrgbi {
943    const SIZE_BYTES: usize = XYRGBI_SAMPLE_SIZE;
944}
945
946impl From<&LaserPoint> for PointXyrgbi {
947    /// Convert a LaserPoint to an IDN PointXyrgbi.
948    ///
949    /// LaserPoint uses f32 coordinates (-1.0 to 1.0) and u16 colors (0-65535).
950    /// IDN PointXyrgbi uses i16 signed coordinates (-32768 to 32767) and u8 colors.
951    /// Coordinates are inverted to match hardware orientation.
952    fn from(p: &LaserPoint) -> Self {
953        PointXyrgbi::new(
954            LaserPoint::coord_to_i16_inverted(p.x),
955            LaserPoint::coord_to_i16_inverted(p.y),
956            LaserPoint::color_to_u8(p.r),
957            LaserPoint::color_to_u8(p.g),
958            LaserPoint::color_to_u8(p.b),
959            LaserPoint::color_to_u8(p.intensity),
960        )
961    }
962}
963
964/// High-resolution XYRGB point format (10 bytes).
965/// Coordinates are signed 16-bit, colors are unsigned 16-bit.
966#[repr(C)]
967#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
968pub struct PointXyrgbHighRes {
969    /// X coordinate (-32768 to 32767)
970    pub x: i16,
971    /// Y coordinate (-32768 to 32767)
972    pub y: i16,
973    /// Red channel (0-65535)
974    pub r: u16,
975    /// Green channel (0-65535)
976    pub g: u16,
977    /// Blue channel (0-65535)
978    pub b: u16,
979}
980
981impl PointXyrgbHighRes {
982    pub fn new(x: i16, y: i16, r: u16, g: u16, b: u16) -> Self {
983        Self { x, y, r, g, b }
984    }
985}
986
987impl WriteToBytes for PointXyrgbHighRes {
988    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
989        writer.write_i16::<BE>(self.x)?;
990        writer.write_i16::<BE>(self.y)?;
991        writer.write_u16::<BE>(self.r)?;
992        writer.write_u16::<BE>(self.g)?;
993        writer.write_u16::<BE>(self.b)?;
994        Ok(())
995    }
996}
997
998impl ReadFromBytes for PointXyrgbHighRes {
999    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
1000        let x = reader.read_i16::<BE>()?;
1001        let y = reader.read_i16::<BE>()?;
1002        let r = reader.read_u16::<BE>()?;
1003        let g = reader.read_u16::<BE>()?;
1004        let b = reader.read_u16::<BE>()?;
1005        Ok(PointXyrgbHighRes { x, y, r, g, b })
1006    }
1007}
1008
1009impl SizeBytes for PointXyrgbHighRes {
1010    const SIZE_BYTES: usize = XYRGB_HIGHRES_SAMPLE_SIZE;
1011}
1012
1013/// Extended point format (20 bytes).
1014/// Includes user-defined channels for additional colors or effects.
1015#[repr(C)]
1016#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
1017pub struct PointExtended {
1018    /// X coordinate (-32768 to 32767)
1019    pub x: i16,
1020    /// Y coordinate (-32768 to 32767)
1021    pub y: i16,
1022    /// Red channel (0-65535)
1023    pub r: u16,
1024    /// Green channel (0-65535)
1025    pub g: u16,
1026    /// Blue channel (0-65535)
1027    pub b: u16,
1028    /// Intensity channel (0-65535)
1029    pub i: u16,
1030    /// User channel 1 (e.g., deep blue)
1031    pub u1: u16,
1032    /// User channel 2 (e.g., yellow)
1033    pub u2: u16,
1034    /// User channel 3 (e.g., cyan)
1035    pub u3: u16,
1036    /// User channel 4 (e.g., x-prime)
1037    pub u4: u16,
1038}
1039
1040impl PointExtended {
1041    #[allow(clippy::too_many_arguments)]
1042    pub fn new(
1043        x: i16,
1044        y: i16,
1045        r: u16,
1046        g: u16,
1047        b: u16,
1048        i: u16,
1049        u1: u16,
1050        u2: u16,
1051        u3: u16,
1052        u4: u16,
1053    ) -> Self {
1054        Self {
1055            x,
1056            y,
1057            r,
1058            g,
1059            b,
1060            i,
1061            u1,
1062            u2,
1063            u3,
1064            u4,
1065        }
1066    }
1067}
1068
1069impl WriteToBytes for PointExtended {
1070    fn write_to_bytes<W: WriteBytesExt>(&self, mut writer: W) -> io::Result<()> {
1071        writer.write_i16::<BE>(self.x)?;
1072        writer.write_i16::<BE>(self.y)?;
1073        writer.write_u16::<BE>(self.r)?;
1074        writer.write_u16::<BE>(self.g)?;
1075        writer.write_u16::<BE>(self.b)?;
1076        writer.write_u16::<BE>(self.i)?;
1077        writer.write_u16::<BE>(self.u1)?;
1078        writer.write_u16::<BE>(self.u2)?;
1079        writer.write_u16::<BE>(self.u3)?;
1080        writer.write_u16::<BE>(self.u4)?;
1081        Ok(())
1082    }
1083}
1084
1085impl ReadFromBytes for PointExtended {
1086    fn read_from_bytes<R: ReadBytesExt>(mut reader: R) -> io::Result<Self> {
1087        let x = reader.read_i16::<BE>()?;
1088        let y = reader.read_i16::<BE>()?;
1089        let r = reader.read_u16::<BE>()?;
1090        let g = reader.read_u16::<BE>()?;
1091        let b = reader.read_u16::<BE>()?;
1092        let i = reader.read_u16::<BE>()?;
1093        let u1 = reader.read_u16::<BE>()?;
1094        let u2 = reader.read_u16::<BE>()?;
1095        let u3 = reader.read_u16::<BE>()?;
1096        let u4 = reader.read_u16::<BE>()?;
1097        Ok(PointExtended {
1098            x,
1099            y,
1100            r,
1101            g,
1102            b,
1103            i,
1104            u1,
1105            u2,
1106            u3,
1107            u4,
1108        })
1109    }
1110}
1111
1112impl SizeBytes for PointExtended {
1113    const SIZE_BYTES: usize = EXTENDED_SAMPLE_SIZE;
1114}
1115
1116// -------------------------------------------------------------------------------------------------
1117//  Blanket Implementations
1118// -------------------------------------------------------------------------------------------------
1119
1120impl<P> WriteToBytes for &P
1121where
1122    P: WriteToBytes,
1123{
1124    fn write_to_bytes<W: WriteBytesExt>(&self, writer: W) -> io::Result<()> {
1125        (*self).write_to_bytes(writer)
1126    }
1127}
1128
1129impl<W> WriteBytes for W
1130where
1131    W: WriteBytesExt,
1132{
1133    fn write_bytes<P: WriteToBytes>(&mut self, protocol: P) -> io::Result<()> {
1134        protocol.write_to_bytes(self)
1135    }
1136}
1137
1138impl<R> ReadBytes for R
1139where
1140    R: ReadBytesExt,
1141{
1142    fn read_bytes<P: ReadFromBytes>(&mut self) -> io::Result<P> {
1143        P::read_from_bytes(self)
1144    }
1145}
1146
1147// -------------------------------------------------------------------------------------------------
1148//  Point Trait
1149// -------------------------------------------------------------------------------------------------
1150
1151/// Trait for laser point types that can be serialized to IDN format.
1152pub trait Point: WriteToBytes + SizeBytes + Copy {
1153    /// Get the X coordinate.
1154    fn x(&self) -> i16;
1155    /// Get the Y coordinate.
1156    fn y(&self) -> i16;
1157
1158    /// Batch-encode a slice of points into a byte buffer.
1159    ///
1160    /// The default implementation falls back to per-point `write_to_bytes`.
1161    /// Concrete types can override this to bypass per-point trait dispatch
1162    /// (see [`PointXyrgbi`] for the optimized version).
1163    fn encode_batch_into(points: &[Self], buf: &mut Vec<u8>) {
1164        buf.reserve(points.len() * Self::SIZE_BYTES);
1165        for p in points {
1166            // write_to_bytes on Vec<u8> is infallible
1167            let _ = p.write_to_bytes(&mut *buf);
1168        }
1169    }
1170}
1171
1172impl Point for PointXyrgbi {
1173    fn x(&self) -> i16 {
1174        self.x
1175    }
1176    fn y(&self) -> i16 {
1177        self.y
1178    }
1179
1180    #[inline]
1181    fn encode_batch_into(points: &[Self], buf: &mut Vec<u8>) {
1182        Self::encode_batch(points, buf);
1183    }
1184}
1185
1186impl Point for PointXyrgbHighRes {
1187    fn x(&self) -> i16 {
1188        self.x
1189    }
1190    fn y(&self) -> i16 {
1191        self.y
1192    }
1193}
1194
1195impl Point for PointExtended {
1196    fn x(&self) -> i16 {
1197        self.x
1198    }
1199    fn y(&self) -> i16 {
1200        self.y
1201    }
1202}
1203
1204// -------------------------------------------------------------------------------------------------
1205//  Channel Descriptors
1206// -------------------------------------------------------------------------------------------------
1207
1208/// Channel descriptor values for different point formats.
1209/// These match the C++ reference implementation exactly.
1210/// Format: base descriptor followed by precision modifier (0x4010 = 16-bit precision).
1211pub mod channel_descriptors {
1212    /// XYRGBI format descriptor (8 bytes per sample)
1213    /// X, Y with 16-bit precision, then R/G/B at specific wavelengths, plus intensity
1214    pub const XYRGBI: &[u16] = &[
1215        0x4200, 0x4010, // X, 16-bit precision
1216        0x4210, 0x4010, // Y, 16-bit precision
1217        0x527E, // Red, 638nm
1218        0x5214, // Green, 532nm
1219        0x51CC, // Blue, 460nm
1220        0x5C10, // Intensity, legacy signal
1221    ];
1222
1223    /// High-res XYRGB format descriptor (10 bytes per sample)
1224    /// X, Y, R, G, B all with 16-bit precision
1225    pub const XYRGB_HIGHRES: &[u16] = &[
1226        0x4200, 0x4010, // X, 16-bit precision
1227        0x4210, 0x4010, // Y, 16-bit precision
1228        0x527E, 0x4010, // Red 638nm, 16-bit precision
1229        0x5214, 0x4010, // Green 532nm, 16-bit precision
1230        0x51CC, 0x4010, // Blue 460nm, 16-bit precision
1231    ];
1232
1233    /// Extended format descriptor (20 bytes per sample)
1234    /// X, Y, R, G, B, I, plus 4 user channels, all 16-bit precision
1235    pub const EXTENDED: &[u16] = &[
1236        0x4200, 0x4010, // X, 16-bit precision
1237        0x4210, 0x4010, // Y, 16-bit precision
1238        0x527E, 0x4010, // Red 638nm, 16-bit precision
1239        0x5214, 0x4010, // Green 532nm, 16-bit precision
1240        0x51CC, 0x4010, // Blue 460nm, 16-bit precision
1241        0x5C10, 0x4010, // Intensity, 16-bit precision
1242        0x51BD, 0x4010, // User 1 (deep blue 445nm), 16-bit precision
1243        0x5241, 0x4010, // User 2 (yellow 577nm), 16-bit precision
1244        0x51E8, 0x4010, // User 3 (cyan 488nm), 16-bit precision
1245        0x4201, 0x4010, // User 4 (X-prime), 16-bit precision
1246    ];
1247}
1248
1249#[cfg(test)]
1250mod tests {
1251    use super::*;
1252    use crate::point::LaserPoint;
1253
1254    // ==========================================================================
1255    // PointXyrgbi Conversion Tests - Testing the From<&LaserPoint> implementation
1256    // ==========================================================================
1257
1258    #[test]
1259    fn test_idn_conversion_center() {
1260        // Center point (0, 0) should map to (0, 0)
1261        // Colors: u16 values that downscale to expected u8 values (128, 64, 32, 200)
1262        let laser_point = LaserPoint::new(0.0, 0.0, 128 * 257, 64 * 257, 32 * 257, 200 * 257);
1263        let idn_point: PointXyrgbi = (&laser_point).into();
1264
1265        assert_eq!(idn_point.x, 0);
1266        assert_eq!(idn_point.y, 0);
1267        // IDN PointXyrgbi uses u8 colors (downscaled from u16 via >> 8)
1268        assert_eq!(idn_point.r, 128);
1269        assert_eq!(idn_point.g, 64);
1270        assert_eq!(idn_point.b, 32);
1271        assert_eq!(idn_point.i, 200);
1272    }
1273
1274    #[test]
1275    fn test_idn_conversion_min() {
1276        // Min point (-1, -1) should map to (32767, 32767) due to axis inversion
1277        let laser_point = LaserPoint::new(-1.0, -1.0, 0, 0, 0, 0);
1278        let idn_point: PointXyrgbi = (&laser_point).into();
1279
1280        assert_eq!(idn_point.x, 32767);
1281        assert_eq!(idn_point.y, 32767);
1282    }
1283
1284    #[test]
1285    fn test_idn_conversion_max() {
1286        // Max point (1, 1) should map to (-32767, -32767) due to axis inversion
1287        let laser_point = LaserPoint::new(1.0, 1.0, 65535, 65535, 65535, 65535);
1288        let idn_point: PointXyrgbi = (&laser_point).into();
1289
1290        assert_eq!(idn_point.x, -32767);
1291        assert_eq!(idn_point.y, -32767);
1292        // Max u16 (65535) >> 8 = 255
1293        assert_eq!(idn_point.r, 255);
1294        assert_eq!(idn_point.g, 255);
1295        assert_eq!(idn_point.b, 255);
1296        assert_eq!(idn_point.i, 255);
1297    }
1298
1299    #[test]
1300    fn test_idn_conversion_half_values() {
1301        let laser_point = LaserPoint::new(0.5, -0.5, 100 * 257, 100 * 257, 100 * 257, 100 * 257);
1302        let idn_point: PointXyrgbi = (&laser_point).into();
1303
1304        // 0.5 * -32767 = -16383.5 -> -16384 (axis inverted)
1305        assert_eq!(idn_point.x, -16384);
1306        assert_eq!(idn_point.y, 16384);
1307    }
1308
1309    #[test]
1310    fn test_idn_conversion_clamps_out_of_range() {
1311        // Out of range values should clamp, then invert
1312        let laser_point = LaserPoint::new(2.0, -3.0, 65535, 65535, 65535, 65535);
1313        let idn_point: PointXyrgbi = (&laser_point).into();
1314
1315        assert_eq!(idn_point.x, -32767);
1316        assert_eq!(idn_point.y, 32767);
1317    }
1318
1319    #[test]
1320    fn test_idn_coordinate_symmetry() {
1321        // Verify that x and -x produce symmetric results around 0
1322        let p1 = LaserPoint::new(0.5, 0.0, 0, 0, 0, 0);
1323        let p2 = LaserPoint::new(-0.5, 0.0, 0, 0, 0, 0);
1324        let i1: PointXyrgbi = (&p1).into();
1325        let i2: PointXyrgbi = (&p2).into();
1326
1327        assert_eq!(i1.x, -i2.x);
1328    }
1329
1330    #[test]
1331    fn test_idn_conversion_infinity_clamps() {
1332        let laser_point = LaserPoint::new(f32::INFINITY, f32::NEG_INFINITY, 0, 0, 0, 0);
1333        let idn_point: PointXyrgbi = (&laser_point).into();
1334
1335        assert_eq!(idn_point.x, -32767);
1336        assert_eq!(idn_point.y, 32767);
1337    }
1338
1339    // ==========================================================================
1340    // GroupRequest/GroupResponse Tests
1341    // ==========================================================================
1342
1343    #[test]
1344    fn test_group_request_size() {
1345        assert_eq!(GroupRequest::SIZE_BYTES, 16);
1346    }
1347
1348    #[test]
1349    fn test_group_response_size() {
1350        assert_eq!(GroupResponse::SIZE_BYTES, 4);
1351    }
1352
1353    #[test]
1354    fn test_group_request_get_constructor() {
1355        let req = GroupRequest::get();
1356        assert_eq!(req.struct_size, 16);
1357        assert_eq!(req.op_code, IDNVAL_GROUPOP_GETMASK);
1358        assert_eq!(req.group_mask, 0);
1359        assert_eq!(req.auth_code, [0u8; 12]);
1360    }
1361
1362    #[test]
1363    fn test_group_request_set_constructor() {
1364        let req = GroupRequest::set(0xABCD);
1365        assert_eq!(req.struct_size, 16);
1366        assert_eq!(req.op_code, IDNVAL_GROUPOP_SETMASK);
1367        assert_eq!(req.group_mask, 0xABCD);
1368        assert_eq!(req.auth_code, [0u8; 12]);
1369    }
1370
1371    #[test]
1372    fn test_group_request_set_with_auth() {
1373        let auth = b"secret123";
1374        let req = GroupRequest::set_with_auth(0x1234, auth);
1375        assert_eq!(req.struct_size, 16);
1376        assert_eq!(req.op_code, IDNVAL_GROUPOP_SETMASK);
1377        assert_eq!(req.group_mask, 0x1234);
1378        assert_eq!(&req.auth_code[..9], b"secret123");
1379        assert_eq!(&req.auth_code[9..], &[0u8; 3]);
1380    }
1381
1382    #[test]
1383    fn test_group_request_roundtrip() {
1384        let original = GroupRequest::set(0xFFFF);
1385        let mut buffer = Vec::new();
1386        original.write_to_bytes(&mut buffer).unwrap();
1387        assert_eq!(buffer.len(), 16);
1388
1389        let parsed = GroupRequest::read_from_bytes(&buffer[..]).unwrap();
1390        assert_eq!(parsed.struct_size, original.struct_size);
1391        assert_eq!(parsed.op_code, original.op_code);
1392        assert_eq!(parsed.group_mask, original.group_mask);
1393        assert_eq!(parsed.auth_code, original.auth_code);
1394    }
1395
1396    #[test]
1397    fn test_group_request_byte_layout() {
1398        let req = GroupRequest::set(0x1234);
1399        let mut buffer = Vec::new();
1400        req.write_to_bytes(&mut buffer).unwrap();
1401
1402        // Byte 0: struct_size = 16
1403        assert_eq!(buffer[0], 16);
1404        // Byte 1: op_code = 0x02 (SETMASK)
1405        assert_eq!(buffer[1], 0x02);
1406        // Bytes 2-3: group_mask = 0x1234 (big-endian)
1407        assert_eq!(buffer[2], 0x12);
1408        assert_eq!(buffer[3], 0x34);
1409        // Bytes 4-15: auth_code (all zeros)
1410        assert_eq!(&buffer[4..16], &[0u8; 12]);
1411    }
1412
1413    #[test]
1414    fn test_group_response_roundtrip() {
1415        let original = GroupResponse {
1416            struct_size: 4,
1417            op_code: IDNVAL_GROUPOP_SUCCESS,
1418            group_mask: 0x8000,
1419        };
1420        let mut buffer = Vec::new();
1421        original.write_to_bytes(&mut buffer).unwrap();
1422        assert_eq!(buffer.len(), 4);
1423
1424        let parsed = GroupResponse::read_from_bytes(&buffer[..]).unwrap();
1425        assert_eq!(parsed.struct_size, original.struct_size);
1426        assert_eq!(parsed.op_code, original.op_code);
1427        assert_eq!(parsed.group_mask, original.group_mask);
1428    }
1429
1430    #[test]
1431    fn test_group_response_byte_layout() {
1432        let resp = GroupResponse {
1433            struct_size: 4,
1434            op_code: IDNVAL_GROUPOP_SUCCESS,
1435            group_mask: 0xABCD,
1436        };
1437        let mut buffer = Vec::new();
1438        resp.write_to_bytes(&mut buffer).unwrap();
1439
1440        // Byte 0: struct_size = 4
1441        assert_eq!(buffer[0], 4);
1442        // Byte 1: op_code = 0x00 (SUCCESS)
1443        assert_eq!(buffer[1], 0x00);
1444        // Bytes 2-3: group_mask = 0xABCD (big-endian)
1445        assert_eq!(buffer[2], 0xAB);
1446        assert_eq!(buffer[3], 0xCD);
1447    }
1448
1449    #[test]
1450    fn test_group_response_is_success() {
1451        let success = GroupResponse {
1452            struct_size: 4,
1453            op_code: IDNVAL_GROUPOP_SUCCESS,
1454            group_mask: 0,
1455        };
1456        assert!(success.is_success());
1457
1458        let error_auth = GroupResponse {
1459            struct_size: 4,
1460            op_code: IDNVAL_GROUPOP_ERR_AUTH,
1461            group_mask: 0,
1462        };
1463        assert!(!error_auth.is_success());
1464
1465        let error_op = GroupResponse {
1466            struct_size: 4,
1467            op_code: IDNVAL_GROUPOP_ERR_OPERATION,
1468            group_mask: 0,
1469        };
1470        assert!(!error_op.is_success());
1471
1472        let error_req = GroupResponse {
1473            struct_size: 4,
1474            op_code: IDNVAL_GROUPOP_ERR_REQUEST,
1475            group_mask: 0,
1476        };
1477        assert!(!error_req.is_success());
1478    }
1479
1480    #[test]
1481    fn test_group_response_is_group_enabled() {
1482        let resp = GroupResponse {
1483            struct_size: 4,
1484            op_code: 0,
1485            group_mask: 0b0000_0000_0000_0101, // Groups 0 and 2 enabled
1486        };
1487        assert!(resp.is_group_enabled(0));
1488        assert!(!resp.is_group_enabled(1));
1489        assert!(resp.is_group_enabled(2));
1490        assert!(!resp.is_group_enabled(3));
1491        assert!(!resp.is_group_enabled(16)); // Out of range
1492    }
1493
1494    #[test]
1495    fn test_group_response_enabled_groups() {
1496        let resp = GroupResponse {
1497            struct_size: 4,
1498            op_code: 0,
1499            group_mask: 0b1000_0000_0000_0011, // Groups 0, 1, and 15 enabled
1500        };
1501        assert_eq!(resp.enabled_groups(), vec![0, 1, 15]);
1502    }
1503}