Skip to main content

stackforge_core/layer/zwave/
mod.rs

1//! Z-Wave wireless protocol layer implementation.
2//!
3//! Implements the Z-Wave home automation wireless protocol.
4//!
5//! ## Frame Format
6//!
7//! ```text
8//! Offset  Size  Field
9//! 0       4     Home ID (big-endian)
10//! 4       1     Source node ID
11//! 5       1     Frame control byte
12//!                 bit 7: routed
13//!                 bit 6: ack request
14//!                 bit 5: low power
15//!                 bit 4: speed modified
16//!                 bits 3-0: header type
17//! 6       1     Beam/Sequence byte
18//!                 bit 7: reserved
19//!                 bits 6-5: beam control
20//!                 bit 4: reserved
21//!                 bits 3-0: sequence number
22//! 7       1     Length (total frame length)
23//! 8       1     Destination node ID
24//! 9..N-1  var   Payload (cmd_class + cmd + data) -- only for Req frames
25//! N       1     CRC (XOR checksum)
26//! ```
27//!
28//! An ACK frame is exactly 10 bytes (no payload between dst and CRC).
29//! A REQ frame is 10 + `payload_len` bytes.
30
31pub mod builder;
32
33pub use builder::ZWaveBuilder;
34
35use crate::layer::field::{FieldError, FieldValue};
36use crate::layer::{Layer, LayerIndex, LayerKind};
37
38/// Minimum Z-Wave header length: homeId(4) + src(1) + frameCtrl(1) + beamSeqn(1) + length(1) + dst(1) + crc(1).
39pub const ZWAVE_MIN_HEADER_LEN: usize = 10;
40
41/// Fixed header size for an ACK frame (no payload).
42pub const ZWAVE_HEADER_LEN: usize = 10;
43
44/// Z-Wave command class constants.
45pub mod cmd_class {
46    pub const NO_OPERATION: u8 = 0x00;
47    pub const BASIC: u8 = 0x20;
48    pub const CONTROLLER_REPLICATION: u8 = 0x21;
49    pub const APPLICATION_STATUS: u8 = 0x22;
50    pub const ZIP_SERVICES: u8 = 0x23;
51    pub const ZIP_SERVER: u8 = 0x24;
52    pub const SWITCH_BINARY: u8 = 0x25;
53    pub const SWITCH_MULTILEVEL: u8 = 0x26;
54    pub const SWITCH_ALL: u8 = 0x27;
55    pub const SWITCH_TOGGLE_BINARY: u8 = 0x28;
56    pub const SWITCH_TOGGLE_MULTILEVEL: u8 = 0x29;
57    pub const CHIMNEY_FAN: u8 = 0x2A;
58    pub const SCENE_ACTIVATION: u8 = 0x2B;
59    pub const SCENE_ACTUATOR_CONF: u8 = 0x2C;
60    pub const SCENE_CONTROLLER_CONF: u8 = 0x2D;
61    pub const ZIP_CLIENT: u8 = 0x2E;
62    pub const ZIP_ADV_SERVICES: u8 = 0x2F;
63    pub const SENSOR_BINARY: u8 = 0x30;
64    pub const SENSOR_MULTILEVEL: u8 = 0x31;
65    pub const METER: u8 = 0x32;
66    pub const ZIP_ADV_SERVER: u8 = 0x33;
67    pub const ZIP_ADV_CLIENT: u8 = 0x34;
68    pub const METER_PULSE: u8 = 0x35;
69    pub const THERMOSTAT_HEATING: u8 = 0x38;
70    pub const METER_TBL_CONFIG: u8 = 0x3C;
71    pub const METER_TBL_MONITOR: u8 = 0x3D;
72    pub const METER_TBL_PUSH: u8 = 0x3E;
73    pub const THERMOSTAT_MODE: u8 = 0x40;
74    pub const THERMOSTAT_OPERATING_STATE: u8 = 0x42;
75    pub const THERMOSTAT_SETPOINT: u8 = 0x43;
76    pub const THERMOSTAT_FAN_MODE: u8 = 0x44;
77    pub const THERMOSTAT_FAN_STATE: u8 = 0x45;
78    pub const CLIMATE_CONTROL_SCHEDULE: u8 = 0x46;
79    pub const THERMOSTAT_SETBACK: u8 = 0x47;
80    pub const DOOR_LOCK_LOGGING: u8 = 0x4C;
81    pub const SCHEDULE_ENTRY_LOCK: u8 = 0x4E;
82    pub const BASIC_WINDOW_COVERING: u8 = 0x50;
83    pub const MTP_WINDOW_COVERING: u8 = 0x51;
84    pub const MULTI_CHANNEL_V2: u8 = 0x60;
85    pub const MULTI_INSTANCE: u8 = 0x61;
86    pub const DOOR_LOCK: u8 = 0x62;
87    pub const USER_CODE: u8 = 0x63;
88    pub const CONFIGURATION: u8 = 0x70;
89    pub const ALARM: u8 = 0x71;
90    pub const MANUFACTURER_SPECIFIC: u8 = 0x72;
91    pub const POWERLEVEL: u8 = 0x73;
92    pub const PROTECTION: u8 = 0x75;
93    pub const LOCK: u8 = 0x76;
94    pub const NODE_NAMING: u8 = 0x77;
95    pub const FIRMWARE_UPDATE_MD: u8 = 0x7A;
96    pub const GROUPING_NAME: u8 = 0x7B;
97    pub const REMOTE_ASSOCIATION_ACTIVATE: u8 = 0x7C;
98    pub const REMOTE_ASSOCIATION: u8 = 0x7D;
99    pub const BATTERY: u8 = 0x80;
100    pub const CLOCK: u8 = 0x81;
101    pub const HAIL: u8 = 0x82;
102    pub const WAKE_UP: u8 = 0x84;
103    pub const ASSOCIATION: u8 = 0x85;
104    pub const VERSION: u8 = 0x86;
105    pub const INDICATOR: u8 = 0x87;
106    pub const PROPRIETARY: u8 = 0x88;
107    pub const LANGUAGE: u8 = 0x89;
108    pub const TIME: u8 = 0x8A;
109    pub const TIME_PARAMETERS: u8 = 0x8B;
110    pub const GEOGRAPHIC_LOCATION: u8 = 0x8C;
111    pub const COMPOSITE: u8 = 0x8D;
112    pub const MULTI_INSTANCE_ASSOCIATION: u8 = 0x8E;
113    pub const MULTI_CMD: u8 = 0x8F;
114    pub const ENERGY_PRODUCTION: u8 = 0x90;
115    pub const MANUFACTURER_PROPRIETARY: u8 = 0x91;
116    pub const SCREEN_MD: u8 = 0x92;
117    pub const SCREEN_ATTRIBUTES: u8 = 0x93;
118    pub const SIMPLE_AV_CONTROL: u8 = 0x94;
119    pub const AV_CONTENT_DIRECTORY_MD: u8 = 0x95;
120    pub const AV_RENDERER_STATUS: u8 = 0x96;
121    pub const AV_CONTENT_SEARCH_MD: u8 = 0x97;
122    pub const SECURITY: u8 = 0x98;
123    pub const AV_TAGGING_MD: u8 = 0x99;
124    pub const SIP_CONFIGURATION: u8 = 0x9A;
125    pub const ASSOCIATION_COMMAND_CONFIGURATION: u8 = 0x9B;
126    pub const SENSOR_ALARM: u8 = 0x9C;
127    pub const SILENCE_ALARM: u8 = 0x9D;
128    pub const MARK: u8 = 0x9E;
129    pub const NON_INTEROPERABLE: u8 = 0xF0;
130}
131
132/// Return a human-readable name for a command class byte value.
133#[must_use]
134pub fn cmd_class_name(cc: u8) -> &'static str {
135    match cc {
136        cmd_class::NO_OPERATION => "NO_OPERATION",
137        cmd_class::BASIC => "BASIC",
138        cmd_class::CONTROLLER_REPLICATION => "CONTROLLER_REPLICATION",
139        cmd_class::APPLICATION_STATUS => "APPLICATION_STATUS",
140        cmd_class::ZIP_SERVICES => "ZIP_SERVICES",
141        cmd_class::ZIP_SERVER => "ZIP_SERVER",
142        cmd_class::SWITCH_BINARY => "SWITCH_BINARY",
143        cmd_class::SWITCH_MULTILEVEL => "SWITCH_MULTILEVEL",
144        cmd_class::SWITCH_ALL => "SWITCH_ALL",
145        cmd_class::SWITCH_TOGGLE_BINARY => "SWITCH_TOGGLE_BINARY",
146        cmd_class::SWITCH_TOGGLE_MULTILEVEL => "SWITCH_TOGGLE_MULTILEVEL",
147        cmd_class::CHIMNEY_FAN => "CHIMNEY_FAN",
148        cmd_class::SCENE_ACTIVATION => "SCENE_ACTIVATION",
149        cmd_class::SCENE_ACTUATOR_CONF => "SCENE_ACTUATOR_CONF",
150        cmd_class::SCENE_CONTROLLER_CONF => "SCENE_CONTROLLER_CONF",
151        cmd_class::ZIP_CLIENT => "ZIP_CLIENT",
152        cmd_class::ZIP_ADV_SERVICES => "ZIP_ADV_SERVICES",
153        cmd_class::SENSOR_BINARY => "SENSOR_BINARY",
154        cmd_class::SENSOR_MULTILEVEL => "SENSOR_MULTILEVEL",
155        cmd_class::METER => "METER",
156        cmd_class::ZIP_ADV_SERVER => "ZIP_ADV_SERVER",
157        cmd_class::ZIP_ADV_CLIENT => "ZIP_ADV_CLIENT",
158        cmd_class::METER_PULSE => "METER_PULSE",
159        cmd_class::THERMOSTAT_HEATING => "THERMOSTAT_HEATING",
160        cmd_class::METER_TBL_CONFIG => "METER_TBL_CONFIG",
161        cmd_class::METER_TBL_MONITOR => "METER_TBL_MONITOR",
162        cmd_class::METER_TBL_PUSH => "METER_TBL_PUSH",
163        cmd_class::THERMOSTAT_MODE => "THERMOSTAT_MODE",
164        cmd_class::THERMOSTAT_OPERATING_STATE => "THERMOSTAT_OPERATING_STATE",
165        cmd_class::THERMOSTAT_SETPOINT => "THERMOSTAT_SETPOINT",
166        cmd_class::THERMOSTAT_FAN_MODE => "THERMOSTAT_FAN_MODE",
167        cmd_class::THERMOSTAT_FAN_STATE => "THERMOSTAT_FAN_STATE",
168        cmd_class::CLIMATE_CONTROL_SCHEDULE => "CLIMATE_CONTROL_SCHEDULE",
169        cmd_class::THERMOSTAT_SETBACK => "THERMOSTAT_SETBACK",
170        cmd_class::DOOR_LOCK_LOGGING => "DOOR_LOCK_LOGGING",
171        cmd_class::SCHEDULE_ENTRY_LOCK => "SCHEDULE_ENTRY_LOCK",
172        cmd_class::BASIC_WINDOW_COVERING => "BASIC_WINDOW_COVERING",
173        cmd_class::MTP_WINDOW_COVERING => "MTP_WINDOW_COVERING",
174        cmd_class::MULTI_CHANNEL_V2 => "MULTI_CHANNEL_V2",
175        cmd_class::MULTI_INSTANCE => "MULTI_INSTANCE",
176        cmd_class::DOOR_LOCK => "DOOR_LOCK",
177        cmd_class::USER_CODE => "USER_CODE",
178        cmd_class::CONFIGURATION => "CONFIGURATION",
179        cmd_class::ALARM => "ALARM",
180        cmd_class::MANUFACTURER_SPECIFIC => "MANUFACTURER_SPECIFIC",
181        cmd_class::POWERLEVEL => "POWERLEVEL",
182        cmd_class::PROTECTION => "PROTECTION",
183        cmd_class::LOCK => "LOCK",
184        cmd_class::NODE_NAMING => "NODE_NAMING",
185        cmd_class::FIRMWARE_UPDATE_MD => "FIRMWARE_UPDATE_MD",
186        cmd_class::GROUPING_NAME => "GROUPING_NAME",
187        cmd_class::REMOTE_ASSOCIATION_ACTIVATE => "REMOTE_ASSOCIATION_ACTIVATE",
188        cmd_class::REMOTE_ASSOCIATION => "REMOTE_ASSOCIATION",
189        cmd_class::BATTERY => "BATTERY",
190        cmd_class::CLOCK => "CLOCK",
191        cmd_class::HAIL => "HAIL",
192        cmd_class::WAKE_UP => "WAKE_UP",
193        cmd_class::ASSOCIATION => "ASSOCIATION",
194        cmd_class::VERSION => "VERSION",
195        cmd_class::INDICATOR => "INDICATOR",
196        cmd_class::PROPRIETARY => "PROPRIETARY",
197        cmd_class::LANGUAGE => "LANGUAGE",
198        cmd_class::TIME => "TIME",
199        cmd_class::TIME_PARAMETERS => "TIME_PARAMETERS",
200        cmd_class::GEOGRAPHIC_LOCATION => "GEOGRAPHIC_LOCATION",
201        cmd_class::COMPOSITE => "COMPOSITE",
202        cmd_class::MULTI_INSTANCE_ASSOCIATION => "MULTI_INSTANCE_ASSOCIATION",
203        cmd_class::MULTI_CMD => "MULTI_CMD",
204        cmd_class::ENERGY_PRODUCTION => "ENERGY_PRODUCTION",
205        cmd_class::MANUFACTURER_PROPRIETARY => "MANUFACTURER_PROPRIETARY",
206        cmd_class::SCREEN_MD => "SCREEN_MD",
207        cmd_class::SCREEN_ATTRIBUTES => "SCREEN_ATTRIBUTES",
208        cmd_class::SIMPLE_AV_CONTROL => "SIMPLE_AV_CONTROL",
209        cmd_class::AV_CONTENT_DIRECTORY_MD => "AV_CONTENT_DIRECTORY_MD",
210        cmd_class::AV_RENDERER_STATUS => "AV_RENDERER_STATUS",
211        cmd_class::AV_CONTENT_SEARCH_MD => "AV_CONTENT_SEARCH_MD",
212        cmd_class::SECURITY => "SECURITY",
213        cmd_class::AV_TAGGING_MD => "AV_TAGGING_MD",
214        cmd_class::SIP_CONFIGURATION => "SIP_CONFIGURATION",
215        cmd_class::ASSOCIATION_COMMAND_CONFIGURATION => "ASSOCIATION_COMMAND_CONFIGURATION",
216        cmd_class::SENSOR_ALARM => "SENSOR_ALARM",
217        cmd_class::SILENCE_ALARM => "SILENCE_ALARM",
218        cmd_class::MARK => "MARK",
219        cmd_class::NON_INTEROPERABLE => "NON_INTEROPERABLE",
220        _ => "UNKNOWN",
221    }
222}
223
224/// Field names exported for Python/generic access.
225pub static ZWAVE_FIELD_NAMES: &[&str] = &[
226    "home_id",
227    "src",
228    "dst",
229    "routed",
230    "ackreq",
231    "lowpower",
232    "speedmodified",
233    "headertype",
234    "beam_control",
235    "seqn",
236    "length",
237    "cmd_class",
238    "cmd",
239    "crc",
240];
241
242/// Compute the Z-Wave CRC: XOR all bytes starting from an initial value of 0xFF.
243#[must_use]
244pub fn zwave_crc(data: &[u8]) -> u8 {
245    data.iter().fold(0xFFu8, |acc, &b| acc ^ b)
246}
247
248/// Check if a buffer looks like a valid Z-Wave frame.
249///
250/// Z-Wave is a wireless protocol (not carried over TCP/UDP), so this is used
251/// for detecting Z-Wave frames in raw captures.
252#[must_use]
253pub fn is_zwave_frame(buf: &[u8]) -> bool {
254    if buf.len() < 10 {
255        return false;
256    }
257    let length = buf[7] as usize;
258    length >= 10 && length <= buf.len()
259}
260
261/// Z-Wave layer -- a zero-copy view into a packet buffer.
262#[derive(Debug, Clone)]
263pub struct ZWaveLayer {
264    pub index: LayerIndex,
265}
266
267impl ZWaveLayer {
268    /// Create a new Z-Wave layer from a layer index.
269    #[must_use]
270    pub fn new(index: LayerIndex) -> Self {
271        Self { index }
272    }
273
274    /// Create a Z-Wave layer starting at offset 0 (for standalone parsing).
275    #[must_use]
276    pub fn at_start(end: usize) -> Self {
277        Self {
278            index: LayerIndex::new(LayerKind::ZWave, 0, end),
279        }
280    }
281
282    /// Return a reference to the slice of the buffer corresponding to this layer.
283    fn slice<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
284        self.index.slice(buf)
285    }
286
287    // ========================================================================
288    // Field accessors (fixed offsets from index.start)
289    // ========================================================================
290
291    /// Read the 4-byte Home ID (big-endian u32) at offset 0.
292    pub fn home_id(&self, buf: &[u8]) -> Result<u32, FieldError> {
293        let s = self.slice(buf);
294        if s.len() < 4 {
295            return Err(FieldError::BufferTooShort {
296                offset: self.index.start,
297                need: 4,
298                have: s.len(),
299            });
300        }
301        Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
302    }
303
304    /// Set the Home ID (big-endian u32) at offset 0.
305    pub fn set_home_id(&self, buf: &mut [u8], value: u32) -> Result<(), FieldError> {
306        let off = self.index.start;
307        if buf.len() < off + 4 {
308            return Err(FieldError::BufferTooShort {
309                offset: off,
310                need: 4,
311                have: buf.len().saturating_sub(off),
312            });
313        }
314        buf[off..off + 4].copy_from_slice(&value.to_be_bytes());
315        Ok(())
316    }
317
318    /// Read the source node ID at offset 4.
319    pub fn src(&self, buf: &[u8]) -> Result<u8, FieldError> {
320        let s = self.slice(buf);
321        if s.len() < 5 {
322            return Err(FieldError::BufferTooShort {
323                offset: self.index.start + 4,
324                need: 1,
325                have: s.len().saturating_sub(4),
326            });
327        }
328        Ok(s[4])
329    }
330
331    /// Set the source node ID at offset 4.
332    pub fn set_src(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
333        let off = self.index.start + 4;
334        if buf.len() <= off {
335            return Err(FieldError::BufferTooShort {
336                offset: off,
337                need: 1,
338                have: buf.len().saturating_sub(off),
339            });
340        }
341        buf[off] = value;
342        Ok(())
343    }
344
345    /// Read the raw frame control byte at offset 5.
346    pub fn frame_ctrl(&self, buf: &[u8]) -> Result<u8, FieldError> {
347        let s = self.slice(buf);
348        if s.len() < 6 {
349            return Err(FieldError::BufferTooShort {
350                offset: self.index.start + 5,
351                need: 1,
352                have: s.len().saturating_sub(5),
353            });
354        }
355        Ok(s[5])
356    }
357
358    /// Set the raw frame control byte at offset 5.
359    pub fn set_frame_ctrl(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
360        let off = self.index.start + 5;
361        if buf.len() <= off {
362            return Err(FieldError::BufferTooShort {
363                offset: off,
364                need: 1,
365                have: buf.len().saturating_sub(off),
366            });
367        }
368        buf[off] = value;
369        Ok(())
370    }
371
372    /// Get the routed flag (bit 7 of frame control).
373    pub fn routed(&self, buf: &[u8]) -> Result<bool, FieldError> {
374        let fc = self.frame_ctrl(buf)?;
375        Ok((fc >> 7) & 0x01 == 1)
376    }
377
378    /// Set the routed flag (bit 7 of frame control).
379    pub fn set_routed(&self, buf: &mut [u8], value: bool) -> Result<(), FieldError> {
380        let fc = self.frame_ctrl(buf)?;
381        let fc = if value { fc | 0x80 } else { fc & !0x80 };
382        self.set_frame_ctrl(buf, fc)
383    }
384
385    /// Get the ack request flag (bit 6 of frame control).
386    pub fn ackreq(&self, buf: &[u8]) -> Result<bool, FieldError> {
387        let fc = self.frame_ctrl(buf)?;
388        Ok((fc >> 6) & 0x01 == 1)
389    }
390
391    /// Set the ack request flag (bit 6 of frame control).
392    pub fn set_ackreq(&self, buf: &mut [u8], value: bool) -> Result<(), FieldError> {
393        let fc = self.frame_ctrl(buf)?;
394        let fc = if value { fc | 0x40 } else { fc & !0x40 };
395        self.set_frame_ctrl(buf, fc)
396    }
397
398    /// Get the low power flag (bit 5 of frame control).
399    pub fn lowpower(&self, buf: &[u8]) -> Result<bool, FieldError> {
400        let fc = self.frame_ctrl(buf)?;
401        Ok((fc >> 5) & 0x01 == 1)
402    }
403
404    /// Set the low power flag (bit 5 of frame control).
405    pub fn set_lowpower(&self, buf: &mut [u8], value: bool) -> Result<(), FieldError> {
406        let fc = self.frame_ctrl(buf)?;
407        let fc = if value { fc | 0x20 } else { fc & !0x20 };
408        self.set_frame_ctrl(buf, fc)
409    }
410
411    /// Get the speed modified flag (bit 4 of frame control).
412    pub fn speedmodified(&self, buf: &[u8]) -> Result<bool, FieldError> {
413        let fc = self.frame_ctrl(buf)?;
414        Ok((fc >> 4) & 0x01 == 1)
415    }
416
417    /// Set the speed modified flag (bit 4 of frame control).
418    pub fn set_speedmodified(&self, buf: &mut [u8], value: bool) -> Result<(), FieldError> {
419        let fc = self.frame_ctrl(buf)?;
420        let fc = if value { fc | 0x10 } else { fc & !0x10 };
421        self.set_frame_ctrl(buf, fc)
422    }
423
424    /// Get the header type (bits 3-0 of frame control).
425    pub fn headertype(&self, buf: &[u8]) -> Result<u8, FieldError> {
426        let fc = self.frame_ctrl(buf)?;
427        Ok(fc & 0x0F)
428    }
429
430    /// Set the header type (bits 3-0 of frame control).
431    pub fn set_headertype(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
432        let fc = self.frame_ctrl(buf)?;
433        let fc = (fc & 0xF0) | (value & 0x0F);
434        self.set_frame_ctrl(buf, fc)
435    }
436
437    /// Read the raw beam/sequence byte at offset 6.
438    pub fn beam_seqn(&self, buf: &[u8]) -> Result<u8, FieldError> {
439        let s = self.slice(buf);
440        if s.len() < 7 {
441            return Err(FieldError::BufferTooShort {
442                offset: self.index.start + 6,
443                need: 1,
444                have: s.len().saturating_sub(6),
445            });
446        }
447        Ok(s[6])
448    }
449
450    /// Set the raw beam/sequence byte at offset 6.
451    pub fn set_beam_seqn(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
452        let off = self.index.start + 6;
453        if buf.len() <= off {
454            return Err(FieldError::BufferTooShort {
455                offset: off,
456                need: 1,
457                have: buf.len().saturating_sub(off),
458            });
459        }
460        buf[off] = value;
461        Ok(())
462    }
463
464    /// Get the beam control field (bits 6-5 of beam/sequence byte).
465    pub fn beam_control(&self, buf: &[u8]) -> Result<u8, FieldError> {
466        let bs = self.beam_seqn(buf)?;
467        Ok((bs >> 5) & 0x03)
468    }
469
470    /// Set the beam control field (bits 6-5 of beam/sequence byte).
471    pub fn set_beam_control(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
472        let bs = self.beam_seqn(buf)?;
473        let bs = (bs & !0x60) | ((value & 0x03) << 5);
474        self.set_beam_seqn(buf, bs)
475    }
476
477    /// Get the sequence number (bits 3-0 of beam/sequence byte).
478    pub fn seqn(&self, buf: &[u8]) -> Result<u8, FieldError> {
479        let bs = self.beam_seqn(buf)?;
480        Ok(bs & 0x0F)
481    }
482
483    /// Set the sequence number (bits 3-0 of beam/sequence byte).
484    pub fn set_seqn(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
485        let bs = self.beam_seqn(buf)?;
486        let bs = (bs & 0xF0) | (value & 0x0F);
487        self.set_beam_seqn(buf, bs)
488    }
489
490    /// Read the length field at offset 7.
491    pub fn length(&self, buf: &[u8]) -> Result<u8, FieldError> {
492        let s = self.slice(buf);
493        if s.len() < 8 {
494            return Err(FieldError::BufferTooShort {
495                offset: self.index.start + 7,
496                need: 1,
497                have: s.len().saturating_sub(7),
498            });
499        }
500        Ok(s[7])
501    }
502
503    /// Set the length field at offset 7.
504    pub fn set_length(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
505        let off = self.index.start + 7;
506        if buf.len() <= off {
507            return Err(FieldError::BufferTooShort {
508                offset: off,
509                need: 1,
510                have: buf.len().saturating_sub(off),
511            });
512        }
513        buf[off] = value;
514        Ok(())
515    }
516
517    /// Read the destination node ID at offset 8.
518    pub fn dst(&self, buf: &[u8]) -> Result<u8, FieldError> {
519        let s = self.slice(buf);
520        if s.len() < 9 {
521            return Err(FieldError::BufferTooShort {
522                offset: self.index.start + 8,
523                need: 1,
524                have: s.len().saturating_sub(8),
525            });
526        }
527        Ok(s[8])
528    }
529
530    /// Set the destination node ID at offset 8.
531    pub fn set_dst(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
532        let off = self.index.start + 8;
533        if buf.len() <= off {
534            return Err(FieldError::BufferTooShort {
535                offset: off,
536                need: 1,
537                have: buf.len().saturating_sub(off),
538            });
539        }
540        buf[off] = value;
541        Ok(())
542    }
543
544    /// Read the CRC byte. For ACK frames it is at offset 9.
545    /// For REQ frames it is the last byte of the frame.
546    pub fn crc(&self, buf: &[u8]) -> Result<u8, FieldError> {
547        let s = self.slice(buf);
548        if s.len() < ZWAVE_MIN_HEADER_LEN {
549            return Err(FieldError::BufferTooShort {
550                offset: self.index.start + 9,
551                need: 1,
552                have: s.len().saturating_sub(9),
553            });
554        }
555        // CRC is always the last byte of the frame
556        Ok(s[s.len() - 1])
557    }
558
559    /// Set the CRC byte (last byte of the frame).
560    pub fn set_crc(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
561        let end = self.index.end;
562        if end == 0 || buf.len() < end {
563            return Err(FieldError::BufferTooShort {
564                offset: end.saturating_sub(1),
565                need: 1,
566                have: 0,
567            });
568        }
569        buf[end - 1] = value;
570        Ok(())
571    }
572
573    // ========================================================================
574    // Payload field accessors (only present in Req frames)
575    // ========================================================================
576
577    /// Returns true if this frame is an ACK (no payload -- total length is 10).
578    #[must_use]
579    pub fn is_ack(&self, buf: &[u8]) -> bool {
580        let s = self.slice(buf);
581        s.len() <= ZWAVE_MIN_HEADER_LEN
582    }
583
584    /// Read the command class byte at offset 9 (only valid for Req frames).
585    /// In the wire format, the payload starts at offset 9 (between dst and crc).
586    /// Layout for Req: [homeId(4), src(1), fc(1), bs(1), len(1), dst(1), cmdClass(1), cmd(1), ...data, crc(1)]
587    pub fn cmd_class(&self, buf: &[u8]) -> Result<u8, FieldError> {
588        let s = self.slice(buf);
589        if s.len() <= ZWAVE_MIN_HEADER_LEN {
590            return Err(FieldError::InvalidValue(
591                "ACK frame has no cmd_class field".into(),
592            ));
593        }
594        // Payload starts at offset 9: cmdClass is at index 9
595        Ok(s[9])
596    }
597
598    /// Set the command class byte at offset 9.
599    pub fn set_cmd_class(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
600        let off = self.index.start + 9;
601        if self.is_ack(buf) {
602            return Err(FieldError::InvalidValue(
603                "ACK frame has no cmd_class field".into(),
604            ));
605        }
606        if buf.len() <= off {
607            return Err(FieldError::BufferTooShort {
608                offset: off,
609                need: 1,
610                have: buf.len().saturating_sub(off),
611            });
612        }
613        buf[off] = value;
614        Ok(())
615    }
616
617    /// Read the command byte at offset 10 (only valid for Req frames with sufficient payload).
618    pub fn cmd(&self, buf: &[u8]) -> Result<u8, FieldError> {
619        let s = self.slice(buf);
620        if s.len() <= ZWAVE_MIN_HEADER_LEN + 1 {
621            return Err(FieldError::InvalidValue("frame has no cmd field".into()));
622        }
623        Ok(s[10])
624    }
625
626    /// Set the command byte at offset 10.
627    pub fn set_cmd(&self, buf: &mut [u8], value: u8) -> Result<(), FieldError> {
628        let off = self.index.start + 10;
629        let s = self.slice(buf);
630        if s.len() <= ZWAVE_MIN_HEADER_LEN + 1 {
631            return Err(FieldError::InvalidValue("frame has no cmd field".into()));
632        }
633        if buf.len() <= off {
634            return Err(FieldError::BufferTooShort {
635                offset: off,
636                need: 1,
637                have: buf.len().saturating_sub(off),
638            });
639        }
640        buf[off] = value;
641        Ok(())
642    }
643
644    /// Read the command data bytes (everything after cmd and before CRC).
645    pub fn cmd_data<'a>(&self, buf: &'a [u8]) -> Result<&'a [u8], FieldError> {
646        let s = self.slice(buf);
647        if s.len() <= ZWAVE_MIN_HEADER_LEN + 2 {
648            // No data after cmd_class + cmd (or it's an ACK)
649            return Ok(&[]);
650        }
651        // Data is from offset 11 to len-1 (last byte is CRC)
652        Ok(&s[11..s.len() - 1])
653    }
654
655    // ========================================================================
656    // CRC verification
657    // ========================================================================
658
659    /// Verify the CRC of this frame. Computes XOR of all bytes except the last
660    /// (starting from 0xFF) and compares with the stored CRC.
661    #[must_use]
662    pub fn verify_crc(&self, buf: &[u8]) -> bool {
663        let s = self.slice(buf);
664        if s.len() < ZWAVE_MIN_HEADER_LEN {
665            return false;
666        }
667        let computed = zwave_crc(&s[..s.len() - 1]);
668        computed == s[s.len() - 1]
669    }
670
671    // ========================================================================
672    // Summary / display
673    // ========================================================================
674
675    /// Generate a one-line summary of this Z-Wave frame.
676    #[must_use]
677    pub fn summary(&self, buf: &[u8]) -> String {
678        let s = self.slice(buf);
679        if s.len() < ZWAVE_MIN_HEADER_LEN {
680            return "Z-Wave".to_string();
681        }
682
683        let home = self.home_id(buf).unwrap_or(0);
684        let src_id = self.src(buf).unwrap_or(0);
685        let dst_id = self.dst(buf).unwrap_or(0);
686
687        if self.is_ack(buf) {
688            format!("Z-Wave ACK {home:#010x} {src_id}->{dst_id}")
689        } else {
690            let cc = self
691                .cmd_class(buf)
692                .map_or_else(|_| "?".to_string(), |c| cmd_class_name(c).to_string());
693            format!("Z-Wave {home:#010x} {src_id}->{dst_id}  {cc}")
694        }
695    }
696
697    /// Compute the header length. For ACK frames this is the full 10 bytes.
698    /// For REQ frames this is the entire frame (header + payload + CRC).
699    fn compute_header_len(&self, buf: &[u8]) -> usize {
700        let s = self.slice(buf);
701        // The entire Z-Wave frame is this layer (no sub-layers to chain)
702        s.len()
703    }
704
705    // ========================================================================
706    // Field access API
707    // ========================================================================
708
709    /// Get the field names for this layer.
710    #[must_use]
711    pub fn field_names() -> &'static [&'static str] {
712        ZWAVE_FIELD_NAMES
713    }
714
715    /// Get a field value by name.
716    pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
717        match name {
718            "home_id" => Some(self.home_id(buf).map(FieldValue::U32)),
719            "src" => Some(self.src(buf).map(FieldValue::U8)),
720            "dst" => Some(self.dst(buf).map(FieldValue::U8)),
721            "routed" => Some(self.routed(buf).map(FieldValue::Bool)),
722            "ackreq" => Some(self.ackreq(buf).map(FieldValue::Bool)),
723            "lowpower" => Some(self.lowpower(buf).map(FieldValue::Bool)),
724            "speedmodified" => Some(self.speedmodified(buf).map(FieldValue::Bool)),
725            "headertype" => Some(self.headertype(buf).map(FieldValue::U8)),
726            "beam_control" => Some(self.beam_control(buf).map(FieldValue::U8)),
727            "seqn" => Some(self.seqn(buf).map(FieldValue::U8)),
728            "length" => Some(self.length(buf).map(FieldValue::U8)),
729            "cmd_class" => {
730                if self.is_ack(buf) {
731                    Some(Ok(FieldValue::U8(0)))
732                } else {
733                    Some(self.cmd_class(buf).map(FieldValue::U8))
734                }
735            },
736            "cmd" => {
737                if self.is_ack(buf) {
738                    Some(Ok(FieldValue::U8(0)))
739                } else {
740                    Some(self.cmd(buf).map(FieldValue::U8))
741                }
742            },
743            "crc" => Some(self.crc(buf).map(FieldValue::U8)),
744            _ => None,
745        }
746    }
747
748    /// Set a field value by name.
749    pub fn set_field(
750        &self,
751        buf: &mut [u8],
752        name: &str,
753        value: FieldValue,
754    ) -> Option<Result<(), FieldError>> {
755        match name {
756            "home_id" => {
757                if let FieldValue::U32(v) = value {
758                    Some(self.set_home_id(buf, v))
759                } else {
760                    Some(Err(FieldError::InvalidValue(format!(
761                        "home_id: expected U32, got {value:?}"
762                    ))))
763                }
764            },
765            "src" => {
766                if let FieldValue::U8(v) = value {
767                    Some(self.set_src(buf, v))
768                } else {
769                    Some(Err(FieldError::InvalidValue(format!(
770                        "src: expected U8, got {value:?}"
771                    ))))
772                }
773            },
774            "dst" => {
775                if let FieldValue::U8(v) = value {
776                    Some(self.set_dst(buf, v))
777                } else {
778                    Some(Err(FieldError::InvalidValue(format!(
779                        "dst: expected U8, got {value:?}"
780                    ))))
781                }
782            },
783            "routed" => {
784                if let FieldValue::Bool(v) = value {
785                    Some(self.set_routed(buf, v))
786                } else {
787                    Some(Err(FieldError::InvalidValue(format!(
788                        "routed: expected Bool, got {value:?}"
789                    ))))
790                }
791            },
792            "ackreq" => {
793                if let FieldValue::Bool(v) = value {
794                    Some(self.set_ackreq(buf, v))
795                } else {
796                    Some(Err(FieldError::InvalidValue(format!(
797                        "ackreq: expected Bool, got {value:?}"
798                    ))))
799                }
800            },
801            "lowpower" => {
802                if let FieldValue::Bool(v) = value {
803                    Some(self.set_lowpower(buf, v))
804                } else {
805                    Some(Err(FieldError::InvalidValue(format!(
806                        "lowpower: expected Bool, got {value:?}"
807                    ))))
808                }
809            },
810            "speedmodified" => {
811                if let FieldValue::Bool(v) = value {
812                    Some(self.set_speedmodified(buf, v))
813                } else {
814                    Some(Err(FieldError::InvalidValue(format!(
815                        "speedmodified: expected Bool, got {value:?}"
816                    ))))
817                }
818            },
819            "headertype" => {
820                if let FieldValue::U8(v) = value {
821                    Some(self.set_headertype(buf, v))
822                } else {
823                    Some(Err(FieldError::InvalidValue(format!(
824                        "headertype: expected U8, got {value:?}"
825                    ))))
826                }
827            },
828            "beam_control" => {
829                if let FieldValue::U8(v) = value {
830                    Some(self.set_beam_control(buf, v))
831                } else {
832                    Some(Err(FieldError::InvalidValue(format!(
833                        "beam_control: expected U8, got {value:?}"
834                    ))))
835                }
836            },
837            "seqn" => {
838                if let FieldValue::U8(v) = value {
839                    Some(self.set_seqn(buf, v))
840                } else {
841                    Some(Err(FieldError::InvalidValue(format!(
842                        "seqn: expected U8, got {value:?}"
843                    ))))
844                }
845            },
846            "length" => {
847                if let FieldValue::U8(v) = value {
848                    Some(self.set_length(buf, v))
849                } else {
850                    Some(Err(FieldError::InvalidValue(format!(
851                        "length: expected U8, got {value:?}"
852                    ))))
853                }
854            },
855            "cmd_class" => {
856                if let FieldValue::U8(v) = value {
857                    Some(self.set_cmd_class(buf, v))
858                } else {
859                    Some(Err(FieldError::InvalidValue(format!(
860                        "cmd_class: expected U8, got {value:?}"
861                    ))))
862                }
863            },
864            "cmd" => {
865                if let FieldValue::U8(v) = value {
866                    Some(self.set_cmd(buf, v))
867                } else {
868                    Some(Err(FieldError::InvalidValue(format!(
869                        "cmd: expected U8, got {value:?}"
870                    ))))
871                }
872            },
873            "crc" => {
874                if let FieldValue::U8(v) = value {
875                    Some(self.set_crc(buf, v))
876                } else {
877                    Some(Err(FieldError::InvalidValue(format!(
878                        "crc: expected U8, got {value:?}"
879                    ))))
880                }
881            },
882            _ => None,
883        }
884    }
885}
886
887impl Layer for ZWaveLayer {
888    fn kind(&self) -> LayerKind {
889        LayerKind::ZWave
890    }
891
892    fn summary(&self, data: &[u8]) -> String {
893        self.summary(data)
894    }
895
896    fn header_len(&self, data: &[u8]) -> usize {
897        self.compute_header_len(data)
898    }
899
900    fn field_names(&self) -> &'static [&'static str] {
901        ZWAVE_FIELD_NAMES
902    }
903
904    fn hashret(&self, data: &[u8]) -> Vec<u8> {
905        // Hash on home ID for packet matching
906        self.home_id(data)
907            .map(|h| h.to_be_bytes().to_vec())
908            .unwrap_or_default()
909    }
910
911    fn answers(&self, data: &[u8], other: &Self, other_data: &[u8]) -> bool {
912        // A reply swaps src and dst within the same home ID
913        let same_home = self.home_id(data) == other.home_id(other_data);
914        let swapped =
915            self.src(data) == other.dst(other_data) && self.dst(data) == other.src(other_data);
916        same_home && swapped
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    /// Build an ACK frame (10 bytes): homeId(4) + src(1) + fc(1) + bs(1) + len(1) + dst(1) + crc(1)
925    fn ack_frame(home_id: u32, src: u8, dst: u8) -> Vec<u8> {
926        let mut buf = Vec::with_capacity(ZWAVE_MIN_HEADER_LEN);
927        buf.extend_from_slice(&home_id.to_be_bytes()); // 0-3: home_id
928        buf.push(src); // 4: src
929        buf.push(0x40); // 5: frame_ctrl (ackreq=1)
930        buf.push(0x01); // 6: beam_seqn (seqn=1)
931        buf.push(0x0A); // 7: length=10
932        buf.push(dst); // 8: dst
933        let crc = zwave_crc(&buf);
934        buf.push(crc); // 9: CRC
935        buf
936    }
937
938    /// Build a REQ frame with a command class.
939    fn req_frame(home_id: u32, src: u8, dst: u8, cmd_class: u8, cmd: u8, data: &[u8]) -> Vec<u8> {
940        let payload_len = 2 + data.len(); // cmd_class + cmd + data
941        let total_len = ZWAVE_MIN_HEADER_LEN + payload_len;
942        let mut buf = Vec::with_capacity(total_len);
943        buf.extend_from_slice(&home_id.to_be_bytes()); // 0-3: home_id
944        buf.push(src); // 4: src
945        buf.push(0x40); // 5: frame_ctrl (ackreq=1)
946        buf.push(0x01); // 6: beam_seqn (seqn=1)
947        buf.push(total_len as u8); // 7: length
948        buf.push(dst); // 8: dst
949        buf.push(cmd_class); // 9: cmd_class
950        buf.push(cmd); // 10: cmd
951        buf.extend_from_slice(data); // 11..: cmd_data
952        let crc = zwave_crc(&buf);
953        buf.push(crc); // last: CRC
954        buf
955    }
956
957    #[test]
958    fn test_crc_computation() {
959        // XOR of all bytes starting from 0xFF
960        assert_eq!(zwave_crc(&[]), 0xFF);
961        assert_eq!(zwave_crc(&[0xFF]), 0x00);
962        assert_eq!(zwave_crc(&[0x01, 0x02]), 0xFF ^ 0x01 ^ 0x02);
963        assert_eq!(zwave_crc(&[0xAA, 0x55]), 0xFF ^ 0xAA ^ 0x55);
964    }
965
966    #[test]
967    fn test_parse_ack_frame() {
968        let data = ack_frame(0x0161f498, 1, 2);
969        assert_eq!(data.len(), 10);
970
971        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
972        let zw = ZWaveLayer::new(idx);
973
974        assert_eq!(zw.home_id(&data).unwrap(), 0x0161f498);
975        assert_eq!(zw.src(&data).unwrap(), 1);
976        assert_eq!(zw.dst(&data).unwrap(), 2);
977        assert_eq!(zw.length(&data).unwrap(), 10);
978        assert!(zw.is_ack(&data));
979        assert!(zw.verify_crc(&data));
980    }
981
982    #[test]
983    fn test_parse_req_switch_binary() {
984        let data = req_frame(0x0161f498, 1, 2, cmd_class::SWITCH_BINARY, 0x01, &[0xFF]);
985        // Total: 10 + 3 (cmd_class + cmd + one data byte) = 13
986        assert_eq!(data.len(), 13);
987
988        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
989        let zw = ZWaveLayer::new(idx);
990
991        assert!(!zw.is_ack(&data));
992        assert_eq!(zw.cmd_class(&data).unwrap(), cmd_class::SWITCH_BINARY);
993        assert_eq!(zw.cmd(&data).unwrap(), 0x01);
994        assert_eq!(zw.cmd_data(&data).unwrap(), &[0xFF]);
995        assert!(zw.verify_crc(&data));
996    }
997
998    #[test]
999    fn test_parse_req_sensor_binary() {
1000        let data = req_frame(0xDEADBEEF, 3, 5, cmd_class::SENSOR_BINARY, 0x03, &[0xFF]);
1001        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1002        let zw = ZWaveLayer::new(idx);
1003
1004        assert!(!zw.is_ack(&data));
1005        assert_eq!(zw.home_id(&data).unwrap(), 0xDEADBEEF);
1006        assert_eq!(zw.cmd_class(&data).unwrap(), cmd_class::SENSOR_BINARY);
1007        assert_eq!(cmd_class_name(cmd_class::SENSOR_BINARY), "SENSOR_BINARY");
1008        assert!(zw.verify_crc(&data));
1009    }
1010
1011    #[test]
1012    fn test_frame_ctrl_bitfields() {
1013        let mut data = ack_frame(0x0161f498, 1, 2);
1014        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1015        let zw = ZWaveLayer::new(idx);
1016
1017        // Default: ackreq=true, routed=false
1018        assert!(zw.ackreq(&data).unwrap());
1019        assert!(!zw.routed(&data).unwrap());
1020        assert!(!zw.lowpower(&data).unwrap());
1021        assert!(!zw.speedmodified(&data).unwrap());
1022        assert_eq!(zw.headertype(&data).unwrap(), 0);
1023
1024        // Set routed
1025        zw.set_routed(&mut data, true).unwrap();
1026        assert!(zw.routed(&data).unwrap());
1027        assert!(zw.ackreq(&data).unwrap()); // preserved
1028
1029        // Set headertype
1030        zw.set_headertype(&mut data, 0x05).unwrap();
1031        assert_eq!(zw.headertype(&data).unwrap(), 0x05);
1032        assert!(zw.routed(&data).unwrap()); // preserved
1033    }
1034
1035    #[test]
1036    fn test_beam_seqn_bitfields() {
1037        let mut data = ack_frame(0x0161f498, 1, 2);
1038        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1039        let zw = ZWaveLayer::new(idx);
1040
1041        // Default: beam_control=0, seqn=1
1042        assert_eq!(zw.beam_control(&data).unwrap(), 0);
1043        assert_eq!(zw.seqn(&data).unwrap(), 1);
1044
1045        // Set beam control
1046        zw.set_beam_control(&mut data, 2).unwrap();
1047        assert_eq!(zw.beam_control(&data).unwrap(), 2);
1048        assert_eq!(zw.seqn(&data).unwrap(), 1); // preserved
1049
1050        // Set sequence number
1051        zw.set_seqn(&mut data, 0x0F).unwrap();
1052        assert_eq!(zw.seqn(&data).unwrap(), 0x0F);
1053        assert_eq!(zw.beam_control(&data).unwrap(), 2); // preserved
1054    }
1055
1056    #[test]
1057    fn test_cmd_class_name_helper() {
1058        assert_eq!(cmd_class_name(cmd_class::NO_OPERATION), "NO_OPERATION");
1059        assert_eq!(cmd_class_name(cmd_class::SWITCH_BINARY), "SWITCH_BINARY");
1060        assert_eq!(cmd_class_name(cmd_class::SENSOR_BINARY), "SENSOR_BINARY");
1061        assert_eq!(cmd_class_name(cmd_class::BATTERY), "BATTERY");
1062        assert_eq!(cmd_class_name(cmd_class::SECURITY), "SECURITY");
1063        assert_eq!(
1064            cmd_class_name(cmd_class::NON_INTEROPERABLE),
1065            "NON_INTEROPERABLE"
1066        );
1067        assert_eq!(cmd_class_name(0x99), "AV_TAGGING_MD");
1068        assert_eq!(cmd_class_name(0xBB), "UNKNOWN");
1069    }
1070
1071    #[test]
1072    fn test_is_ack_detection() {
1073        let ack = ack_frame(0x0161f498, 1, 2);
1074        let idx = LayerIndex::new(LayerKind::ZWave, 0, ack.len());
1075        let zw = ZWaveLayer::new(idx);
1076        assert!(zw.is_ack(&ack));
1077
1078        let req = req_frame(0x0161f498, 1, 2, cmd_class::BASIC, 0x01, &[]);
1079        let idx = LayerIndex::new(LayerKind::ZWave, 0, req.len());
1080        let zw = ZWaveLayer::new(idx);
1081        assert!(!zw.is_ack(&req));
1082    }
1083
1084    #[test]
1085    fn test_verify_crc_valid() {
1086        let data = req_frame(0x0161f498, 1, 2, cmd_class::SWITCH_BINARY, 0x01, &[0xFF]);
1087        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1088        let zw = ZWaveLayer::new(idx);
1089        assert!(zw.verify_crc(&data));
1090    }
1091
1092    #[test]
1093    fn test_verify_crc_invalid() {
1094        let mut data = req_frame(0x0161f498, 1, 2, cmd_class::SWITCH_BINARY, 0x01, &[0xFF]);
1095        // Corrupt the CRC
1096        let last = data.len() - 1;
1097        data[last] ^= 0x01;
1098        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1099        let zw = ZWaveLayer::new(idx);
1100        assert!(!zw.verify_crc(&data));
1101    }
1102
1103    #[test]
1104    fn test_summary_ack() {
1105        let data = ack_frame(0x0161f498, 1, 2);
1106        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1107        let zw = ZWaveLayer::new(idx);
1108        let s = zw.summary(&data);
1109        assert!(s.contains("ACK"));
1110        assert!(s.contains("0x0161f498"));
1111        assert!(s.contains("1->2"));
1112    }
1113
1114    #[test]
1115    fn test_summary_req() {
1116        let data = req_frame(0x0161f498, 1, 2, cmd_class::SWITCH_BINARY, 0x01, &[0xFF]);
1117        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1118        let zw = ZWaveLayer::new(idx);
1119        let s = zw.summary(&data);
1120        assert!(s.contains("SWITCH_BINARY"));
1121        assert!(s.contains("0x0161f498"));
1122        assert!(s.contains("1->2"));
1123        assert!(!s.contains("ACK"));
1124    }
1125
1126    #[test]
1127    fn test_get_field() {
1128        let data = req_frame(0x0161f498, 1, 2, cmd_class::SWITCH_BINARY, 0x01, &[0xFF]);
1129        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1130        let zw = ZWaveLayer::new(idx);
1131
1132        assert_eq!(
1133            zw.get_field(&data, "home_id").unwrap().unwrap(),
1134            FieldValue::U32(0x0161f498)
1135        );
1136        assert_eq!(
1137            zw.get_field(&data, "src").unwrap().unwrap(),
1138            FieldValue::U8(1)
1139        );
1140        assert_eq!(
1141            zw.get_field(&data, "dst").unwrap().unwrap(),
1142            FieldValue::U8(2)
1143        );
1144        assert_eq!(
1145            zw.get_field(&data, "ackreq").unwrap().unwrap(),
1146            FieldValue::Bool(true)
1147        );
1148        assert_eq!(
1149            zw.get_field(&data, "cmd_class").unwrap().unwrap(),
1150            FieldValue::U8(cmd_class::SWITCH_BINARY)
1151        );
1152        assert!(zw.get_field(&data, "nonexistent").is_none());
1153    }
1154
1155    #[test]
1156    fn test_builder_round_trip() {
1157        let built = ZWaveBuilder::new()
1158            .home_id(0x0161f498)
1159            .src(1)
1160            .dst(2)
1161            .cmd_class(cmd_class::SWITCH_BINARY)
1162            .cmd(0x01)
1163            .cmd_data(vec![0xFF])
1164            .build();
1165
1166        let idx = LayerIndex::new(LayerKind::ZWave, 0, built.len());
1167        let zw = ZWaveLayer::new(idx);
1168
1169        assert_eq!(zw.home_id(&built).unwrap(), 0x0161f498);
1170        assert_eq!(zw.src(&built).unwrap(), 1);
1171        assert_eq!(zw.dst(&built).unwrap(), 2);
1172        assert_eq!(zw.cmd_class(&built).unwrap(), cmd_class::SWITCH_BINARY);
1173        assert_eq!(zw.cmd(&built).unwrap(), 0x01);
1174        assert_eq!(zw.cmd_data(&built).unwrap(), &[0xFF]);
1175        assert!(zw.verify_crc(&built));
1176    }
1177
1178    #[test]
1179    fn test_is_zwave_frame_detection() {
1180        // Valid ACK frame
1181        let ack = ack_frame(0x0161f498, 1, 2);
1182        assert!(is_zwave_frame(&ack));
1183
1184        // Valid REQ frame
1185        let req = req_frame(0x0161f498, 1, 2, cmd_class::BASIC, 0x01, &[]);
1186        assert!(is_zwave_frame(&req));
1187
1188        // Too short
1189        assert!(!is_zwave_frame(&[0x00; 9]));
1190
1191        // Length field too large
1192        let mut bad = ack.clone();
1193        bad[7] = 0xFF; // length=255, but buffer is only 10
1194        assert!(!is_zwave_frame(&bad));
1195
1196        // Length field too small
1197        let mut bad2 = ack.clone();
1198        bad2[7] = 5; // length=5, less than minimum 10
1199        assert!(!is_zwave_frame(&bad2));
1200    }
1201
1202    #[test]
1203    fn test_set_field_home_id() {
1204        let mut data = ack_frame(0x0161f498, 1, 2);
1205        let idx = LayerIndex::new(LayerKind::ZWave, 0, data.len());
1206        let zw = ZWaveLayer::new(idx);
1207
1208        zw.set_field(&mut data, "home_id", FieldValue::U32(0xAABBCCDD))
1209            .unwrap()
1210            .unwrap();
1211        assert_eq!(zw.home_id(&data).unwrap(), 0xAABBCCDD);
1212    }
1213
1214    #[test]
1215    fn test_hashret_and_answers() {
1216        let frame1 = req_frame(0x0161f498, 1, 2, cmd_class::BASIC, 0x01, &[]);
1217        let frame2 = req_frame(0x0161f498, 2, 1, cmd_class::BASIC, 0x03, &[]);
1218
1219        let idx1 = LayerIndex::new(LayerKind::ZWave, 0, frame1.len());
1220        let zw1 = ZWaveLayer::new(idx1);
1221
1222        let idx2 = LayerIndex::new(LayerKind::ZWave, 0, frame2.len());
1223        let zw2 = ZWaveLayer::new(idx2);
1224
1225        // hashret should match (same home ID)
1226        assert_eq!(zw1.hashret(&frame1), zw2.hashret(&frame2));
1227
1228        // answers should be true (swapped src/dst)
1229        assert!(zw1.answers(&frame1, &zw2, &frame2));
1230        assert!(zw2.answers(&frame2, &zw1, &frame1));
1231
1232        // Different home ID should not match
1233        let frame3 = req_frame(0xDEADBEEF, 2, 1, cmd_class::BASIC, 0x03, &[]);
1234        let idx3 = LayerIndex::new(LayerKind::ZWave, 0, frame3.len());
1235        let zw3 = ZWaveLayer::new(idx3);
1236        assert!(!zw1.answers(&frame1, &zw3, &frame3));
1237    }
1238}