1use kenwood_thd75::types::{Band, BatteryLevel, PowerLevel};
19
20#[must_use]
23pub fn band_name(band: Band) -> &'static str {
24 if band == Band::A { "A" } else { "B" }
25}
26
27#[must_use]
33#[allow(clippy::cast_precision_loss)]
34pub fn freq_mhz(hz: u32) -> String {
35 let mhz = f64::from(hz) / 1_000_000.0;
36 let s = format!("{mhz:.6}");
37 let s = s.trim_end_matches('0');
38 let s = if s.ends_with('.') {
39 format!("{s}0")
40 } else {
41 s.to_string()
42 };
43 format!("{s} megahertz")
44}
45
46#[must_use]
48pub fn frequency(band: Band, hz: u32) -> String {
49 format!("Band {} frequency: {}", band_name(band), freq_mhz(hz))
50}
51
52#[must_use]
54pub fn tuned_to(band: Band, hz: u32) -> String {
55 format!("Band {} tuned to {}", band_name(band), freq_mhz(hz))
56}
57
58#[must_use]
60pub fn stepped_up(band: Band, hz: u32) -> String {
61 format!("Band {} stepped up to {}", band_name(band), freq_mhz(hz))
62}
63
64#[must_use]
66pub fn stepped_down(band: Band, hz: u32) -> String {
67 format!("Band {} stepped down to {}", band_name(band), freq_mhz(hz))
68}
69
70#[must_use]
72pub fn step_size_read(band: Band, step_display: &str) -> String {
73 format!("Band {} step size: {step_display}", band_name(band))
74}
75
76#[must_use]
78pub fn step_size_set(band: Band, step_display: &str) -> String {
79 format!("Band {} step size set to {step_display}", band_name(band))
80}
81
82#[must_use]
84pub fn tx_offset(band: Band, hz: u32) -> String {
85 format!("Band {} transmit offset: {hz} hertz", band_name(band))
86}
87
88#[must_use]
91pub fn mode_read(band: Band, mode_display: &str) -> String {
92 format!("Band {} mode: {mode_display}", band_name(band))
93}
94
95#[must_use]
97pub fn error(e: impl std::fmt::Display) -> String {
98 format!("Error: {e}")
99}
100
101#[must_use]
103pub fn warning(e: impl std::fmt::Display) -> String {
104 format!("Warning: {e}")
105}
106
107#[must_use]
109pub fn mode_set(band: Band, mode_display: &str) -> String {
110 format!("Band {} mode set to {mode_display}", band_name(band))
111}
112
113#[must_use]
115pub const fn power_level_display(level: PowerLevel) -> &'static str {
116 match level {
117 PowerLevel::High => "high, 5 watts",
118 PowerLevel::Medium => "medium, 2 watts",
119 PowerLevel::Low => "low, half watt",
120 PowerLevel::ExtraLow => "extra-low, 50 milliwatts",
121 }
122}
123
124#[must_use]
126pub fn power_read(band: Band, level: PowerLevel) -> String {
127 format!(
128 "Band {} power: {}",
129 band_name(band),
130 power_level_display(level)
131 )
132}
133
134#[must_use]
136pub fn power_set(band: Band, level: PowerLevel) -> String {
137 format!(
138 "Band {} power set to {}",
139 band_name(band),
140 power_level_display(level)
141 )
142}
143
144#[must_use]
146pub fn squelch_read(band: Band, level: u8) -> String {
147 format!("Band {} squelch level: {level}", band_name(band))
148}
149
150#[must_use]
152pub fn squelch_set(band: Band, level: u8) -> String {
153 format!("Band {} squelch set to {level}", band_name(band))
154}
155
156#[must_use]
158pub fn smeter(band: Band, reading_display: &str) -> String {
159 format!("Band {} S-meter: {reading_display}", band_name(band))
160}
161
162#[must_use]
164pub const fn battery_level_display(level: BatteryLevel) -> &'static str {
165 match level {
166 BatteryLevel::Empty => "empty",
167 BatteryLevel::OneThird => "one third",
168 BatteryLevel::TwoThirds => "two thirds",
169 BatteryLevel::Full => "full",
170 BatteryLevel::Charging => "charging",
171 }
172}
173
174#[must_use]
176pub const fn on_off(value: bool) -> &'static str {
177 if value { "on" } else { "off" }
178}
179
180#[must_use]
182pub fn radio_model(model: impl std::fmt::Display) -> String {
183 format!("Radio model: {model}")
184}
185
186#[must_use]
188pub fn firmware_version(version: impl std::fmt::Display) -> String {
189 format!("Firmware version: {version}")
190}
191
192#[must_use]
194pub fn battery(level: BatteryLevel) -> String {
195 format!("Battery level: {}", battery_level_display(level))
196}
197
198#[must_use]
200pub fn clock(time: impl std::fmt::Display) -> String {
201 format!("Radio clock: {time}")
202}
203
204#[must_use]
206pub fn key_lock(locked: bool) -> String {
207 format!("Key lock: {}", on_off(locked))
208}
209
210#[must_use]
212pub fn bluetooth(enabled: bool) -> String {
213 format!("Bluetooth: {}", on_off(enabled))
214}
215
216#[must_use]
218pub fn dual_band(enabled: bool) -> String {
219 format!("Dual band: {}", on_off(enabled))
220}
221
222#[must_use]
224pub fn attenuator(band: Band, enabled: bool) -> String {
225 format!("Band {} attenuator: {}", band_name(band), on_off(enabled))
226}
227
228#[must_use]
230pub fn vox(enabled: bool) -> String {
231 format!("VOX: {}", on_off(enabled))
232}
233
234#[must_use]
236pub fn vox_set(enabled: bool) -> String {
237 format!("VOX set to {}", on_off(enabled))
238}
239
240#[must_use]
242pub fn vox_gain_read(level: u8) -> String {
243 format!("VOX gain: {level}")
244}
245
246#[must_use]
248pub fn vox_gain_set(level: u8) -> String {
249 format!("VOX gain set to {level}")
250}
251
252#[must_use]
254pub fn vox_delay_read(level: u8) -> String {
255 format!("VOX delay: {level}")
256}
257
258#[must_use]
260pub fn vox_delay_set(level: u8) -> String {
261 format!("VOX delay set to {level}")
262}
263
264#[must_use]
266pub fn fm_radio(enabled: bool) -> String {
267 format!("FM radio: {}", on_off(enabled))
268}
269
270#[must_use]
272pub fn fm_radio_set(enabled: bool) -> String {
273 format!("FM radio set to {}", on_off(enabled))
274}
275
276#[must_use]
278pub fn channel_read(number: u16, hz: u32) -> String {
279 format!("Channel {number}: {}", freq_mhz(hz))
280}
281
282#[must_use]
284pub fn channels_reading(start: u16, end_inclusive: u16) -> String {
285 format!("Reading channels {start} through {end_inclusive}, please wait.")
286}
287
288#[must_use]
291pub fn channels_summary(count: usize) -> String {
292 if count == 0 {
293 "No programmed channels in that range.".to_string()
294 } else {
295 format!("{count} programmed channels found.")
296 }
297}
298
299#[must_use]
301pub fn gps_config(gps_on: bool, pc_on: bool) -> String {
302 format!("GPS: {}, PC output: {}", on_off(gps_on), on_off(pc_on))
303}
304
305#[must_use]
307pub fn urcall_read(call: &str, suffix: &str) -> String {
308 if suffix.is_empty() {
309 format!("Destination callsign: {call}")
310 } else {
311 format!("Destination callsign: {call} suffix {suffix}")
312 }
313}
314
315#[must_use]
317pub fn urcall_set(call: &str) -> String {
318 format!("Destination callsign set to {call}")
319}
320
321#[must_use]
323pub const fn cq_set() -> &'static str {
324 "Destination set to CQCQCQ"
325}
326
327#[must_use]
329pub fn reflector_connected(name: &str, module: char) -> String {
330 format!("Connected to {name} module {module}")
331}
332
333#[must_use]
335pub const fn reflector_disconnected() -> &'static str {
336 "Disconnected from reflector"
337}
338
339#[must_use]
345pub fn aprs_station_heard(callsign: &str) -> String {
346 format!("APRS station heard: {callsign}.")
347}
348
349#[must_use]
351pub fn aprs_message_received(addressee: &str, text: &str) -> String {
352 format!("APRS message received for {addressee}: {text}.")
353}
354
355#[must_use]
357pub fn aprs_message_delivered(id: &str) -> String {
358 format!("APRS message delivered, ID {id}.")
359}
360
361#[must_use]
363pub fn aprs_message_rejected(id: &str) -> String {
364 format!("APRS message rejected by remote station, ID {id}.")
365}
366
367#[must_use]
369pub fn aprs_message_expired(id: &str) -> String {
370 format!("APRS message expired after all retries, ID {id}.")
371}
372
373#[must_use]
375pub fn aprs_position(source: &str, lat: f64, lon: f64) -> String {
376 format!("APRS position from {source}: latitude {lat:.4}, longitude {lon:.4}.")
377}
378
379#[must_use]
381pub fn aprs_weather(source: &str) -> String {
382 format!("APRS weather report from {source}.")
383}
384
385#[must_use]
387pub fn aprs_digipeated(source: &str) -> String {
388 format!("APRS packet relayed from {source}.")
389}
390
391#[must_use]
397pub fn aprs_query_responded(to: &str) -> String {
398 format!("APRS position query from {to}, responded with beacon.")
399}
400
401#[must_use]
403pub fn aprs_raw_packet(source: &str) -> String {
404 format!("APRS packet from {source}.")
405}
406
407#[must_use]
409pub const fn aprs_mode_active() -> &'static str {
410 "APRS mode active. Type aprs stop to exit."
411}
412
413#[must_use]
419pub const fn aprs_is_connected() -> &'static str {
420 "APRS-IS connected. Forwarding RF to internet. Press Ctrl-C to stop."
421}
422
423#[must_use]
428pub fn aprs_is_incoming(line: &str) -> String {
429 format!("APRS-IS incoming: {line}")
430}
431
432#[must_use]
434pub fn aprs_station_entry(
435 callsign: &str,
436 position: Option<(f64, f64)>,
437 packet_count: u32,
438 elapsed_display: &str,
439) -> String {
440 let pos = match position {
441 Some((lat, lon)) => format!(" at {lat:.4}, {lon:.4}"),
442 None => String::new(),
443 };
444 format!("Station {callsign}{pos}, {packet_count} packets, heard {elapsed_display} ago.")
445}
446
447#[must_use]
449pub fn aprs_stations_summary(count: usize) -> String {
450 format!("{count} stations heard.")
451}
452
453#[must_use]
459pub fn dstar_voice_start(my_call: &str, my_suffix: &str, ur_call: &str) -> String {
460 let suffix_part = if my_suffix.trim().is_empty() {
461 String::new()
462 } else {
463 format!(" /{}", my_suffix.trim())
464 };
465 format!(
466 "D-STAR voice from {}{suffix_part}, to {}.",
467 my_call.trim(),
468 ur_call.trim()
469 )
470}
471
472#[must_use]
474pub const fn dstar_voice_end() -> &'static str {
475 "D-STAR voice transmission ended."
476}
477
478#[must_use]
480pub const fn dstar_voice_lost() -> &'static str {
481 "D-STAR voice signal lost, no clean end of transmission."
482}
483
484#[must_use]
486pub fn dstar_text_message(text: &str) -> String {
487 format!("D-STAR message: \"{text}\"")
488}
489
490#[must_use]
492pub fn dstar_gps(text: &str) -> String {
493 if text.trim().is_empty() {
494 "D-STAR GPS position data received.".to_string()
495 } else {
496 format!("D-STAR GPS data: {text}")
497 }
498}
499
500#[must_use]
502pub fn dstar_station_heard(callsign: &str) -> String {
503 format!("D-STAR station heard: {callsign}.")
504}
505
506#[must_use]
508pub const fn dstar_command_cq() -> &'static str {
509 "D-STAR command: call CQ."
510}
511
512#[must_use]
514pub const fn dstar_command_echo() -> &'static str {
515 "D-STAR command: echo test."
516}
517
518#[must_use]
520pub const fn dstar_command_unlink() -> &'static str {
521 "D-STAR command: unlink reflector."
522}
523
524#[must_use]
526pub const fn dstar_command_info() -> &'static str {
527 "D-STAR command: request info."
528}
529
530#[must_use]
532pub fn dstar_command_link(reflector: &str, module: char) -> String {
533 format!("D-STAR command: link to {reflector} module {module}.")
534}
535
536#[must_use]
538pub fn dstar_command_callsign(call: &str) -> String {
539 format!("D-STAR command: route to callsign {call}.")
540}
541
542#[must_use]
544pub fn dstar_modem_status(buffer: u8, tx_active: bool) -> String {
545 format!(
546 "D-STAR modem: buffer {buffer}, transmit {}.",
547 if tx_active { "active" } else { "idle" }
548 )
549}
550
551#[must_use]
555pub const fn reflector_event_connected() -> &'static str {
556 "Reflector: connected."
557}
558
559#[must_use]
561pub const fn reflector_event_rejected() -> &'static str {
562 "Reflector: connection rejected."
563}
564
565#[must_use]
567pub const fn reflector_event_disconnected() -> &'static str {
568 "Reflector: disconnected."
569}
570
571#[must_use]
573pub fn reflector_event_voice_start(my_call: &str, my_suffix: &str, ur_call: &str) -> String {
574 let suffix_part = if my_suffix.is_empty() {
575 String::new()
576 } else {
577 format!(" /{my_suffix}")
578 };
579 format!("Reflector: voice from {my_call}{suffix_part}, to {ur_call}.")
580}
581
582#[must_use]
584pub const fn reflector_event_voice_end() -> &'static str {
585 "Reflector: voice transmission ended."
586}
587
588#[must_use]
594pub fn startup_banner(version: &str) -> String {
595 format!("Kenwood TH-D75 accessible radio control, version {version}.")
596}
597
598#[must_use]
600pub fn connected_via(path: &str) -> String {
601 format!("Connected via {path}.")
602}
603
604#[must_use]
606pub const fn goodbye() -> &'static str {
607 "Goodbye."
608}
609
610#[must_use]
612pub const fn type_help_hint() -> &'static str {
613 "Type help for a list of commands, or quit to exit."
614}
615
616#[must_use]
618pub fn startup_identified(model: &str, firmware: &str) -> String {
619 format!("Radio model: {model}. Firmware version: {firmware}.")
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use crate::lint;
626 use kenwood_thd75::types::Band;
627
628 fn assert_lint(s: &str) {
629 lint::check_output(s).unwrap_or_else(|v| {
630 panic!("line {s:?} failed lint: {v:?}");
631 });
632 }
633
634 #[test]
635 fn freq_mhz_standard() {
636 assert_eq!(freq_mhz(146_520_000), "146.52 megahertz");
637 assert_eq!(freq_mhz(446_000_000), "446.0 megahertz");
638 assert_eq!(freq_mhz(0), "0.0 megahertz");
639 }
640
641 #[test]
642 fn freq_mhz_high_band() {
643 assert_eq!(freq_mhz(1_200_000_000), "1200.0 megahertz");
644 assert_eq!(freq_mhz(1_300_000_000), "1300.0 megahertz");
645 }
646
647 #[test]
648 fn frequency_band_a() {
649 let s = frequency(Band::A, 146_520_000);
650 assert_eq!(s, "Band A frequency: 146.52 megahertz");
651 assert_lint(&s);
652 }
653
654 #[test]
655 fn frequency_band_b() {
656 let s = frequency(Band::B, 446_000_000);
657 assert_eq!(s, "Band B frequency: 446.0 megahertz");
658 assert_lint(&s);
659 }
660
661 #[test]
662 fn tuned_to_band_a() {
663 let s = tuned_to(Band::A, 146_520_000);
664 assert_eq!(s, "Band A tuned to 146.52 megahertz");
665 assert_lint(&s);
666 }
667
668 #[test]
669 fn stepped_up_and_down() {
670 let up = stepped_up(Band::A, 146_525_000);
671 let down = stepped_down(Band::A, 146_515_000);
672 assert_eq!(up, "Band A stepped up to 146.525 megahertz");
673 assert_eq!(down, "Band A stepped down to 146.515 megahertz");
674 assert_lint(&up);
675 assert_lint(&down);
676 }
677
678 #[test]
679 fn step_size_formats() {
680 let s = step_size_read(Band::A, "25 kilohertz");
681 assert_eq!(s, "Band A step size: 25 kilohertz");
682 assert_lint(&s);
683 }
684
685 #[test]
686 fn step_size_set_format() {
687 let s = step_size_set(Band::B, "12.5 kilohertz");
688 assert_eq!(s, "Band B step size set to 12.5 kilohertz");
689 assert_lint(&s);
690 }
691
692 #[test]
693 fn error_prefix() {
694 let s = error("invalid frequency");
695 assert_eq!(s, "Error: invalid frequency");
696 assert_lint(&s);
697 }
698
699 #[test]
700 fn warning_prefix() {
701 let s = warning("could not detect time zone");
702 assert_eq!(s, "Warning: could not detect time zone");
703 assert_lint(&s);
704 }
705
706 #[test]
707 fn tx_offset_formatted() {
708 let s = tx_offset(Band::A, 600_000);
709 assert_eq!(s, "Band A transmit offset: 600000 hertz");
710 assert_lint(&s);
711 }
712
713 #[test]
714 fn mode_read_basic() {
715 let s = mode_read(Band::A, "FM");
716 assert_eq!(s, "Band A mode: FM");
717 assert_lint(&s);
718 }
719
720 #[test]
721 fn mode_read_and_set() {
722 let r = mode_read(Band::A, "FM");
723 let s = mode_set(Band::B, "DV");
724 assert_eq!(r, "Band A mode: FM");
725 assert_eq!(s, "Band B mode set to DV");
726 assert_lint(&r);
727 assert_lint(&s);
728 }
729
730 #[test]
731 fn power_read_all_levels() {
732 use kenwood_thd75::types::PowerLevel;
733 assert_eq!(
734 power_read(Band::A, PowerLevel::High),
735 "Band A power: high, 5 watts"
736 );
737 assert_eq!(
738 power_read(Band::A, PowerLevel::Medium),
739 "Band A power: medium, 2 watts"
740 );
741 assert_eq!(
742 power_read(Band::A, PowerLevel::Low),
743 "Band A power: low, half watt"
744 );
745 assert_eq!(
746 power_read(Band::A, PowerLevel::ExtraLow),
747 "Band A power: extra-low, 50 milliwatts"
748 );
749 assert_lint(&power_read(Band::A, PowerLevel::High));
750 assert_lint(&power_read(Band::A, PowerLevel::ExtraLow));
751 }
752
753 #[test]
754 fn power_set_lints() {
755 use kenwood_thd75::types::PowerLevel;
756 let s = power_set(Band::B, PowerLevel::Medium);
757 assert_eq!(s, "Band B power set to medium, 2 watts");
758 assert_lint(&s);
759 }
760
761 #[test]
762 fn squelch_read_and_set() {
763 let r = squelch_read(Band::A, 3);
764 let s = squelch_set(Band::A, 5);
765 assert_eq!(r, "Band A squelch level: 3");
766 assert_eq!(s, "Band A squelch set to 5");
767 assert_lint(&r);
768 assert_lint(&s);
769 }
770
771 #[test]
772 fn smeter_format() {
773 let s = smeter(Band::A, "S0");
774 assert_eq!(s, "Band A S-meter: S0");
775 assert_lint(&s);
776 }
777
778 #[test]
779 fn battery_all_levels() {
780 use kenwood_thd75::types::BatteryLevel;
781 let cases = [
782 (BatteryLevel::Empty, "Battery level: empty"),
783 (BatteryLevel::OneThird, "Battery level: one third"),
784 (BatteryLevel::TwoThirds, "Battery level: two thirds"),
785 (BatteryLevel::Full, "Battery level: full"),
786 (BatteryLevel::Charging, "Battery level: charging"),
787 ];
788 for (level, expected) in cases {
789 let s = battery(level);
790 assert_eq!(s, expected);
791 assert_lint(&s);
792 }
793 }
794
795 #[test]
796 fn radio_model_format() {
797 let s = radio_model("TH-D75");
798 assert_eq!(s, "Radio model: TH-D75");
799 assert_lint(&s);
800 }
801
802 #[test]
803 fn firmware_version_format() {
804 let s = firmware_version("1.03");
805 assert_eq!(s, "Firmware version: 1.03");
806 assert_lint(&s);
807 }
808
809 #[test]
810 fn clock_format() {
811 let s = clock("2026-04-10 14:32:07");
812 assert_eq!(s, "Radio clock: 2026-04-10 14:32:07");
813 assert_lint(&s);
814 }
815
816 #[test]
817 fn booleans_as_words() {
818 assert_eq!(key_lock(true), "Key lock: on");
819 assert_eq!(key_lock(false), "Key lock: off");
820 assert_eq!(bluetooth(true), "Bluetooth: on");
821 assert_eq!(dual_band(false), "Dual band: off");
822 assert_lint(&key_lock(true));
823 assert_lint(&bluetooth(false));
824 assert_lint(&dual_band(true));
825 }
826
827 #[test]
828 fn attenuator_format() {
829 let s = attenuator(Band::A, true);
830 assert_eq!(s, "Band A attenuator: on");
831 assert_lint(&s);
832 let s = attenuator(Band::B, false);
833 assert_eq!(s, "Band B attenuator: off");
834 assert_lint(&s);
835 }
836
837 #[test]
838 fn vox_formats() {
839 assert_eq!(vox(true), "VOX: on");
840 assert_eq!(vox_set(false), "VOX set to off");
841 assert_eq!(vox_gain_read(5), "VOX gain: 5");
842 assert_eq!(vox_gain_set(7), "VOX gain set to 7");
843 assert_eq!(vox_delay_read(3), "VOX delay: 3");
844 assert_eq!(vox_delay_set(1), "VOX delay set to 1");
845 assert_lint(&vox(true));
846 assert_lint(&vox_set(true));
847 assert_lint(&vox_gain_set(9));
848 assert_lint(&vox_delay_read(6));
849 }
850
851 #[test]
852 fn fm_radio_formats() {
853 assert_eq!(fm_radio(true), "FM radio: on");
854 assert_eq!(fm_radio_set(false), "FM radio set to off");
855 assert_lint(&fm_radio(true));
856 assert_lint(&fm_radio_set(false));
857 }
858
859 #[test]
860 fn channel_read_format() {
861 let s = channel_read(5, 146_520_000);
862 assert_eq!(s, "Channel 5: 146.52 megahertz");
863 assert_lint(&s);
864 }
865
866 #[test]
867 fn channels_reading_format() {
868 let s = channels_reading(0, 19);
869 assert_eq!(s, "Reading channels 0 through 19, please wait.");
870 assert_lint(&s);
871 }
872
873 #[test]
874 fn channels_summary_non_empty() {
875 let s = channels_summary(3);
876 assert_eq!(s, "3 programmed channels found.");
877 assert_lint(&s);
878 }
879
880 #[test]
881 fn channels_summary_empty() {
882 let s = channels_summary(0);
883 assert_eq!(s, "No programmed channels in that range.");
884 assert_lint(&s);
885 }
886
887 #[test]
888 fn gps_config_format() {
889 assert_eq!(gps_config(true, true), "GPS: on, PC output: on");
890 assert_eq!(gps_config(false, true), "GPS: off, PC output: on");
891 assert_eq!(gps_config(true, false), "GPS: on, PC output: off");
892 assert_eq!(gps_config(false, false), "GPS: off, PC output: off");
893 assert_lint(&gps_config(true, true));
894 }
895
896 #[test]
897 fn urcall_read_with_and_without_suffix() {
898 assert_eq!(urcall_read("W1AW", ""), "Destination callsign: W1AW");
899 assert_eq!(
900 urcall_read("W1AW", "P"),
901 "Destination callsign: W1AW suffix P"
902 );
903 assert_lint(&urcall_read("W1AW", ""));
904 assert_lint(&urcall_read("W1AW", "P"));
905 }
906
907 #[test]
908 fn urcall_set_format() {
909 assert_eq!(urcall_set("W1AW"), "Destination callsign set to W1AW");
910 assert_lint(&urcall_set("W1AW"));
911 }
912
913 #[test]
914 fn cq_and_reflector_strings() {
915 assert_eq!(cq_set(), "Destination set to CQCQCQ");
916 assert_eq!(
917 reflector_connected("REF030", 'C'),
918 "Connected to REF030 module C"
919 );
920 assert_eq!(reflector_disconnected(), "Disconnected from reflector");
921 assert_lint(cq_set());
922 assert_lint(&reflector_connected("REF030", 'C'));
923 assert_lint(reflector_disconnected());
924 }
925
926 #[test]
927 fn aprs_events_all_pass_lint() {
928 let cases = vec![
929 aprs_station_heard("W1AW"),
930 aprs_message_received("W1AW", "Hello there"),
931 aprs_message_delivered("42"),
932 aprs_message_rejected("43"),
933 aprs_message_expired("44"),
934 aprs_position("W1AW", 35.3, -82.46),
935 aprs_weather("W1AW"),
936 aprs_digipeated("W1AW"),
937 aprs_query_responded("W1AW"),
938 aprs_raw_packet("W1AW"),
939 aprs_mode_active().to_string(),
940 aprs_is_connected().to_string(),
941 aprs_is_incoming("W1AW-7>APRS:hello"),
942 aprs_station_entry("W1AW", Some((35.3, -82.46)), 12, "2 minutes"),
943 aprs_station_entry("W1AW", None, 1, "15 seconds"),
944 aprs_stations_summary(5),
945 ];
946 for s in &cases {
947 assert_lint(s);
948 }
949 }
950
951 #[test]
952 fn aprs_position_format() {
953 let s = aprs_position("W1AW", 35.3, -82.46);
954 assert_eq!(
955 s,
956 "APRS position from W1AW: latitude 35.3000, longitude -82.4600."
957 );
958 }
959
960 #[test]
961 fn aprs_is_connected_has_no_unicode() {
962 let s = aprs_is_connected();
963 assert!(!s.contains('\u{2194}'), "must not contain left-right arrow");
964 assert!(!s.contains('\u{2192}'), "must not contain right arrow");
965 assert_lint(s);
966 }
967
968 #[test]
969 fn aprs_query_responded_uses_comma_not_em_dash() {
970 let s = aprs_query_responded("W1AW");
971 assert!(!s.contains('\u{2014}'), "must not contain em dash");
972 assert_eq!(s, "APRS position query from W1AW, responded with beacon.");
973 assert_lint(&s);
974 }
975
976 #[test]
977 fn aprs_is_incoming_format() {
978 let s = aprs_is_incoming("W1AW>APRS:hello");
979 assert_eq!(s, "APRS-IS incoming: W1AW>APRS:hello");
980 assert_lint(&s);
981 }
982
983 #[test]
984 fn aprs_station_entry_with_position() {
985 let s = aprs_station_entry("W1AW", Some((35.3, -82.46)), 12, "2 minutes");
986 assert_eq!(
987 s,
988 "Station W1AW at 35.3000, -82.4600, 12 packets, heard 2 minutes ago."
989 );
990 }
991
992 #[test]
993 fn aprs_station_entry_without_position() {
994 let s = aprs_station_entry("W1AW", None, 1, "15 seconds");
995 assert_eq!(s, "Station W1AW, 1 packets, heard 15 seconds ago.");
996 }
997
998 #[test]
999 fn dstar_events_pass_lint() {
1000 let cases: Vec<String> = vec![
1001 dstar_voice_start("W1AW", "P", "CQCQCQ"),
1002 dstar_voice_start("W1AW", "", "W9ABC"),
1003 dstar_voice_end().to_string(),
1004 dstar_voice_lost().to_string(),
1005 dstar_text_message("Hello from W1AW"),
1006 dstar_gps(""),
1007 dstar_gps("$GPGGA,..."),
1008 dstar_station_heard("W1AW"),
1009 dstar_command_cq().to_string(),
1010 dstar_command_echo().to_string(),
1011 dstar_command_unlink().to_string(),
1012 dstar_command_info().to_string(),
1013 dstar_command_link("REF030", 'C'),
1014 dstar_command_callsign("W1AW"),
1015 dstar_modem_status(5, false),
1016 dstar_modem_status(0, true),
1017 reflector_event_connected().to_string(),
1018 reflector_event_rejected().to_string(),
1019 reflector_event_disconnected().to_string(),
1020 reflector_event_voice_start("W1AW", "", "CQCQCQ"),
1021 reflector_event_voice_start("W1AW", "P", "CQCQCQ"),
1022 reflector_event_voice_end().to_string(),
1023 ];
1024 for s in &cases {
1025 assert_lint(s);
1026 }
1027 }
1028
1029 #[test]
1030 fn dstar_voice_start_with_suffix() {
1031 let s = dstar_voice_start("W1AW", "P", "CQCQCQ");
1032 assert_eq!(s, "D-STAR voice from W1AW /P, to CQCQCQ.");
1033 }
1034
1035 #[test]
1036 fn dstar_voice_start_without_suffix() {
1037 let s = dstar_voice_start("W1AW", "", "CQCQCQ");
1038 assert_eq!(s, "D-STAR voice from W1AW, to CQCQCQ.");
1039 }
1040
1041 #[test]
1042 fn dstar_gps_empty_and_with_text() {
1043 assert_eq!(dstar_gps(""), "D-STAR GPS position data received.");
1044 assert_eq!(dstar_gps("$GPGGA,..."), "D-STAR GPS data: $GPGGA,...");
1045 }
1046
1047 #[test]
1048 fn dstar_modem_status_formats() {
1049 assert_eq!(
1050 dstar_modem_status(5, false),
1051 "D-STAR modem: buffer 5, transmit idle."
1052 );
1053 assert_eq!(
1054 dstar_modem_status(0, true),
1055 "D-STAR modem: buffer 0, transmit active."
1056 );
1057 }
1058
1059 #[test]
1060 fn startup_strings_lint() {
1061 assert_lint(&startup_banner("0.1.0"));
1062 assert_lint(&connected_via("/dev/cu.usbmodem1234"));
1063 assert_lint(goodbye());
1064 assert_lint(type_help_hint());
1065 assert_lint(&startup_identified("TH-D75", "1.03"));
1066 }
1067
1068 #[test]
1069 fn startup_banner_format() {
1070 assert_eq!(
1071 startup_banner("0.1.0"),
1072 "Kenwood TH-D75 accessible radio control, version 0.1.0."
1073 );
1074 }
1075
1076 #[test]
1077 fn connected_via_format() {
1078 assert_eq!(
1079 connected_via("/dev/cu.usbmodem1234"),
1080 "Connected via /dev/cu.usbmodem1234."
1081 );
1082 }
1083
1084 #[test]
1085 fn goodbye_and_hint_format() {
1086 assert_eq!(goodbye(), "Goodbye.");
1087 assert_eq!(
1088 type_help_hint(),
1089 "Type help for a list of commands, or quit to exit."
1090 );
1091 }
1092
1093 #[test]
1094 fn startup_identified_format() {
1095 assert_eq!(
1096 startup_identified("TH-D75", "1.03"),
1097 "Radio model: TH-D75. Firmware version: 1.03."
1098 );
1099 }
1100
1101 proptest::proptest! {
1102 #[test]
1103 fn frequency_always_lints(hz in 0u32..=1_300_000_000u32) {
1104 let s = frequency(Band::A, hz);
1105 lint::check_line(&s).unwrap_or_else(|v| panic!("{s:?}: {v:?}"));
1106 proptest::prop_assert!(s.chars().count() <= 80);
1107 }
1108 }
1109}