1pub mod air_quality;
26pub mod e1;
27pub mod error;
28pub mod ruuvi_data;
29pub mod v5;
30pub mod v6;
31
32pub use error::{DecodeError, Result};
33pub use ruuvi_data::{DataFormat, RuuviData};
34
35pub fn decode(hex_data: &str) -> Result<RuuviData> {
61 let clean_hex = hex_data.trim().trim_start_matches("0x").replace(' ', "");
63
64 let bytes = hex_to_bytes(&clean_hex)?;
66
67 if bytes.is_empty() {
68 return Err(DecodeError::InvalidLength("Empty data".into()));
69 }
70
71 match bytes[0] {
73 5 => {
74 let data = v5::decode(&bytes)?;
75 Ok(RuuviData::V5(data))
76 }
77 6 => {
78 let data = v6::decode(&bytes)?;
79 Ok(RuuviData::V6(data))
80 }
81 0xE1 => {
82 let data = e1::decode(&bytes)?;
83 Ok(RuuviData::E1(data))
84 }
85 format => Err(DecodeError::UnsupportedFormat(format)),
86 }
87}
88
89#[must_use]
102pub fn extract_ruuvi_from_ble(ble_data: &str) -> Option<String> {
103 let clean_data = ble_data.trim().to_uppercase();
104
105 if !clean_data.chars().all(|c| c.is_ascii_hexdigit()) {
107 return None;
108 }
109
110 for pattern in ["9904", "0499"] {
113 if let Some(start_idx) = clean_data.find(pattern) {
114 if start_idx != 0 {
115 continue;
116 }
117
118 let payload_start = start_idx + 4; if payload_start <= clean_data.len() {
123 let payload = &clean_data[payload_start..];
124 return Some(payload.to_string());
125 }
126 }
127 }
128
129 None
130}
131
132fn hex_to_bytes(hex_str: &str) -> Result<Vec<u8>> {
134 if !hex_str.len().is_multiple_of(2) {
135 return Err(DecodeError::InvalidHex(format!(
136 "Odd number of hex characters: {}",
137 hex_str.len()
138 )));
139 }
140
141 hex::decode(hex_str).map_err(|_| DecodeError::InvalidHex(hex_str.to_string()))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_hex_to_bytes() {
150 assert_eq!(hex_to_bytes("01FF").unwrap(), vec![0x01, 0xFF]);
151 assert_eq!(hex_to_bytes("").unwrap(), Vec::<u8>::new());
152 assert!(hex_to_bytes("0").is_err()); assert!(hex_to_bytes("GG").is_err()); }
155
156 #[test]
157 fn test_extract_ruuvi_from_ble() {
158 let ble_data = "99040512FC5394C37C0004FFFC040CAC364200CDCBB8334C884F";
159 let payload = extract_ruuvi_from_ble(ble_data).expect("Failed to extract Ruuvi data");
160 assert_eq!(
161 payload,
162 "0512FC5394C37C0004FFFC040CAC364200CDCBB8334C884F".to_string()
163 );
164 assert_eq!(payload.len(), v5::PAYLOAD_WITH_MAC_LENGTH * 2);
165 assert!(payload.starts_with("05")); let ble_data_le = "04990512FC5394C37C0004FFFC040CAC364200CDCBB8334C884F";
169 let extracted_le = extract_ruuvi_from_ble(ble_data_le);
170 assert!(extracted_le.is_some());
171
172 let non_ruuvi = "020106030316910255AA";
174 assert!(extract_ruuvi_from_ble(non_ruuvi).is_none());
175 }
176
177 #[test]
178 fn test_decode_empty_data() {
179 assert!(decode("").is_err());
180 }
181
182 #[test]
183 fn test_unsupported_format() {
184 let result = decode("63000000000000000000000000000000000000000000000000");
186 assert!(matches!(result, Err(DecodeError::UnsupportedFormat(99))));
187 }
188
189 #[test]
190 fn test_decoding_ruuvi_data() {
191 let ble_data = "99040512FC5394C37C0004FFFC040CAC364200CDCBB8334C884F";
192 let payload = decode(&extract_ruuvi_from_ble(ble_data).expect("ble_data"))
193 .expect("Failed to extract Ruuvi data");
194 match payload {
195 RuuviData::V5(_) => (),
196 _ => panic!("Unexpected data format"),
197 }
198
199 let ble_data = "990406170C5668C79E007000C90501D9FFCD004C884F";
200 let payload = decode(&extract_ruuvi_from_ble(ble_data).expect("ble_data"))
201 .expect("Failed to extract Ruuvi data");
202 match payload {
203 RuuviData::V6(_) => (),
204 _ => panic!("Unexpected data format"),
205 }
206
207 let ble_data =
208 "9904E1170C5668C79E0065007004BD11CA00C90A0213E0AC000000DECDEE100000000000CBB8334C884F";
209 let payload = decode(&extract_ruuvi_from_ble(ble_data).expect("ble_data"))
210 .expect("Failed to extract Ruuvi data");
211 match payload {
212 RuuviData::E1(_) => (),
213 _ => panic!("Unexpected data format"),
214 }
215 }
216}