kenwood_thd75/memory/
dstar.rs1use crate::protocol::programming;
20use crate::types::dstar::{DstarCallsign, RepeaterDuplex, RepeaterEntry};
21
22const DSTAR_CHANNEL_INFO_OFFSET: usize = 0x03F0;
24
25const DSTAR_CHANNEL_INFO_SIZE: usize = 16;
27
28const DSTAR_RPT_OFFSET: usize = programming::DSTAR_RPT_START as usize * programming::PAGE_SIZE;
30
31const DSTAR_END_OFFSET: usize = programming::BT_START as usize * programming::PAGE_SIZE;
33
34const REPEATER_RECORD_SIZE: usize = 108;
36
37const MAX_REPEATER_ENTRIES: u16 = 1500;
39
40const DSTAR_MY_CALLSIGN_OFFSET: usize = 0x1300;
46
47const RPT_RPT1_OFFSET: usize = 0x00;
53
54const RPT_RPT2_OFFSET: usize = 0x10;
56
57const RPT_NAME_OFFSET: usize = 0x20;
59
60const RPT_AREA_OFFSET: usize = 0x30;
62
63const RPT_FREQ_OFFSET: usize = 0x58;
65
66#[derive(Debug)]
85pub struct DstarAccess<'a> {
86 image: &'a [u8],
87}
88
89impl<'a> DstarAccess<'a> {
90 pub(crate) const fn new(image: &'a [u8]) -> Self {
92 Self { image }
93 }
94
95 #[must_use]
99 pub fn channel_info(&self) -> Option<&[u8]> {
100 let end = DSTAR_CHANNEL_INFO_OFFSET + DSTAR_CHANNEL_INFO_SIZE;
101 if end <= self.image.len() {
102 Some(&self.image[DSTAR_CHANNEL_INFO_OFFSET..end])
103 } else {
104 None
105 }
106 }
107
108 #[must_use]
114 pub fn repeater_callsign_region(&self) -> Option<&[u8]> {
115 if DSTAR_END_OFFSET <= self.image.len() {
116 Some(&self.image[DSTAR_RPT_OFFSET..DSTAR_END_OFFSET])
117 } else {
118 None
119 }
120 }
121
122 #[must_use]
124 pub const fn region_size(&self) -> usize {
125 DSTAR_END_OFFSET - DSTAR_RPT_OFFSET
126 }
127
128 #[must_use]
133 pub fn repeater_record(&self, index: usize) -> Option<&[u8]> {
134 let offset = DSTAR_RPT_OFFSET + index * REPEATER_RECORD_SIZE;
135 let end = offset + REPEATER_RECORD_SIZE;
136 if end <= self.image.len() && end <= DSTAR_END_OFFSET {
137 Some(&self.image[offset..end])
138 } else {
139 None
140 }
141 }
142
143 #[must_use]
148 pub fn read_bytes(&self, offset: usize, len: usize) -> Option<&[u8]> {
149 let end = offset + len;
150 if end <= self.image.len() {
151 Some(&self.image[offset..end])
152 } else {
153 None
154 }
155 }
156
157 #[must_use]
173 pub fn my_callsign(&self) -> String {
174 let offset = DSTAR_MY_CALLSIGN_OFFSET;
175 let end = offset + DstarCallsign::WIRE_LEN;
176 if end > self.image.len() {
177 return String::new();
178 }
179 let slice = &self.image[offset..end];
180 let s = std::str::from_utf8(slice).unwrap_or("");
182 s.trim_end_matches([' ', '\0']).to_owned()
183 }
184
185 #[must_use]
197 pub fn my_callsign_typed(&self) -> Option<DstarCallsign> {
198 let raw = self.my_callsign();
199 if raw.is_empty() {
200 return None;
201 }
202 DstarCallsign::new(&raw)
203 }
204
205 #[must_use]
220 pub fn repeater_entry(&self, index: u16) -> Option<RepeaterEntry> {
221 if index >= MAX_REPEATER_ENTRIES {
222 return None;
223 }
224 let record = self.repeater_record(index as usize)?;
225
226 if record.iter().all(|&b| b == 0xFF) {
228 return None;
229 }
230 if record.iter().all(|&b| b == 0x00) {
232 return None;
233 }
234
235 let rpt1 = extract_dstar_callsign(&record[RPT_RPT1_OFFSET..RPT_RPT1_OFFSET + 8]);
236 let rpt2 = extract_dstar_callsign(&record[RPT_RPT2_OFFSET..RPT_RPT2_OFFSET + 8]);
237 let name = extract_string_field(record, RPT_NAME_OFFSET, 16);
238 let sub_name = extract_string_field(record, RPT_AREA_OFFSET, 16);
239
240 let frequency = if RPT_FREQ_OFFSET + 4 <= record.len() {
241 u32::from_le_bytes([
242 record[RPT_FREQ_OFFSET],
243 record[RPT_FREQ_OFFSET + 1],
244 record[RPT_FREQ_OFFSET + 2],
245 record[RPT_FREQ_OFFSET + 3],
246 ])
247 } else {
248 0
249 };
250
251 Some(RepeaterEntry {
252 group_name: String::new(), name,
254 sub_name,
255 callsign_rpt1: rpt1,
256 gateway_rpt2: rpt2,
257 frequency,
258 duplex: RepeaterDuplex::Minus, offset: 0,
260 module: crate::types::dstar::DstarModule::B,
261 latitude: 0.0,
262 longitude: 0.0,
263 utc_offset: String::new(),
264 position_accuracy: crate::types::dstar::PositionAccuracy::Invalid,
265 lockout: false,
266 })
267 }
268
269 #[must_use]
274 pub fn repeater_count(&self) -> u16 {
275 let mut count: u16 = 0;
276 for i in 0..MAX_REPEATER_ENTRIES {
277 if self.repeater_entry(i).is_some() {
278 count = count.saturating_add(1);
279 }
280 }
281 count
282 }
283}
284
285fn extract_dstar_callsign(slice: &[u8]) -> DstarCallsign {
287 if slice.len() < 8 {
288 return DstarCallsign::default();
289 }
290 let mut bytes = [0u8; 8];
291 bytes.copy_from_slice(&slice[..8]);
292 for b in &mut bytes {
294 if *b == 0 {
295 *b = b' ';
296 }
297 }
298 DstarCallsign::from_wire_bytes(&bytes)
299}
300
301fn extract_string_field(record: &[u8], offset: usize, max_len: usize) -> String {
303 let end = (offset + max_len).min(record.len());
304 if offset >= record.len() {
305 return String::new();
306 }
307 let slice = &record[offset..end];
308 let nul = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
309 String::from_utf8_lossy(&slice[..nul]).trim().to_string()
310}
311
312#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::protocol::programming::TOTAL_SIZE;
320
321 fn make_dstar_image() -> Vec<u8> {
322 vec![0u8; TOTAL_SIZE]
323 }
324
325 #[test]
326 fn dstar_channel_info_accessible() {
327 let mut image = make_dstar_image();
328 image[DSTAR_CHANNEL_INFO_OFFSET..DSTAR_CHANNEL_INFO_OFFSET + 4]
330 .copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
331
332 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
333 let dstar = mi.dstar();
334 let info = dstar.channel_info().unwrap();
335 assert_eq!(info.len(), DSTAR_CHANNEL_INFO_SIZE);
336 assert_eq!(&info[..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
337 }
338
339 #[test]
340 fn dstar_repeater_region_accessible() {
341 let image = make_dstar_image();
342 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
343 let dstar = mi.dstar();
344 let region = dstar.repeater_callsign_region().unwrap();
345 assert!(!region.is_empty());
346 assert_eq!(region.len(), dstar.region_size());
347 }
348
349 #[test]
350 fn dstar_repeater_record() {
351 let mut image = make_dstar_image();
352 let offset = DSTAR_RPT_OFFSET;
354 image[offset..offset + 8].copy_from_slice(b"JR6YPR B");
355
356 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
357 let dstar = mi.dstar();
358 let record = dstar.repeater_record(0).unwrap();
359 assert_eq!(record.len(), REPEATER_RECORD_SIZE);
360 assert_eq!(&record[..8], b"JR6YPR B");
361 }
362
363 #[test]
364 fn dstar_region_size_positive() {
365 let image = make_dstar_image();
366 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
367 let dstar = mi.dstar();
368 assert!(dstar.region_size() > 100_000);
370 }
371
372 #[test]
373 fn dstar_my_callsign() {
374 let mut image = make_dstar_image();
375 image[DSTAR_MY_CALLSIGN_OFFSET..DSTAR_MY_CALLSIGN_OFFSET + 8].copy_from_slice(b"N0CALL ");
376
377 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
378 let dstar = mi.dstar();
379 assert_eq!(dstar.my_callsign(), "N0CALL");
380 }
381
382 #[test]
383 fn dstar_my_callsign_typed() {
384 let mut image = make_dstar_image();
385 image[DSTAR_MY_CALLSIGN_OFFSET..DSTAR_MY_CALLSIGN_OFFSET + 8].copy_from_slice(b"W1AW ");
386
387 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
388 let dstar = mi.dstar();
389 let typed = dstar.my_callsign_typed().unwrap();
390 assert_eq!(typed.as_str(), "W1AW");
391 }
392
393 #[test]
394 fn dstar_my_callsign_empty() {
395 let image = make_dstar_image();
396 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
397 let dstar = mi.dstar();
398 assert_eq!(dstar.my_callsign(), "");
399 assert!(dstar.my_callsign_typed().is_none());
400 }
401
402 #[test]
403 fn dstar_repeater_entry_empty_record() {
404 let image = make_dstar_image();
405 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
406 let dstar = mi.dstar();
407 assert!(dstar.repeater_entry(0).is_none());
409 }
410
411 #[test]
412 fn dstar_repeater_entry_populated() {
413 let mut image = make_dstar_image();
414 let offset = DSTAR_RPT_OFFSET;
415
416 image[offset..offset + 8].copy_from_slice(b"JR6YPR B");
418 image[offset + 0x10..offset + 0x18].copy_from_slice(b"JR6YPR G");
420 let name = b"Test Rptr\0\0\0\0\0\0\0";
422 image[offset + 0x20..offset + 0x30].copy_from_slice(name);
423 let freq: u32 = 439_010_000;
425 image[offset + 0x58..offset + 0x5C].copy_from_slice(&freq.to_le_bytes());
426
427 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
428 let dstar = mi.dstar();
429 let entry = dstar.repeater_entry(0).unwrap();
430 assert_eq!(entry.callsign_rpt1.as_str(), "JR6YPR B");
431 assert_eq!(entry.gateway_rpt2.as_str(), "JR6YPR G");
432 assert_eq!(entry.name, "Test Rptr");
433 assert_eq!(entry.frequency, 439_010_000);
434 }
435
436 #[test]
437 fn dstar_repeater_entry_out_of_range() {
438 let image = make_dstar_image();
439 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
440 let dstar = mi.dstar();
441 assert!(dstar.repeater_entry(MAX_REPEATER_ENTRIES).is_none());
442 }
443
444 #[test]
445 fn dstar_repeater_count_all_empty() {
446 let image = make_dstar_image();
447 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
448 let dstar = mi.dstar();
449 assert_eq!(dstar.repeater_count(), 0);
450 }
451
452 #[test]
453 fn dstar_repeater_count_with_entries() {
454 let mut image = make_dstar_image();
455 for i in 0..3 {
457 let offset = DSTAR_RPT_OFFSET + i * REPEATER_RECORD_SIZE;
458 image[offset..offset + 8].copy_from_slice(b"TESTCALL");
459 }
460
461 let mi = crate::memory::MemoryImage::from_raw(image).unwrap();
462 let dstar = mi.dstar();
463 assert_eq!(dstar.repeater_count(), 3);
464 }
465}