timex_datalink/protocol_3/
wrist_app.rs

1//! Wrist App implementation for Protocol 3
2//!
3//! This module handles wrist app functionality for Timex Datalink watches.
4
5use std::path::PathBuf;
6use crate::PacketGenerator;
7use crate::helpers::crc_packets_wrapper;
8use crate::helpers::cpacket_paginator;
9
10/// Wrist App structure for Protocol 3
11///
12/// This allows loading wrist apps from ZAP files or raw data.
13pub struct WristApp {
14    /// The wrist app data bytes
15    pub wrist_app_data: Vec<u8>,
16}
17
18impl WristApp {
19    /// Create a new WristApp from raw data
20    pub fn new(wrist_app_data: Vec<u8>) -> Self {
21        Self {
22            wrist_app_data,
23        }
24    }
25    
26    /// Create a new WristApp from a ZAP file path
27    /// 
28    /// This follows the Ruby implementation's approach of parsing ZAP files
29    // pub fn from_zap_file(file_path: &str) -> std::io::Result<Self> {
30    pub fn from_zap_file(file_path: PathBuf) -> std::io::Result<Self> {
31        // Read the ZAP file
32        let file_data = std::fs::read(file_path)?;
33        
34        // Parse the ZAP file
35        let wrist_app_data = Self::parse_zap_file(&file_data)?;
36        
37        Ok(Self {
38            wrist_app_data,
39        })
40    }
41    
42    /// Process a ZAP file to extract binary data
43    /// 
44    /// This implementation follows the Ruby version which:
45    /// 1. Finds sections delimited by "\xac.*\r\n"
46    /// 2. Extracts section at WRIST_APP_CODE_INDEX (8)
47    /// 3. Decodes the hex string to binary
48    pub fn parse_zap_file(zap_data: &[u8]) -> std::io::Result<Vec<u8>> {
49        // Constants from Ruby implementation
50        const WRIST_APP_CODE_INDEX: usize = 8; // Index 8 as in Ruby implementation
51        
52        // Split the ZAP file by the delimiter '\xAC'
53        let mut sections = Vec::new();
54        let mut current_section = Vec::new();
55        let mut i = 0;
56        
57        while i < zap_data.len() {
58            if zap_data[i] == 0xAC {
59                // End of a section
60                if !current_section.is_empty() {
61                    sections.push(current_section);
62                    current_section = Vec::new();
63                }
64                
65                // Skip the delimiter and look for \r\n
66                i += 1;
67                while i < zap_data.len() && !(zap_data[i] == b'\r' && i + 1 < zap_data.len() && zap_data[i + 1] == b'\n') {
68                    i += 1;
69                }
70                
71                // Skip the \r\n if found
72                if i < zap_data.len() && zap_data[i] == b'\r' && i + 1 < zap_data.len() && zap_data[i + 1] == b'\n' {
73                    i += 2; // Skip \r\n
74                }
75            } else {
76                // Add to current section
77                current_section.push(zap_data[i]);
78                i += 1;
79            }
80        }
81        
82        // Add the last section if not empty
83        if !current_section.is_empty() {
84            sections.push(current_section);
85        }
86        
87        // Check if we have enough sections
88        if sections.len() <= WRIST_APP_CODE_INDEX {
89            return Err(std::io::Error::new(
90                std::io::ErrorKind::InvalidData,
91                format!("ZAP file does not contain enough sections, expected at least {}", WRIST_APP_CODE_INDEX + 1)
92            ));
93        }
94        
95        // Get the target section - using the fixed index from Ruby implementation
96        let target_section = &sections[WRIST_APP_CODE_INDEX];
97        
98        // Convert the hex string to binary
99        // In Ruby: [zap_file_data_ascii].pack("H*")
100        let mut result = Vec::new();
101        
102        // Each pair of hex chars becomes one byte
103        let mut i = 0;
104        while i + 1 < target_section.len() {
105            let high = Self::hex_digit_to_value(target_section[i]);
106            let low = Self::hex_digit_to_value(target_section[i + 1]);
107            
108            if let (Some(high), Some(low)) = (high, low) {
109                result.push((high << 4) | low);
110            } else {
111                // If high isn't valid, skip to the next char
112                if high.is_none() {
113                    i += 1;
114                }
115                // If low isn't valid, we've already used high, so proceed as normal
116            }
117            
118            i += 2;
119        }
120        
121        Ok(result)
122    }
123    
124    /// Convert a hex digit (0-9, A-F) to its numeric value
125    fn hex_digit_to_value(digit: u8) -> Option<u8> {
126        match digit {
127            b'0'..=b'9' => Some(digit - b'0'),
128            b'A'..=b'F' => Some(digit - b'A' + 10),
129            b'a'..=b'f' => Some(digit - b'a' + 10),
130            _ => None,
131        }
132    }
133}
134
135impl PacketGenerator for WristApp {
136    fn packets(&self) -> Vec<Vec<u8>> {
137        // Define constants from Ruby implementation
138        const CPACKET_CLEAR: [u8; 2] = [0x93, 0x02];
139        const CPACKET_SECT: [u8; 2] = [0x90, 0x02];
140        const CPACKET_DATA: [u8; 2] = [0x91, 0x02];
141        const CPACKET_END: [u8; 2] = [0x92, 0x02];
142        const CPACKET_DATA_LENGTH: usize = 32;
143        
144        // Create payloads using the cpacket_paginator (as in Ruby)
145        let payloads = cpacket_paginator::paginate_cpackets(
146            &CPACKET_DATA,
147            CPACKET_DATA_LENGTH,
148            &self.wrist_app_data
149        );
150        
151        // Create sect_packet like in Ruby
152        let mut sect_packet = Vec::new();
153        sect_packet.extend_from_slice(&CPACKET_SECT);
154        sect_packet.push(payloads.len() as u8);
155        sect_packet.push(1); // Constant value in protocol 3 (always 1)
156        
157        // Combine all packets in the right order as in Ruby
158        // Ruby: [CPACKET_CLEAR, cpacket_sect] + payloads + [CPACKET_END]
159        let mut all_packets = Vec::with_capacity(payloads.len() + 3);
160        all_packets.push(CPACKET_CLEAR.to_vec());
161        all_packets.push(sect_packet);
162        all_packets.extend(payloads);
163        all_packets.push(CPACKET_END.to_vec());
164        
165        // Wrap with CRC
166        crc_packets_wrapper::wrap_packets_with_crc(all_packets)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use std::path::PathBuf;
174
175    #[test]
176    fn test_wrist_app() {
177        // Use the example ZAP file from fixtures
178        let fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
179            .join("fixtures")
180            .join("EXAMPLE.ZAP");
181            
182        let wrist_app = WristApp::from_zap_file(fixture_path).unwrap();
183        
184        // From golden fixture: wrist_app.jsonl
185        #[rustfmt::skip]
186        let expected = vec![
187          vec![ 5, 147, 2, 48, 253 ],
188          vec![ 7, 144, 2, 5, 1, 144, 251 ],
189          vec![ 38, 145, 2, 1, 49, 53, 48, 32, 100, 97, 116, 97, 58, 32, 76, 111, 114, 101, 109, 32, 105, 112, 115, 117, 109, 32, 100, 111, 108, 111, 114, 32, 115, 105, 116, 32, 211, 127 ],
190          vec![ 38, 145, 2, 2, 97, 109, 101, 116, 44, 32, 99, 111, 110, 115, 101, 99, 116, 101, 116, 117, 114, 32, 97, 100, 105, 112, 105, 115, 99, 105, 110, 103, 32, 101, 108, 105, 63, 42 ],
191          vec![ 38, 145, 2, 3, 116, 44, 32, 115, 101, 100, 32, 100, 111, 32, 101, 105, 117, 115, 109, 111, 100, 32, 116, 101, 109, 112, 111, 114, 32, 105, 110, 99, 105, 100, 105, 100, 140, 40 ],
192          vec![ 38, 145, 2, 4, 117, 110, 116, 32, 117, 116, 32, 108, 97, 98, 111, 114, 101, 32, 101, 116, 32, 100, 111, 108, 111, 114, 101, 32, 109, 97, 103, 110, 97, 32, 97, 108, 167, 146 ],
193          vec![ 11, 145, 2, 5, 105, 113, 117, 97, 46, 102, 103 ],
194          vec![ 5, 146, 2, 160, 252 ]
195        ];
196
197        assert_eq!(wrist_app.packets(), expected);
198    }
199}