timex_datalink/protocol_4/
wrist_app.rs

1//! WristApp implementation for Protocol 4
2//!
3//! This module handles wrist applications for Timex Datalink watches.
4
5use crate::PacketGenerator;
6use crate::helpers::cpacket_paginator::paginate_cpackets;
7
8/// WristApp structure for Protocol 4
9pub struct WristApp {
10    /// Wrist app data bytes
11    pub wrist_app_data: Vec<u8>,
12}
13
14impl WristApp {
15    /// Create a new WristApp from a ZAP file path
16    /// 
17    /// This follows the Ruby implementation's approach of parsing ZAP files
18    pub fn from_zap_file(file_path: &str) -> std::io::Result<Self> {
19        // Read the ZAP file
20        let file_data = std::fs::read(file_path)?;
21        
22        // Parse the ZAP file
23        let wrist_app_data = Self::parse_zap_file(&file_data)?;
24        
25        Ok(Self {
26            wrist_app_data,
27        })
28    }
29    
30    /// Process a ZAP file to extract binary data
31    /// 
32    /// This implementation follows the Ruby version which:
33    /// 1. Finds sections delimited by "\xac.*\r\n"
34    /// 2. Extracts section at WRIST_APP_CODE_INDEX (18)
35    /// 3. Decodes the hex string to binary
36    pub fn parse_zap_file(zap_data: &[u8]) -> std::io::Result<Vec<u8>> {
37        // Constants from Ruby implementation
38        const WRIST_APP_CODE_INDEX: usize = 18;
39        
40        // Split the ZAP file by the delimiter '\xAC'
41        let mut sections = Vec::new();
42        let mut current_section = Vec::new();
43        let mut i = 0;
44        
45        while i < zap_data.len() {
46            if zap_data[i] == 0xAC {
47                // End of a section
48                if !current_section.is_empty() {
49                    sections.push(current_section);
50                    current_section = Vec::new();
51                }
52                
53                // Skip the delimiter and look for \r\n
54                i += 1;
55                while i < zap_data.len() && !(zap_data[i] == b'\r' && i + 1 < zap_data.len() && zap_data[i + 1] == b'\n') {
56                    i += 1;
57                }
58                
59                // Skip the \r\n if found
60                if i < zap_data.len() && zap_data[i] == b'\r' && i + 1 < zap_data.len() && zap_data[i + 1] == b'\n' {
61                    i += 2; // Skip \r\n
62                }
63            } else {
64                // Add to current section
65                current_section.push(zap_data[i]);
66                i += 1;
67            }
68        }
69        
70        // Add the last section if not empty
71        if !current_section.is_empty() {
72            sections.push(current_section);
73        }
74        
75        // Check if we have enough sections
76        if sections.len() <= WRIST_APP_CODE_INDEX {
77            return Err(std::io::Error::new(
78                std::io::ErrorKind::InvalidData,
79                format!("ZAP file does not contain enough sections, expected at least {}", WRIST_APP_CODE_INDEX + 1)
80            ));
81        }
82        
83        // Get the target section
84        let target_section = &sections[WRIST_APP_CODE_INDEX];
85        
86        // Convert the hex string to binary
87        // In Ruby: [zap_file_data_ascii].pack("H*")
88        let mut result = Vec::new();
89        
90        // Each pair of hex chars becomes one byte
91        let mut i = 0;
92        while i + 1 < target_section.len() {
93            let high = Self::hex_digit_to_value(target_section[i]);
94            let low = Self::hex_digit_to_value(target_section[i + 1]);
95            
96            if let (Some(high), Some(low)) = (high, low) {
97                result.push((high << 4) | low);
98            } else {
99                // If high isn't valid, skip to the next char
100                if high.is_none() {
101                    i += 1;
102                }
103                // If low isn't valid, we've already used high, so proceed as normal
104            }
105            
106            i += 2;
107        }
108        
109        Ok(result)
110    }
111    
112    /// Convert a hex digit (0-9, A-F) to its numeric value
113    fn hex_digit_to_value(digit: u8) -> Option<u8> {
114        match digit {
115            b'0'..=b'9' => Some(digit - b'0'),
116            b'A'..=b'F' => Some(digit - b'A' + 10),
117            b'a'..=b'f' => Some(digit - b'a' + 10),
118            _ => None,
119        }
120    }
121}
122
123impl PacketGenerator for WristApp {
124    fn packets(&self) -> Vec<Vec<u8>> {
125        // Constants from Ruby implementation
126        const CPACKET_CLEAR: [u8; 2] = [0x93, 0x02];
127        const CPACKET_SECT: [u8; 2] = [0x90, 0x02];
128        const CPACKET_DATA: [u8; 2] = [0x91, 0x02];
129        const CPACKET_END: [u8; 2] = [0x92, 0x02];
130        const CPACKET_DATA_LENGTH: usize = 32;
131        
132        // Process wrist app data (in Ruby there's parsing from ZAP files, 
133        // but here we're assuming the data is already preprocessed)
134        let wrist_app_data = &self.wrist_app_data;
135        
136        // Create the payloads using our paginator
137        let payloads = paginate_cpackets(&CPACKET_DATA, CPACKET_DATA_LENGTH, wrist_app_data);
138        
139        // Create the section packet
140        let mut cpacket_sect = Vec::new();
141        cpacket_sect.extend_from_slice(&CPACKET_SECT);
142        cpacket_sect.push(payloads.len() as u8);
143        cpacket_sect.push(1); // Fixed value from Ruby implementation
144        
145        // Combine all packets
146        let mut all_packets = Vec::with_capacity(payloads.len() + 3);
147        all_packets.push(CPACKET_CLEAR.to_vec());
148        all_packets.push(cpacket_sect);
149        all_packets.extend(payloads);
150        all_packets.push(CPACKET_END.to_vec());
151        
152        // Apply CRC wrapping
153        use crate::helpers::crc_packets_wrapper::wrap_packets_with_crc;
154        wrap_packets_with_crc(all_packets)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    // Include the actual ZAP file at compile time 
163    // The path is relative to the Cargo.toml file
164    const EXAMPLE_ZAP: &[u8] = include_bytes!("../../fixtures/EXAMPLE.ZAP");
165
166    #[test]
167    fn test_wrist_app() {
168        // Parse the ZAP file using our new method
169        let processed_data = WristApp::parse_zap_file(EXAMPLE_ZAP).unwrap();
170        
171        let wrist_app = WristApp {
172            wrist_app_data: processed_data,
173        };
174
175        // From golden fixture: wrist_app.jsonl
176        #[rustfmt::skip]
177        let expected = vec![
178    vec![
179      5,
180      147,
181      2,
182      48,
183      253
184    ],
185    vec![
186      7,
187      144,
188      2,
189      5,
190      1,
191      144,
192      251
193    ],
194    vec![
195      38,
196      145,
197      2,
198      1,
199      49,
200      53,
201      48,
202      115,
203      32,
204      100,
205      97,
206      116,
207      97,
208      58,
209      32,
210      76,
211      111,
212      114,
213      101,
214      109,
215      32,
216      105,
217      112,
218      115,
219      117,
220      109,
221      32,
222      100,
223      111,
224      108,
225      111,
226      114,
227      32,
228      115,
229      105,
230      116,
231      28,
232      52
233    ],
234    vec![
235      38,
236      145,
237      2,
238      2,
239      32,
240      97,
241      109,
242      101,
243      116,
244      44,
245      32,
246      99,
247      111,
248      110,
249      115,
250      101,
251      99,
252      116,
253      101,
254      116,
255      117,
256      114,
257      32,
258      97,
259      100,
260      105,
261      112,
262      105,
263      115,
264      99,
265      105,
266      110,
267      103,
268      32,
269      101,
270      108,
271      240,
272      169
273    ],
274    vec![
275      38,
276      145,
277      2,
278      3,
279      105,
280      116,
281      44,
282      32,
283      115,
284      101,
285      100,
286      32,
287      100,
288      111,
289      32,
290      101,
291      105,
292      117,
293      115,
294      109,
295      111,
296      100,
297      32,
298      116,
299      101,
300      109,
301      112,
302      111,
303      114,
304      32,
305      105,
306      110,
307      99,
308      105,
309      100,
310      105,
311      19,
312      82
313    ],
314    vec![
315      38,
316      145,
317      2,
318      4,
319      100,
320      117,
321      110,
322      116,
323      32,
324      117,
325      116,
326      32,
327      108,
328      97,
329      98,
330      111,
331      114,
332      101,
333      32,
334      101,
335      116,
336      32,
337      100,
338      111,
339      108,
340      111,
341      114,
342      101,
343      32,
344      109,
345      97,
346      103,
347      110,
348      97,
349      32,
350      97,
351      208,
352      63
353    ],
354    vec![
355      12,
356      145,
357      2,
358      5,
359      108,
360      105,
361      113,
362      117,
363      97,
364      46,
365      127,
366      67
367    ],
368    vec![
369      5,
370      146,
371      2,
372      160,
373      252
374    ]
375  ];
376
377        assert_eq!(wrist_app.packets(), expected);
378    }
379}