1use serde::Serialize;
9
10#[derive(Debug, Clone, Serialize, schemars::JsonSchema, PartialEq, Eq)]
15pub struct Bytes {
16 pub hex: String,
18 pub bytes: Vec<u8>,
20}
21
22impl Bytes {
23 pub fn from_slice(data: &[u8]) -> Self {
25 let hex = data
26 .iter()
27 .map(|b| format!("0x{b:02X}"))
28 .collect::<Vec<_>>()
29 .join(",");
30 Self {
31 hex,
32 bytes: data.to_vec(),
33 }
34 }
35}
36
37pub fn parse_byte(s: &str) -> Result<u8, String> {
39 let s = s.trim();
40 let parsed = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
41 u8::from_str_radix(hex, 16)
42 } else if let Some(bin) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) {
43 u8::from_str_radix(bin, 2)
44 } else {
45 s.parse::<u8>()
46 };
47 parsed.map_err(|e| format!("invalid byte '{s}': {e}"))
48}
49
50pub fn parse_bytes(s: &str) -> Result<Vec<u8>, String> {
56 let s = s.trim();
57 if s.is_empty() {
58 return Ok(Vec::new());
59 }
60 if s.contains(',') {
61 return s.split(',').map(|tok| parse_byte(tok.trim())).collect();
62 }
63 if (s.starts_with("0x") || s.starts_with("0X") || s.starts_with("0b") || s.starts_with("0B"))
65 && let Ok(b) = parse_byte(s)
66 {
67 return Ok(vec![b]);
68 }
69 let hex = s
71 .strip_prefix("0x")
72 .or_else(|| s.strip_prefix("0X"))
73 .unwrap_or(s);
74 if !hex.len().is_multiple_of(2) {
75 return Err(format!("hex string '{s}' has an odd number of digits"));
76 }
77 (0..hex.len())
78 .step_by(2)
79 .map(|i| {
80 u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| format!("invalid hex at {i}: {e}"))
81 })
82 .collect()
83}
84
85pub fn validate_i2c_address(addr: u8) -> Result<u8, String> {
87 if addr > 0x7F {
88 return Err(format!(
89 "I2C address 0x{addr:02X} exceeds 7-bit range (max 0x7F)"
90 ));
91 }
92 Ok(addr)
93}
94
95pub fn validate_adc_channel(ch: u8) -> Result<u8, String> {
97 if ch > 3 {
98 return Err(format!("ADC channel {ch} out of range (0..=3)"));
99 }
100 Ok(ch)
101}
102
103pub fn validate_timeout_ms(ms: u32) -> Result<u32, String> {
105 if ms == 0 {
106 return Err("timeout_ms must be non-zero".to_string());
107 }
108 Ok(ms)
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn parse_byte_accepts_hex_bin_decimal() {
117 assert_eq!(parse_byte("0x48").unwrap(), 0x48);
118 assert_eq!(parse_byte("0xab").unwrap(), 0xAB);
119 assert_eq!(parse_byte("0b10101010").unwrap(), 0xAA);
120 assert_eq!(parse_byte("20").unwrap(), 20);
121 }
122
123 #[test]
124 fn parse_byte_rejects_garbage_and_overflow() {
125 assert!(parse_byte("0xGG").is_err());
126 assert!(parse_byte("0x100").is_err());
127 assert!(parse_byte("").is_err());
128 }
129
130 #[test]
131 fn parse_bytes_accepts_comma_list_and_bare_hex() {
132 assert_eq!(parse_bytes("0x0A,20,0xFF").unwrap(), vec![0x0A, 20, 0xFF]);
133 assert_eq!(parse_bytes("cc44").unwrap(), vec![0xCC, 0x44]);
134 assert_eq!(parse_bytes("0xCC44").unwrap(), vec![0xCC, 0x44]);
135 assert_eq!(parse_bytes("").unwrap(), Vec::<u8>::new());
136 }
137
138 #[test]
139 fn parse_bytes_rejects_odd_bare_hex() {
140 assert!(parse_bytes("ccc").is_err());
141 }
142
143 #[test]
144 fn bytes_from_slice_roundtrip() {
145 let b = Bytes::from_slice(&[0x48, 0x00]);
146 assert_eq!(b.hex, "0x48,0x00");
147 assert_eq!(b.bytes, vec![0x48, 0x00]);
148 }
149
150 #[test]
151 fn bytes_empty_formats_cleanly() {
152 let b = Bytes::from_slice(&[]);
153 assert_eq!(b.hex, "");
154 assert!(b.bytes.is_empty());
155 }
156
157 #[test]
158 fn validate_i2c_address_bounds() {
159 assert_eq!(validate_i2c_address(0x48).unwrap(), 0x48);
160 assert!(validate_i2c_address(0x80).is_err());
161 }
162
163 #[test]
164 fn validate_adc_channel_bounds() {
165 for c in 0..=3u8 {
166 assert!(validate_adc_channel(c).is_ok());
167 }
168 assert!(validate_adc_channel(4).is_err());
169 }
170
171 #[test]
172 fn validate_timeout_rejects_zero() {
173 assert!(validate_timeout_ms(0).is_err());
174 assert_eq!(validate_timeout_ms(1000).unwrap(), 1000);
175 }
176}