]> code.octet-stream.net Git - m17rt/blob - tools/m17rt-netclient/src/main.rs
Change clap to use derive syntax
[m17rt] / tools / m17rt-netclient / src / main.rs
1 use std::{io::stdin, sync::Arc};
2
3 use clap::Parser;
4 use m17app::{
5 adapter::StreamAdapter,
6 app::M17App,
7 link_setup::M17Address,
8 reflector::{ReflectorClientConfig, ReflectorClientTnc, StatusHandler},
9 };
10 use m17codec2::{rx::Codec2RxAdapter, tx::Codec2TxAdapter};
11
12 #[derive(Parser)]
13 struct Args {
14 #[arg(short = 's', help = "Domain or IP of reflector")]
15 hostname: String,
16 #[arg(
17 short = 'p',
18 default_value = "17000",
19 help = "Reflector listening port"
20 )]
21 port: u16,
22 #[arg(short = 'c', value_parser = valid_callsign, help = "Your callsign for reflector registration and transmissions")]
23 callsign: M17Address,
24 #[arg(short = 'r', value_parser = valid_callsign, help = "Reflector designator/callsign, often starting with 'M17-'")]
25 reflector: M17Address,
26 #[arg(short = 'm', value_parser = valid_module, help = "Module to connect to (A-Z)")]
27 module: char,
28 #[arg(
29 short = 'i',
30 help = "Soundcard name for microphone, otherwise system default"
31 )]
32 input: Option<String>,
33 #[arg(
34 short = 'o',
35 help = "Soundcard name for speaker, otherwise system default"
36 )]
37 output: Option<String>,
38 }
39
40 fn main() {
41 let args = Args::parse();
42
43 // It is current convention that mrefd requires the destination of transmissions to match the reflector.
44 // If you are connected to "M17-XXX" on module B then you must set the dst to "M17-XXX B".
45 // This requirement is likely to change but for the purposes of this test client we'll hard-code the
46 // behaviour for the time being.
47 let ref_with_mod = format!("{} {}", args.reflector, args.module);
48 let Ok(reflector) = M17Address::from_callsign(&ref_with_mod) else {
49 println!(
50 "Unable to create valid destination address for reflector + callsign '{ref_with_mod}'"
51 );
52 std::process::exit(1);
53 };
54
55 let mut tx = Codec2TxAdapter::new(args.callsign.clone(), reflector);
56 if let Some(input) = args.input {
57 tx.set_input_card(input);
58 }
59 let ptt = tx.ptt();
60
61 let mut rx = Codec2RxAdapter::new();
62 if let Some(output) = args.output {
63 rx.set_output_card(output);
64 }
65
66 let config = ReflectorClientConfig {
67 hostname: args.hostname,
68 port: args.port,
69 module: args.module,
70 local_callsign: args.callsign,
71 };
72 let tnc = ReflectorClientTnc::new(config, ConsoleStatusHandler);
73 let app = M17App::new(tnc);
74 app.add_stream_adapter(ConsoleAdapter).unwrap();
75 app.add_stream_adapter(tx).unwrap();
76 app.add_stream_adapter(rx).unwrap();
77 app.start().unwrap();
78
79 println!(">>> PRESS ENTER TO TOGGLE PTT <<<");
80 let mut buf = String::new();
81
82 loop {
83 let _ = stdin().read_line(&mut buf);
84 ptt.set_ptt(true);
85 println!("PTT ON: PRESS ENTER TO END");
86
87 let _ = stdin().read_line(&mut buf);
88 ptt.set_ptt(false);
89 println!("PTT OFF");
90 }
91 }
92
93 fn valid_module(m: &str) -> Result<char, String> {
94 let m = m.to_ascii_uppercase();
95 if m.len() != 1 || !m.chars().next().unwrap().is_alphabetic() {
96 return Err("Module must be a single letter from A to Z".to_owned());
97 }
98 Ok(m.chars().next().unwrap())
99 }
100
101 fn valid_callsign(c: &str) -> Result<M17Address, String> {
102 M17Address::from_callsign(c).map_err(|e| e.to_string())
103 }
104
105 struct ConsoleAdapter;
106 impl StreamAdapter for ConsoleAdapter {
107 fn stream_began(&self, link_setup: m17app::link_setup::LinkSetup) {
108 println!(
109 "Incoming transmission begins. From: {} To: {}",
110 link_setup.source(),
111 link_setup.destination()
112 );
113 }
114
115 fn stream_data(&self, _frame_number: u16, is_final: bool, _data: Arc<[u8; 16]>) {
116 if is_final {
117 println!("Incoming transmission ends.");
118 }
119 }
120 }
121
122 struct ConsoleStatusHandler;
123 impl StatusHandler for ConsoleStatusHandler {
124 fn status_changed(&mut self, status: m17app::reflector::TncStatus) {
125 println!("Client status: {status:?}")
126 }
127 }