]> code.octet-stream.net Git - m17rt/blob - m17app/README.md
Support i16 and i32
[m17rt] / m17app / README.md
1 # m17app
2
3 Part of the [M17 Rust Toolkit](https://octet-stream.net/p/m17rt/). This crate provides a high-level API for working with the [M17 digital radio protocol](https://m17project.org/). It is designed for building radio software that runs on regular PCs or equivalently powerful devices like a smartphone or Raspberry Pi. You can either point it at an external TNC, or activate the built-in soundmodem to use a standard soundcard and serial PTT.
4
5 `m17app` can be considered an easy-to-use wrapper around `m17core`, a separate crate which provides all the modem and TNC functions.
6
7 ## Creating an `M17App`
8
9 The most important type is `M17App`. This is what your program can use to transmit packets and streams, or to subscribe to incoming packets and streams. To create an `M17App` you must provide it with a TNC, which is any type that implements the trait `Tnc`. This could be a `TcpStream` to another TNC device exposed to the network or it could be an instance of the built-in `Soundmodem`. To connect to reflector like `mrefd` you can use the provided `ReflectorClientTnc`.
10
11 ## Creating a `Soundmodem`
12
13 A `Soundmodem` can use soundcards in your computer to send and receive M17 baseband signals via a radio. More generally it can accept input samples from any compatible source, and provide output samples to any compatible sink, and it will coordinate the modem and TNC in realtime in a background thread.
14
15 A `Soundmodem` requires four parameters:
16
17 * **Input source** - the signal we are receiving
18 * **Output sink** - somewhere to send the modulated signal we want to transmit
19 * **PTT** - a transmit switch that can be turned on or off
20 * **Error handler** - a callback that tells you if problems occur during operation
21
22 These are all traits that you can implement yourself but you can probably use one of the types already included in `m17app`.
23
24 Provided inputs:
25
26 * `Soundcard` - Once you have initialised a card, call `input()` to get an input source handle to provide to the `Soundmodem`.
27 * `RtlSdr` - Receive using an RTL-SDR dongle. This requires that the `rtl_fm` utility is installed and present in your path.
28 * `InputRrcFile` - Read from an M17 `.rrc` file on disk, which contains shaped baseband data as 16-bit LE 48 kHz samples.
29 * `NullInputSource` - Fake device that provides a continuous stream of silence.
30
31 Provided outputs:
32
33 * `Soundcard` - Once you have initialised a card, call `output()` to get an output sink handle.
34 * `OutputRrcFile` - Write transmissions to a `.rrc` on disk.
35 * `NullOutputSink` - Fake device that will swallow any samples it is given.
36
37 Provided PTTs:
38
39 * `SerialPtt` - Use a serial/COM port with either the RTS or DTR pin to activate PTT.
40 * `NullPtt` - Fake device that will not control any real PTT.
41
42 Provided error handlers:
43
44 * `StdoutErrorHandler` - Basic handler that will print events as they occur.
45 * `LogErrorHandler` - Uses the common `log` facility to record the event at DEBUG level.
46 * `NullErrorHandler` - Ignore errors.
47
48 For `Soundcard` you will need to identify the soundcard by a string name. The format of this card name is specific to the audio library used (`cpal`). Use `Soundcard::supported_input_cards()` and `Soundcard::supported_output_cards()` to list compatible devices. The bundled utility `m17rt-soundcards` may be useful. Similarly, `SerialPtt::available_ports()` lists the available serial ports.
49
50 If you're using a Digirig on a Linux PC, M17 setup might look like this:
51
52 ```rust,ignore
53 let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
54 let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts).unwrap();
55 let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt, StdoutErrorHandler);
56 let app = M17App::new(soundmodem);
57 app.start();
58 ```
59
60 ## Working with packets
61
62 First let's transmit a packet. We will need to configure some metadata for the transmission, beginning with the source and destination callsigns. Create suitable addresses of type `M17Address`, which will validate that the address is a valid format.
63
64 ```rust,ignore
65 let source = M17Address::from_callsign("VK7XT-1").unwrap();
66 let destination = M17Address::new_broadcast();
67 ```
68
69 All M17 transmissions require a link setup frame which includes the source and destination addresses plus other data. If you wish, you can use the raw `LsfFrame` type to create exactly the frame you want. Here we will use a convenience method to create an LSF for unencrypted packet data.
70
71 ```rust,ignore
72 let link_setup = LinkSetup::new_packet(&source, &destination);
73 ```
74
75 Transmissions are made via a `TxHandle`, which you can create by calling `app.tx()`. We must provide the packet application type and the payload as a byte slice, up to approx 822 bytes. This sends the transmission command to the TNC, which will transmit it when the channel is clear.
76
77 ```rust,ignore
78 let payload = b"Hello, world!";
79 app.tx()
80 .transmit_packet(&link_setup, PacketType::Sms, payload)
81 .unwrap();
82 ```
83
84 Next let's see how to receive a packet. To subscribe to incoming packets you need to provide a subscriber that implements the trait `PacketAdapter`. This includes a number of lifecycle methods which are optional to implement. In this case we will handle `packet_received` and print a summary of the received packet and its contents to stdout.
85
86 ```rust,ignore
87 struct PacketPrinter;
88
89 impl PacketAdapter for PacketPrinter {
90 fn packet_received(&self, link_setup: LinkSetup, packet_type: PacketType, content: Arc<[u8]>) {
91 println!(
92 "from {} to {} type {:?} len {}",
93 link_setup.source(),
94 link_setup.destination(),
95 packet_type,
96 content.len()
97 );
98 println!("{}", String::from_utf8_lossy(&content));
99 }
100 }
101 ```
102
103 We instantiate one of these subscribers and provide it to our instance of `M17App`.
104
105 ```rust,ignore
106 app.add_packet_adapter(PacketPrinter).unwrap();
107 ```
108
109 Note that if the adapter also implemented `adapter_registered`, then it would receive a copy of `TxHandle`. This allows you to create self-contained adapter implementations that can both transmit and receive.
110
111 Adding an adapter returns an identifier that you can use it to remove it again later if you wish. You can add an arbitrary number of adapters. Each will receive its own copy of the packet (or stream, as in the next section).
112
113 ## Working with streams
114
115 M17 also provides streams, which are continuous transmissions of arbitrary length. Unlike packets, you are not guaranteed to receive every frame, and it is possible for a receiver to lock on to a transmission that has previously started and begin decoding it in the middle. These streams may contain voice (generally 3200 bit/s Codec2), arbitrary data, or a combination of voice and data.
116
117 For our first example, let's see how to use the `m17codec2` helper crate to send and receive Codec2 audio.
118
119 The following line will register an adapter that monitors incoming M17 streams, attempts to decode the Codec2, and play the decoded audio on the default system sound card.
120
121 ```rust,ignore
122 // optionally call set_output_card(...) on the adapter
123 app.add_stream_adapter(Codec2RxAdapter::new()).unwrap();
124 ```
125
126 This is how you set up to transmit Codec2 audio:
127
128 ```rust,ignore
129 // optionally call set_input_card(...) on the adapter
130 let mut tx = Codec2TxAdapter::new(args.callsign.clone(), reflector);
131 let ptt = tx.ptt();
132 app.add_stream_adapter(tx).unwrap();
133 ```
134
135 Later, after you have called `start()`:
136
137 ```rust,ignore
138 ptt.set_ptt(true);
139 ```
140
141 This is how to transmit a wave file of human speech (8 kHz, mono, 16 bit LE) as a Codec2 stream:
142
143 ```rust,ignore
144 WavePlayer::play(
145 PathBuf::from("audio.wav"),
146 app.tx(), // TxHandle
147 &M17Address::from_callsign("VK7XT-1").unwrap(), // source
148 &M17Address::new_broadcast(), // destination
149 0, // channel access number
150 );
151 ```
152
153 Transmitting and receiving your own stream types works in a similar way to packets however the requirements are somewhat stricter.
154
155 To transmit:
156
157 * Construct a `LinkSetup` frame, possibly using the `LinkSetup::new_voice()` helper, and call `tx.transmit_stream_start(lsf)`
158 * Immediately construct a `StreamFrame` with data and call `tx.transmit_stream_next(stream_frame)`
159 * Continue sending a `StreamFrame` every 40 ms until you finish with one where `end_of_stream` is set to `true`.
160
161 You are required to fill in two LICH-related fields in `StreamFrame` yourself. The counter should rotate from 0 to 5 (inclusive), and you can get the corresponding bytes using the `lich_part()` helper method on your original `LinkSetup`. The frame number starts at 0 and counts upward.
162
163 To receive:
164
165 * Create an adapter that implements trait `StreamAdapter`
166 * Handle the `stream_began` and `stream_data` methods
167 * Add it to your `M17App` with `add_stream_adapter`