knx_pico/addressing/
group.rs

1//! KNX Group Address implementation.
2//!
3//! Group addresses represent logical groupings of devices for functional control.
4//! Two formats are supported:
5//! - 2-level: Main/Sub (e.g., 1/234)
6//! - 3-level: Main/Middle/Sub (e.g., 1/2/3) - most common
7//!
8//! Internally stored as 16 bits:
9//! - Main: 5 bits (0-31)
10//! - Middle: 3 bits (0-7)
11//! - Sub: 8 bits (0-255)
12
13use crate::error::{KnxError, Result};
14use core::fmt;
15
16/// KNX Group Address
17///
18/// Used for logical grouping of devices and functions.
19///
20/// # Examples
21///
22/// ```
23/// use knx_pico::GroupAddress;
24///
25/// // Create 3-level address
26/// let addr = GroupAddress::new(1, 2, 3).unwrap();
27/// assert_eq!(addr.to_string(), "1/2/3");
28///
29/// // Create 2-level address
30/// let addr = GroupAddress::new_2level(1, 234).unwrap();
31/// assert_eq!(addr.to_string_2level(), "1/234");
32///
33/// // Create from raw u16
34/// let addr = GroupAddress::from(0x0A03u16);
35/// assert_eq!(addr.main(), 1);
36/// assert_eq!(addr.middle(), 2);
37/// assert_eq!(addr.sub(), 3);
38///
39/// // Parse from string (auto-detects format)
40/// let addr: GroupAddress = "1/2/3".parse().unwrap();
41/// assert_eq!(u16::from(addr), 0x0A03);
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[cfg_attr(feature = "defmt", derive(defmt::Format))]
45pub struct GroupAddress {
46    raw: u16,
47}
48
49impl GroupAddress {
50    /// Maximum main group value (5 bits)
51    pub const MAX_MAIN: u8 = 31;
52    /// Maximum middle group value (3 bits)
53    pub const MAX_MIDDLE: u8 = 7;
54    /// Maximum sub group value (8 bits)
55    pub const MAX_SUB: u8 = 255;
56    /// Maximum sub value for 2-level format (11 bits)
57    pub const MAX_SUB_2LEVEL: u16 = 2047;
58
59    /// Create a new 3-level Group Address (Main/Middle/Sub).
60    ///
61    /// # Arguments
62    ///
63    /// * `main` - Main group (0-31)
64    /// * `middle` - Middle group (0-7)
65    /// * `sub` - Sub group (0-255)
66    ///
67    /// # Errors
68    ///
69    /// Returns `KnxError::AddressOutOfRange` if any component is out of range.
70    pub fn new(main: u8, middle: u8, sub: u8) -> Result<Self> {
71        if main > Self::MAX_MAIN {
72            return Err(KnxError::address_out_of_range());
73        }
74        if middle > Self::MAX_MIDDLE {
75            return Err(KnxError::address_out_of_range());
76        }
77        // sub is u8, so it's always in range
78
79        let raw = (u16::from(main) << 11) | (u16::from(middle) << 8) | u16::from(sub);
80        Ok(Self { raw })
81    }
82
83    /// Create a new 2-level Group Address (Main/Sub).
84    ///
85    /// # Arguments
86    ///
87    /// * `main` - Main group (0-31)
88    /// * `sub` - Sub group (0-2047)
89    ///
90    /// # Errors
91    ///
92    /// Returns `KnxError::AddressOutOfRange` if any component is out of range.
93    pub fn new_2level(main: u8, sub: u16) -> Result<Self> {
94        if main > Self::MAX_MAIN {
95            return Err(KnxError::address_out_of_range());
96        }
97        if sub > Self::MAX_SUB_2LEVEL {
98            return Err(KnxError::address_out_of_range());
99        }
100
101        let raw = (u16::from(main) << 11) | sub;
102        Ok(Self { raw })
103    }
104
105    /// Create from a 3-element array `[main, middle, sub]`.
106    ///
107    /// Convenient for creating 3-level addresses from array literals.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use knx_pico::GroupAddress;
113    ///
114    /// let addr = GroupAddress::from_array([1, 2, 3])?;
115    /// assert_eq!(addr.to_string(), "1/2/3");
116    /// # Ok::<(), knx_pico::KnxError>(())
117    /// ```
118    pub fn from_array(parts: [u8; 3]) -> Result<Self> {
119        Self::new(parts[0], parts[1], parts[2])
120    }
121
122    /// Get the raw u16 representation of the address.
123    #[inline(always)]
124    pub const fn raw(self) -> u16 {
125        self.raw
126    }
127
128    /// Get the main group component (0-31).
129    #[inline(always)]
130    pub const fn main(self) -> u8 {
131        ((self.raw >> 11) & 0x1F) as u8
132    }
133
134    /// Get the middle group component for 3-level format (0-7).
135    #[inline(always)]
136    pub const fn middle(self) -> u8 {
137        ((self.raw >> 8) & 0x07) as u8
138    }
139
140    /// Get the sub group component for 3-level format (0-255).
141    #[inline(always)]
142    pub const fn sub(self) -> u8 {
143        (self.raw & 0xFF) as u8
144    }
145
146    /// Get the sub group component for 2-level format (0-2047).
147    #[inline(always)]
148    pub const fn sub_2level(self) -> u16 {
149        self.raw & 0x07FF
150    }
151
152    /// Format as 3-level string (Main/Middle/Sub).
153    ///
154    /// # Panics
155    ///
156    /// May panic if the formatted string exceeds 16 bytes capacity, though this
157    /// should never occur in practice as the maximum length is "31/7/255" (9 bytes).
158    pub fn to_string_3level(&self) -> heapless::String<16> {
159        use core::fmt::Write;
160        let mut s = heapless::String::new();
161        // Ignoring error is safe: max string is "31/7/255" = 9 bytes < 16 capacity
162        let _ = write!(s, "{}/{}/{}", self.main(), self.middle(), self.sub());
163        s
164    }
165
166    /// Format as 2-level string (Main/Sub).
167    ///
168    /// # Panics
169    ///
170    /// May panic if the formatted string exceeds 16 bytes capacity, though this
171    /// should never occur in practice as the maximum length is "31/2047" (8 bytes).
172    pub fn to_string_2level(&self) -> heapless::String<16> {
173        use core::fmt::Write;
174        let mut s = heapless::String::new();
175        // Ignoring error is safe: max string is "31/2047" = 8 bytes < 16 capacity
176        let _ = write!(s, "{}/{}", self.main(), self.sub_2level());
177        s
178    }
179
180    /// Encode the address into a byte buffer (big-endian).
181    ///
182    /// # Arguments
183    ///
184    /// * `buf` - Buffer to write to (must be at least 2 bytes)
185    ///
186    /// # Errors
187    ///
188    /// Returns `KnxError::BufferTooSmall` if buffer is too small.
189    #[inline]
190    pub fn encode(&self, buf: &mut [u8]) -> Result<usize> {
191        if buf.len() < 2 {
192            return Err(KnxError::buffer_too_small());
193        }
194        buf[0..2].copy_from_slice(&self.raw.to_be_bytes());
195        Ok(2)
196    }
197
198    /// Decode an address from a byte buffer (big-endian).
199    ///
200    /// # Arguments
201    ///
202    /// * `buf` - Buffer to read from (must be at least 2 bytes)
203    ///
204    /// # Errors
205    ///
206    /// Returns `KnxError::BufferTooSmall` if buffer is too small.
207    #[inline]
208    pub fn decode(buf: &[u8]) -> Result<Self> {
209        if buf.len() < 2 {
210            return Err(KnxError::buffer_too_small());
211        }
212        let raw = u16::from_be_bytes([buf[0], buf[1]]);
213        Ok(Self { raw })
214    }
215}
216
217impl From<u16> for GroupAddress {
218    #[inline(always)]
219    fn from(raw: u16) -> Self {
220        Self { raw }
221    }
222}
223
224impl From<GroupAddress> for u16 {
225    #[inline(always)]
226    fn from(addr: GroupAddress) -> u16 {
227        addr.raw
228    }
229}
230
231impl fmt::Display for GroupAddress {
232    /// Format as 3-level address by default
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        write!(f, "{}/{}/{}", self.main(), self.middle(), self.sub())
235    }
236}
237
238impl core::str::FromStr for GroupAddress {
239    type Err = KnxError;
240
241    fn from_str(s: &str) -> Result<Self> {
242        // Zero-allocation parsing using iterators
243        let mut parts = s.split('/');
244
245        let main = parts
246            .next()
247            .and_then(|s| s.parse::<u8>().ok())
248            .ok_or_else(KnxError::invalid_group_address)?;
249
250        let middle = parts
251            .next()
252            .and_then(|s| s.parse::<u16>().ok())
253            .ok_or_else(KnxError::invalid_group_address)?;
254
255        // Check if there's a third part (3-level format)
256        if let Some(sub_str) = parts.next() {
257            // 3-level format: Main/Middle/Sub
258            let sub = sub_str
259                .parse::<u8>()
260                .ok()
261                .ok_or_else(KnxError::invalid_group_address)?;
262
263            // Ensure no extra parts
264            if parts.next().is_some() {
265                return Err(KnxError::invalid_group_address());
266            }
267
268            // middle is actually middle (u8), not sub (u16)
269            if middle > 255 {
270                return Err(KnxError::invalid_group_address());
271            }
272
273            Self::new(main, middle as u8, sub)
274        } else {
275            // 2-level format: Main/Sub
276            // middle is actually the sub value
277            // Ensure no extra parts
278            if parts.next().is_some() {
279                return Err(KnxError::invalid_group_address());
280            }
281
282            Self::new_2level(main, middle)
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_new_3level_valid() {
293        let addr = GroupAddress::new(1, 2, 3).unwrap();
294        assert_eq!(addr.main(), 1);
295        assert_eq!(addr.middle(), 2);
296        assert_eq!(addr.sub(), 3);
297    }
298
299    #[test]
300    fn test_new_3level_invalid_main() {
301        let result = GroupAddress::new(32, 0, 0);
302        assert!(result.is_err());
303    }
304
305    #[test]
306    fn test_new_3level_invalid_middle() {
307        let result = GroupAddress::new(0, 8, 0);
308        assert!(result.is_err());
309    }
310
311    #[test]
312    fn test_new_2level_valid() {
313        let addr = GroupAddress::new_2level(1, 234).unwrap();
314        assert_eq!(addr.main(), 1);
315        assert_eq!(addr.sub_2level(), 234);
316    }
317
318    #[test]
319    fn test_new_2level_invalid() {
320        let result = GroupAddress::new_2level(0, 2048);
321        assert!(result.is_err());
322    }
323
324    #[test]
325    fn test_from_raw() {
326        // 1/2/3 = 0b00001_010_00000011 = 0x0A03
327        let addr = GroupAddress::from(0x0A03u16);
328        assert_eq!(addr.main(), 1);
329        assert_eq!(addr.middle(), 2);
330        assert_eq!(addr.sub(), 3);
331    }
332
333    #[test]
334    fn test_to_raw() {
335        let addr = GroupAddress::new(1, 2, 3).unwrap();
336        assert_eq!(u16::from(addr), 0x0A03);
337    }
338
339    #[test]
340    fn test_encode_decode() {
341        let addr = GroupAddress::new(31, 7, 255).unwrap();
342        let mut buf = [0u8; 2];
343        addr.encode(&mut buf).unwrap();
344        let decoded = GroupAddress::decode(&buf).unwrap();
345        assert_eq!(addr, decoded);
346    }
347
348    #[test]
349    fn test_display_3level() {
350        let addr = GroupAddress::new(1, 2, 3).unwrap();
351        assert_eq!(format!("{}", addr), "1/2/3");
352    }
353
354    #[test]
355    fn test_to_string_2level() {
356        let addr = GroupAddress::new_2level(1, 234).unwrap();
357        assert_eq!(addr.to_string_2level(), "1/234");
358    }
359
360    #[test]
361    fn test_from_str_3level() {
362        let addr: GroupAddress = "1/2/3".parse().unwrap();
363        assert_eq!(addr.main(), 1);
364        assert_eq!(addr.middle(), 2);
365        assert_eq!(addr.sub(), 3);
366    }
367
368    #[test]
369    fn test_from_str_2level() {
370        let addr: GroupAddress = "1/234".parse().unwrap();
371        assert_eq!(addr.main(), 1);
372        assert_eq!(addr.sub_2level(), 234);
373    }
374
375    #[test]
376    fn test_from_str_invalid() {
377        // Too few parts
378        let result = "1".parse::<GroupAddress>();
379        assert!(result.is_err());
380
381        // Out of range (main)
382        let result = "32/0/0".parse::<GroupAddress>();
383        assert!(result.is_err());
384
385        // Too many parts
386        let result = "1/2/3/4".parse::<GroupAddress>();
387        assert!(result.is_err());
388
389        // Non-numeric
390        let result = "a/b/c".parse::<GroupAddress>();
391        assert!(result.is_err());
392
393        // Empty
394        let result = "".parse::<GroupAddress>();
395        assert!(result.is_err());
396
397        // Out of range (2-level sub)
398        let result = "1/2048".parse::<GroupAddress>();
399        assert!(result.is_err());
400    }
401}