kenwood_thd75/sdcard/
qso_log.rs1use super::SdCardError;
18
19const EXPECTED_COLUMNS: usize = 24;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct QsoEntry {
28 pub tx_rx: String,
30 pub date: String,
32 pub frequency: String,
34 pub mode: String,
36 pub my_latitude: String,
38 pub my_longitude: String,
40 pub my_altitude: String,
42 pub rf_power: String,
44 pub s_meter: String,
46 pub caller: String,
48 pub memo: String,
50 pub called: String,
52 pub rpt1: String,
54 pub rpt2: String,
56 pub message: String,
58 pub repeater_control: String,
60 pub bk: String,
62 pub emr: String,
64 pub fast_data: String,
66 pub latitude: String,
68 pub longitude: String,
70 pub altitude: String,
72 pub course: String,
74 pub speed: String,
76}
77
78pub fn parse_qso_log(data: &[u8]) -> Result<Vec<QsoEntry>, SdCardError> {
89 let text = String::from_utf8_lossy(data);
90 let mut entries = Vec::new();
91
92 for (line_idx, line) in text.lines().enumerate() {
93 if line_idx == 0 || line.trim().is_empty() {
95 continue;
96 }
97
98 let line_num = line_idx + 1;
99 let cols: Vec<&str> = line.split('\t').collect();
100 if cols.len() < EXPECTED_COLUMNS {
101 return Err(SdCardError::ColumnCount {
102 line: line_num,
103 expected: EXPECTED_COLUMNS,
104 actual: cols.len(),
105 });
106 }
107
108 entries.push(QsoEntry {
109 tx_rx: cols[0].to_owned(),
110 date: cols[1].to_owned(),
111 frequency: cols[2].to_owned(),
112 mode: cols[3].to_owned(),
113 my_latitude: cols[4].to_owned(),
114 my_longitude: cols[5].to_owned(),
115 my_altitude: cols[6].to_owned(),
116 rf_power: cols[7].to_owned(),
117 s_meter: cols[8].to_owned(),
118 caller: cols[9].to_owned(),
119 memo: cols[10].to_owned(),
120 called: cols[11].to_owned(),
121 rpt1: cols[12].to_owned(),
122 rpt2: cols[13].to_owned(),
123 message: cols[14].to_owned(),
124 repeater_control: cols[15].to_owned(),
125 bk: cols[16].to_owned(),
126 emr: cols[17].to_owned(),
127 fast_data: cols[18].to_owned(),
128 latitude: cols[19].to_owned(),
129 longitude: cols[20].to_owned(),
130 altitude: cols[21].to_owned(),
131 course: cols[22].to_owned(),
132 speed: cols[23].to_owned(),
133 });
134 }
135
136 Ok(entries)
137}
138
139#[must_use]
144pub fn write_qso_log(entries: &[QsoEntry]) -> Vec<u8> {
145 let mut text = String::new();
146
147 text.push_str(
149 "TX/RX\tDate\tFrequency\tMode\t\
150 My Latitude\tMy Longitude\tMy Altitude\t\
151 RF Power\tS Meter\tCaller\tMemo\tCalled\t\
152 RPT1\tRPT2\tMessage\tRepeater Control\t\
153 BK\tEMR\tFast Data\t\
154 Latitude\tLongitude\tAltitude\tCourse\tSpeed\r\n",
155 );
156
157 for e in entries {
159 text.push_str(&e.tx_rx);
160 text.push('\t');
161 text.push_str(&e.date);
162 text.push('\t');
163 text.push_str(&e.frequency);
164 text.push('\t');
165 text.push_str(&e.mode);
166 text.push('\t');
167 text.push_str(&e.my_latitude);
168 text.push('\t');
169 text.push_str(&e.my_longitude);
170 text.push('\t');
171 text.push_str(&e.my_altitude);
172 text.push('\t');
173 text.push_str(&e.rf_power);
174 text.push('\t');
175 text.push_str(&e.s_meter);
176 text.push('\t');
177 text.push_str(&e.caller);
178 text.push('\t');
179 text.push_str(&e.memo);
180 text.push('\t');
181 text.push_str(&e.called);
182 text.push('\t');
183 text.push_str(&e.rpt1);
184 text.push('\t');
185 text.push_str(&e.rpt2);
186 text.push('\t');
187 text.push_str(&e.message);
188 text.push('\t');
189 text.push_str(&e.repeater_control);
190 text.push('\t');
191 text.push_str(&e.bk);
192 text.push('\t');
193 text.push_str(&e.emr);
194 text.push('\t');
195 text.push_str(&e.fast_data);
196 text.push('\t');
197 text.push_str(&e.latitude);
198 text.push('\t');
199 text.push_str(&e.longitude);
200 text.push('\t');
201 text.push_str(&e.altitude);
202 text.push('\t');
203 text.push_str(&e.course);
204 text.push('\t');
205 text.push_str(&e.speed);
206 text.push_str("\r\n");
207 }
208
209 text.into_bytes()
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn parse_empty_log() {
218 let data = b"TX/RX\tDate\tFrequency\tMode\t\
219 My Latitude\tMy Longitude\tMy Altitude\t\
220 RF Power\tS Meter\tCaller\tMemo\tCalled\t\
221 RPT1\tRPT2\tMessage\tRepeater Control\t\
222 BK\tEMR\tFast Data\t\
223 Latitude\tLongitude\tAltitude\tCourse\tSpeed\r\n";
224 let entries = parse_qso_log(data).unwrap();
225 assert!(entries.is_empty());
226 }
227
228 #[test]
229 fn parse_too_few_columns() {
230 let data = b"TX/RX\tDate\tFrequency\n\
231 TX\t2026/03/28\n";
232 let err = parse_qso_log(data).unwrap_err();
233 assert!(matches!(err, SdCardError::ColumnCount { line: 2, .. }));
234 }
235}