Skip to main content

grafton_visca/command/bytes/
mod.rs

1//! Zero-allocation, compile-time command encoding for VISCA protocol.
2//!
3//! This module provides const command encoding to eliminate heap allocations
4//! and enable compile-time validation of VISCA command byte sequences.
5
6pub mod builder;
7pub mod constants;
8
9pub use builder::ConstCommandBuilder;
10
11/// VISCA command terminator byte.
12pub const VISCA_TERMINATOR: u8 = 0xFF;
13
14/// Default camera ID for VISCA commands.
15pub const DEFAULT_CAMERA_ID: u8 = 0x81;
16
17/// Stack-allocated, length-aware command buffer.
18///
19/// This type provides a zero-allocation representation for encoded VISCA commands
20/// that carries both the bytes and the actual encoded length. This prevents
21/// accidental transmission of trailing bytes after the terminator.
22///
23/// # Design Rationale
24///
25/// Unlike returning a raw `[u8; N]` which loses length information, `FixedCommandBytes`
26/// ensures that callers always have access to the correct slice via [`as_slice()`](Self::as_slice)
27/// or [`AsRef<[u8]>`](AsRef). The underlying array may contain trailing zeros after
28/// the encoded data, but these are never exposed through the safe API.
29///
30/// # Equality and Hashing
31///
32/// `FixedCommandBytes` implements `PartialEq`, `Eq`, and `Hash` based only on the
33/// meaningful bytes (`&self[..len]`), not the full buffer capacity. This means two
34/// `FixedCommandBytes` with different `N` but the same content compare as equal
35/// when compared via slice, and hash identically.
36///
37/// # Examples
38///
39/// ```ignore
40/// let cmd = MyCommand { value: 42 };
41/// let encoded = cmd.to_fixed_bytes::<16>(CameraId::CAMERA_1)?;
42///
43/// // Safe access via as_slice() - only returns meaningful bytes
44/// let slice = encoded.as_slice();
45/// assert_eq!(slice.last(), Some(&0xFF)); // Properly terminated
46///
47/// // Works with APIs expecting &[u8]
48/// transport.send(encoded.as_ref())?;
49///
50/// // Length is always available
51/// println!("Command is {} bytes", encoded.len());
52/// ```
53#[derive(Debug, Clone, Copy)]
54pub struct FixedCommandBytes<const N: usize> {
55    bytes: [u8; N],
56    len: usize,
57}
58
59impl<const N: usize> FixedCommandBytes<N> {
60    /// Creates a new `FixedCommandBytes` from a buffer and length.
61    ///
62    /// # Safety Invariant
63    ///
64    /// The caller must ensure that `len <= N` and that the bytes `[0..len]`
65    /// contain a valid, terminated VISCA command.
66    #[inline]
67    pub(crate) const fn new(bytes: [u8; N], len: usize) -> Self {
68        debug_assert!(len <= N);
69        Self { bytes, len }
70    }
71
72    /// Returns the encoded command bytes as a slice.
73    ///
74    /// This returns only the meaningful bytes (up to the terminator),
75    /// excluding any trailing zeros in the underlying buffer.
76    #[inline]
77    pub const fn as_slice(&self) -> &[u8] {
78        // SAFETY: split_at panics if len > N, but our invariant guarantees len <= N.
79        // However, const fn can't use &self.bytes[..self.len] directly, so we use
80        // split_at which is const-compatible.
81        self.bytes.split_at(self.len).0
82    }
83
84    /// Returns the length of the encoded command in bytes.
85    #[inline]
86    pub const fn len(&self) -> usize {
87        self.len
88    }
89
90    /// Returns `true` if the command is empty (length 0).
91    #[inline]
92    pub const fn is_empty(&self) -> bool {
93        self.len == 0
94    }
95
96    /// Consumes the wrapper and returns the underlying array.
97    ///
98    /// **Warning:** The returned array may contain trailing zeros after position
99    /// `self.len()`. Prefer using [`as_slice()`](Self::as_slice) for transmission.
100    #[inline]
101    pub const fn into_array(self) -> [u8; N] {
102        self.bytes
103    }
104
105    /// Returns a reference to the underlying array.
106    ///
107    /// **Warning:** The returned array may contain trailing zeros after position
108    /// `self.len()`. Prefer using [`as_slice()`](Self::as_slice) for transmission.
109    #[inline]
110    pub const fn as_array(&self) -> &[u8; N] {
111        &self.bytes
112    }
113}
114
115impl<const N: usize> AsRef<[u8]> for FixedCommandBytes<N> {
116    #[inline]
117    fn as_ref(&self) -> &[u8] {
118        &self.bytes[..self.len]
119    }
120}
121
122impl<const N: usize> core::ops::Deref for FixedCommandBytes<N> {
123    type Target = [u8];
124
125    #[inline]
126    fn deref(&self) -> &Self::Target {
127        &self.bytes[..self.len]
128    }
129}
130
131// Manual PartialEq implementation comparing only meaningful bytes
132impl<const N: usize> PartialEq for FixedCommandBytes<N> {
133    #[inline]
134    fn eq(&self, other: &Self) -> bool {
135        self.as_slice() == other.as_slice()
136    }
137}
138
139impl<const N: usize> Eq for FixedCommandBytes<N> {}
140
141// Allow comparing FixedCommandBytes directly with byte slices
142impl<const N: usize> PartialEq<[u8]> for FixedCommandBytes<N> {
143    #[inline]
144    fn eq(&self, other: &[u8]) -> bool {
145        self.as_slice() == other
146    }
147}
148
149impl<const N: usize> PartialEq<&[u8]> for FixedCommandBytes<N> {
150    #[inline]
151    fn eq(&self, other: &&[u8]) -> bool {
152        self.as_slice() == *other
153    }
154}
155
156// Hash only the meaningful bytes, not the full buffer
157impl<const N: usize> core::hash::Hash for FixedCommandBytes<N> {
158    #[inline]
159    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
160        self.as_slice().hash(state);
161    }
162}
163
164// Borrow as slice for use in HashMap/HashSet lookups
165impl<const N: usize> core::borrow::Borrow<[u8]> for FixedCommandBytes<N> {
166    #[inline]
167    fn borrow(&self) -> &[u8] {
168        self.as_slice()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_fixed_command_bytes_as_slice() {
178        let bytes = [0x81, 0x01, 0x04, 0x47, VISCA_TERMINATOR, 0x00, 0x00, 0x00];
179        let fixed = FixedCommandBytes::new(bytes, 5);
180
181        assert_eq!(
182            fixed.as_slice(),
183            &[0x81, 0x01, 0x04, 0x47, VISCA_TERMINATOR]
184        );
185        assert_eq!(fixed.len(), 5);
186        assert!(!fixed.is_empty());
187    }
188
189    #[test]
190    fn test_fixed_command_bytes_as_ref() {
191        let bytes = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
192        let fixed = FixedCommandBytes::new(bytes, 3);
193
194        let slice: &[u8] = fixed.as_ref();
195        assert_eq!(slice, &[0x81, 0x01, VISCA_TERMINATOR]);
196    }
197
198    #[test]
199    fn test_fixed_command_bytes_deref() {
200        let bytes = [0x81, VISCA_TERMINATOR, 0x00, 0x00];
201        let fixed = FixedCommandBytes::new(bytes, 2);
202
203        // Test Deref trait
204        assert_eq!(&*fixed, &[0x81, VISCA_TERMINATOR]);
205        assert_eq!(fixed.last(), Some(&VISCA_TERMINATOR));
206    }
207
208    #[test]
209    fn test_fixed_command_bytes_into_array() {
210        let bytes = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
211        let fixed = FixedCommandBytes::new(bytes, 3);
212
213        let array = fixed.into_array();
214        assert_eq!(array, [0x81, 0x01, VISCA_TERMINATOR, 0x00]);
215    }
216
217    #[test]
218    fn test_fixed_command_bytes_as_array() {
219        let bytes = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
220        let fixed = FixedCommandBytes::new(bytes, 3);
221
222        assert_eq!(fixed.as_array(), &[0x81, 0x01, VISCA_TERMINATOR, 0x00]);
223    }
224
225    #[test]
226    fn test_fixed_command_bytes_terminates_correctly() {
227        let bytes = [0x81, 0x01, 0x04, 0x47, VISCA_TERMINATOR, 0x00, 0x00, 0x00];
228        let fixed = FixedCommandBytes::new(bytes, 5);
229
230        // Verify terminator is accessible
231        assert_eq!(fixed.as_slice().last(), Some(&VISCA_TERMINATOR));
232
233        // Verify trailing zeros are not exposed
234        assert!(!fixed.as_slice().contains(&0x00) || fixed.as_slice()[0..4].contains(&0x00));
235    }
236
237    #[test]
238    fn test_fixed_command_bytes_equality_ignores_trailing_bytes() {
239        // Two buffers with same meaningful content but different trailing bytes
240        let bytes1 = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
241        let bytes2 = [0x81, 0x01, VISCA_TERMINATOR, 0xAB]; // Different trailing byte
242
243        let fixed1 = FixedCommandBytes::new(bytes1, 3);
244        let fixed2 = FixedCommandBytes::new(bytes2, 3);
245
246        // Should be equal since only first 3 bytes matter
247        assert_eq!(fixed1, fixed2);
248    }
249
250    #[test]
251    fn test_fixed_command_bytes_partial_eq_with_slice() {
252        let bytes = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
253        let fixed = FixedCommandBytes::new(bytes, 3);
254
255        // Compare directly with slice via deref
256        assert!(*fixed == [0x81, 0x01, VISCA_TERMINATOR]);
257        assert!(fixed.as_slice() == [0x81, 0x01, VISCA_TERMINATOR]);
258
259        // Should not equal different slice
260        assert!(*fixed != [0x81, 0x01, 0x00]);
261    }
262
263    #[test]
264    fn test_fixed_command_bytes_hash_consistency() {
265        use core::hash::{Hash, Hasher};
266
267        // Same content, different trailing bytes - should hash the same
268        let bytes1 = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
269        let bytes2 = [0x81, 0x01, VISCA_TERMINATOR, 0xAB]; // Different trailing byte
270
271        let fixed1 = FixedCommandBytes::new(bytes1, 3);
272        let fixed2 = FixedCommandBytes::new(bytes2, 3);
273
274        // Use a simple hasher to verify hash equality
275        fn hash_it<H: Hash>(val: &H) -> u64 {
276            let mut hasher = std::collections::hash_map::DefaultHasher::new();
277            val.hash(&mut hasher);
278            hasher.finish()
279        }
280
281        assert_eq!(hash_it(&fixed1), hash_it(&fixed2));
282    }
283
284    #[test]
285    fn test_fixed_command_bytes_borrow() {
286        use core::borrow::Borrow;
287
288        let bytes = [0x81, 0x01, VISCA_TERMINATOR, 0x00];
289        let fixed = FixedCommandBytes::new(bytes, 3);
290
291        // Can borrow as slice
292        let slice: &[u8] = fixed.borrow();
293        assert_eq!(slice, &[0x81, 0x01, VISCA_TERMINATOR]);
294    }
295}