]> code.octet-stream.net Git - m17rt/commitdiff
Fix timing bugs and add documentation master v0.1.0
authorThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 29 Jan 2025 08:24:20 +0000 (19:24 +1100)
committerThomas Karpiniec <tom.karpiniec@outlook.com>
Wed, 29 Jan 2025 08:24:20 +0000 (19:24 +1100)
19 files changed:
LICENCE.TXT
README.TXT
m17app/Cargo.toml
m17app/README.md
m17app/src/app.rs
m17app/src/lib.rs
m17app/src/rtlsdr.rs
m17app/src/serial.rs
m17app/src/soundmodem.rs
m17codec2/Cargo.toml
m17codec2/README.md
m17codec2/src/lib.rs
m17core/README.md
m17core/src/decode.rs
m17core/src/lib.rs
m17core/src/modem.rs
m17core/src/tnc.rs
tools/m17rt-mod/src/main.rs
tools/m17rt-txpacket/src/main.rs

index 4b4e10240110d2cba8c9c8dc3747d6422de7a329..df053ed6f3e7d7ae278d0e89cbe25980c54a711a 100644 (file)
@@ -1,4 +1,4 @@
-Copyright (c) 2024 Thomas Karpiniec <tom.karpiniec@outlook.com>
+Copyright (c) 2025 Thomas Karpiniec <tom.karpiniec@outlook.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
index 27d42311ea2d34e61e92fb23e9fb9e2b9baa8106..302b3e68387a9f1f9606d2813b1b07789ba2e988 100644 (file)
@@ -2,26 +2,24 @@
  M17 RUST TOOLKIT
 ==================
 
-As of Dec 2024 this software is still under active development. The description that follows is not fully implemented yet.
-
 M17RT is a collection of Rust crates and utilities to make it as easy to possible to implement programs that use the M17
-Protocol for amateur radio: <https://m17foundation.org/projects/>.
+Protocol for amateur radio: <https://m17project.org/>.
 
 
-                        ┌──────────────────────────────────────┐   <.
-                   .>   │ m17app                               │    | Fan in data from adapters to TNC
+                        ┌──────────────────────────────────────┐   <-
+                   ->   │ m17app                               │    | Fan in data from adapters to TNC
  High level API    |    │ - High-level API for packets/streams │    | Fan out data from radio to adapters
- For PC-based apps |    │ - Sound card integration             │   <.
+ For PC-based apps |    │ - Sound card integration             │   <-
                    |    │ - TCP client/server KISS             │         vv KISS ^^
-                   .>   │ - Multithreading                     │   <.
+                   ->   │ - Multithreading                     │   <-
                         └──────────────────────────────────────┘    | Soundmodem worker thread:
                                                                     | Takes a sound card, PTT,
                         ┌──────────────────────────────────────┐    | assembles the components
-                   .>   │ m17core                              │    | from m17core and puts it
+                   ->   │ m17core                              │    | from m17core and puts it
  Low level API     |    │ - M17 KISS protocol                  │    | behind a KISS interface.
  no_std, no heap   |    │ - TNC / CSMA                         │    |
  could be no-float |    │ - M17 Data Link                      │    |
-                   .>   │ - Baseband Modem                     │   <.
+                   ->   │ - Baseband Modem                     │   <-
                         └──────────────────────────────────────┘
 
 
@@ -36,16 +34,16 @@ a KISS TNC, including M17 applications that do not use this toolkit or are not w
 The basic structure of a program is that you will configure your TNC, use it to initialise an M17App, then add adapters to
 the M17App which will handle all or a subset of the traffic. They will all share the same TNC.
 
-Codec2 support follows the same pattern - the m17codec2 crate will provide standard M17App stream adapters to handle both:
-    mic -> encode -> transmit stream
-    incoming stream -> decode -> output on sound card
+Codec2 support follows the same pattern - the m17codec2 crate provides standard M17App stream adapters to handle both:
+    human speech audio -> encode -> transmit stream
+    incoming M17 stream -> decode -> output on sound card
 
 Splitting this into a separate crate serves two purposes. This reduces the dependency count if your app does not actually
 use codec2. It also means you can avoid statically linking LGPL code into your Rust binary if you are relying on M17RT's
 permissive licence. In this situation you can probably still find a way to use codec2 but it's not going to be as simple
 as putting this in your Cargo.toml since Rust makes dynamic linking difficult.
 
-Finally, there will be a series of utility binaries for modulation, demodulation, creating a KISS TCP server, etc. These
+Finally, there is a series of utility binaries for modulation, demodulation, creating a KISS TCP server, etc. These
 may be useful in their own right but their primary purpose is to test and demonstrate the toolkit. User-facing programs
 should be their own projects that will provide proper attention to detail for their use cases.
 
@@ -53,6 +51,6 @@ should be their own projects that will provide proper attention to detail for th
  LICENCE
 =========
 
-Copyright 2024 Thomas Karpiniec <tom.karpiniec@outlook.com>
+Copyright 2025 Thomas Karpiniec <tom.karpiniec@outlook.com>
 
 M17 Rust Toolkit is made available under the MIT Licence. See LICENCE.TXT for details.
index 44a7b5fb2e14373c9629768bd1301012ccb6187e..4c2817f0b3b14714a490443ba5bc73a8ded9a142 100755 (executable)
@@ -14,7 +14,7 @@ readme = "README.md"
 
 [dependencies]
 cpal = "0.15.3"
-m17core = { path = "../m17core" }
+m17core = { version = "0.1", path = "../m17core" }
 log = "0.4.22"
-serialport = {version = "4.7.0", default-features = false }
+serialport = { version = "4.7.0", default-features = false }
 thiserror = "2.0.11"
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d6b548c4939446ad7db9cb996cef3a23583b42f7 100644 (file)
@@ -0,0 +1,143 @@
+# m17app
+
+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.
+
+`m17app` can be considered an easy-to-use wrapper around `m17core`, a separate crate which provides all the modem and TNC functions.
+
+## Creating an `M17App`
+
+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`.
+
+## Creating a `Soundmodem`
+
+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.
+
+A `Soundmodem` requires three parameters:
+
+* **Input source** - the signal we are receiving
+* **Output sink** - somewhere to send the modulated signal we want to transmit
+* **PTT** - a transmit switch that can be turned on or off
+
+These are all traits that you can implement yourself but you can probably use one of the types already included in `m17app`.
+
+Provided inputs:
+
+* `Soundcard` - Once you have initialised a card, call `input()` to get an input source handle to provide to the `Soundmodem`.
+* `RtlSdr` - Receive using an RTL-SDR dongle. This requires that the `rtl_fm` utility is installed and present in your path.
+* `InputRrcFile` - Read from an M17 `.rrc` file on disk, which contains shaped baseband data as 16-bit LE 48 kHz samples.
+* `NullInputSource` - Fake device that provides a continuous stream of silence.
+
+Provided outputs:
+
+* `Soundcard` - Once you have initialised a card, call `output()` to get an output sink handle.
+* `OutputRrcFile` - Write transmissions to a `.rrc` on disk.
+* `NullOutputSink` - Fake device that will swallow any samples it is given.
+
+Provided PTTs:
+
+* `SerialPtt` - Use a serial/COM port with either the RTS or DTR pin to activate PTT.
+* `NullPtt` - Fake device that will not control any real PTT.
+
+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.
+
+If you're using a Digirig on a Linux PC, M17 setup might look like this:
+
+```rust,ignore
+    let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
+    let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts);
+    let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
+    let app = M17App::new(soundmodem);
+    app.start();
+```
+
+## Working with packets
+
+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.
+
+```rust,ignore
+    let source = M17Address::from_callsign("VK7XT-1").unwrap();
+    let destination = M17Address::new_broadcast();
+```
+
+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.
+
+```rust,ignore
+    let link_setup = LinkSetup::new_packet(&source, &destination);
+```
+
+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.
+
+```rust,ignore
+    let payload = b"Hello, world!";
+    app.tx()
+        .transmit_packet(&link_setup, PacketType::Sms, payload);
+```
+
+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.
+
+```rust,ignore
+struct PacketPrinter;
+
+impl PacketAdapter for PacketPrinter {
+    fn packet_received(&self, link_setup: LinkSetup, packet_type: PacketType, content: Arc<[u8]>) {
+        println!(
+            "from {} to {} type {:?} len {}",
+            link_setup.source(),
+            link_setup.destination(),
+            packet_type,
+            content.len()
+        );
+        println!("{}", String::from_utf8_lossy(&content));
+    }
+}
+```
+
+We instantiate one of these subscribers and provide it to our instance of `M17App`.
+
+```rust,ignore
+    app.add_packet_adapter(PacketPrinter);
+```
+
+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.
+
+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).
+
+## Working with streams
+
+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.
+
+For our first example, let's see how to use the `m17codec2` helper crate to send and receive Codec2 audio.
+
+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.
+
+```rust,ignore
+    app.add_stream_adapter(Codec2Adapter::new());
+```
+
+This is how to transmit a wave file of human speech (8 kHz, mono, 16 bit LE) as a Codec2 stream:
+
+```rust,ignore
+    WavePlayer::play(
+        PathBuf::from("audio.wav"),
+        app.tx(),                                       // TxHandle
+        &M17Address::from_callsign("VK7XT-1").unwrap(), // source
+        &M17Address::new_broadcast(),                   // destination
+        0,                                              // channel access number
+    );
+```
+
+Transmitting and receiving your own stream types works in a similar way to packets however the requirements are somewhat stricter.
+
+To transmit:
+
+* Construct a `LinkSetup` frame, possibly using the `LinkSetup::new_voice()` helper, and call `tx.transmit_stream_start(lsf)`
+* Immediately construct a `StreamFrame` with data and call `tx.transmit_stream_next(stream_frame)`
+* Continue sending a `StreamFrame` every 40 ms until you finish with one where `end_of_stream` is set to `true`.
+
+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.
+
+To receive:
+
+* Create an adapter that implements trait `StreamAdapter`
+* Handle the `stream_began` and `stream_data` methods
+* Add it to your `M17App`
index ae019768fe97876bf8960c25f0243e42186e1044..7f17df64a4f5898ba1de7cff2b0b364484f7bc55 100644 (file)
@@ -1,8 +1,9 @@
 use crate::adapter::{PacketAdapter, StreamAdapter};
 use crate::link_setup::LinkSetup;
 use crate::tnc::Tnc;
+use crate::{LsfFrame, PacketType, StreamFrame};
 use m17core::kiss::{KissBuffer, KissCommand, KissFrame};
-use m17core::protocol::{EncryptionType, LsfFrame, PacketType, StreamFrame};
+use m17core::protocol::EncryptionType;
 
 use log::debug;
 use std::collections::HashMap;
index 06a6cfacba8394ff2e5653edaeac4471610ad286..91135151938e23d7db488380ed2b62cb88868d22 100755 (executable)
@@ -1,3 +1,5 @@
+#![doc = include_str!("../README.md")]
+
 pub mod adapter;
 pub mod app;
 pub mod error;
index 33f8070621ae1eb08e05eebdc6cf0f496383b08b..658aca4eada784d80e068173ca0a66bad5167808 100644 (file)
@@ -1,21 +1,12 @@
 use std::{
     io::Read,
     process::{Child, Command, Stdio},
-    sync::{
-        mpsc::{sync_channel, Receiver, SyncSender},
-        Arc, Mutex, RwLock,
-    },
-    time::{Duration, Instant},
-};
-
-use cpal::{
-    traits::{DeviceTrait, HostTrait, StreamTrait},
-    SampleFormat, SampleRate, Stream,
+    sync::{mpsc::SyncSender, Mutex},
 };
 
 use crate::{
     error::M17Error,
-    soundmodem::{InputSource, OutputBuffer, OutputSink, SoundmodemEvent},
+    soundmodem::{InputSource, SoundmodemEvent},
 };
 
 pub struct RtlSdr {
@@ -80,6 +71,7 @@ impl InputSource for RtlSdr {
                 }
             }
         });
+        *self.rtlfm.lock().unwrap() = Some(cmd);
     }
 
     fn close(&self) {
index d3eec1936eab13cd8725b0b67f43de07fb5e5dab..8abb4b93ca170ee95c2cff7833f0eaa65f94e8c2 100644 (file)
@@ -26,7 +26,9 @@ impl SerialPtt {
     pub fn new(port_name: &str, pin: PttPin) -> Self {
         // TODO: error handling
         let port = serialport::new(port_name, 9600).open().unwrap();
-        Self { port, pin }
+        let mut s = Self { port, pin };
+        s.ptt_off();
+        s
     }
 }
 
index 974fd45085600b8fa60d1f7cac377e765415426d..391bfadde1b66b2ee33c1b7a336991aabe126e19 100644 (file)
@@ -138,8 +138,12 @@ fn spawn_soundmodem_worker(
         let mut ptt = false;
         while let Ok(ev) = event_rx.recv() {
             // Update clock on TNC before we do anything
-            let sample_time = (start.elapsed().as_nanos() / 48000) as u64;
-            tnc.set_now(sample_time);
+            let sample_time = start.elapsed();
+            let secs = sample_time.as_secs();
+            let nanos = sample_time.subsec_nanos();
+            // Accurate to within approx 1 sample
+            let now_samples = 48000 * secs + (nanos as u64 / 20833);
+            tnc.set_now(now_samples);
 
             // Handle event
             match ev {
index e0901606db4ad40ba03d5d7bf7a40b3e3faa49e2..01b032f921cdc3aea0e8fe809694a4a60a5425ce 100755 (executable)
@@ -16,7 +16,7 @@ readme = "README.md"
 codec2 = "0.3.0"
 cpal = "0.15.3"
 hound = "3.5.1"
-m17core = { path = "../m17core" }
-m17app = { path = "../m17app" }
+m17core = { version = "0.1", path = "../m17core" }
+m17app = { version = "0.1", path = "../m17app" }
 log = "0.4.22"
 
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0475c2823e17c01b109b783d85a461e91e636cec 100644 (file)
@@ -0,0 +1,8 @@
+# m17codec2
+
+Part of the [M17 Rust Toolkit](https://octet-stream.net/p/m17rt/). Pre-made adapters designed for the `m17app` crate that make it easier to work with Codec2 voice streams.
+
+* `WavePlayer` - transmit a wave file as a stream (8 kHz, mono, 16 bit LE)
+* `Codec2Adapter` - receive all incoming streams and attempt to play the decoded audio on the default sound card
+
+**Important licence note:** While `m17codec2` is under the MIT licence, it uses the `codec2` crate as a dependency, which will statically link LGPL code in the build. If you are distributing software in a way where LGPL compliance requires special care (e.g., dynamic linking), consider implementing your own codec2 adapters in a way that is compliant in your scenario.
index 1d05f2720f827ac23ed982c6f243b9b06486232e..e4bfbacc76f1a0cf2f28f7a7915672b4d41eb82b 100755 (executable)
@@ -1,3 +1,5 @@
+#![doc = include_str!("../README.md")]
+
 use codec2::{Codec2, Codec2Mode};
 use cpal::traits::DeviceTrait;
 use cpal::traits::HostTrait;
@@ -40,6 +42,7 @@ pub fn decode_codec2<P: AsRef<Path>>(data: &[u8], out_path: P) -> Vec<i16> {
     all_samples
 }
 
+/// Subscribes to M17 streams and attempts to play the decoded Codec2
 pub struct Codec2Adapter {
     state: Arc<Mutex<AdapterState>>,
     // TODO: make this configurable
@@ -162,9 +165,17 @@ fn stream_thread(end: Receiver<()>, state: Arc<Mutex<AdapterState>>, output_card
     // it seems concrete impls of Stream have a Drop implementation that will handle termination
 }
 
+/// Transmits a wave file as an M17 stream
 pub struct WavePlayer;
 
 impl WavePlayer {
+    /// Plays a wave file (blocking).
+    ///
+    /// * `path`: wave file to transmit, must be 8 kHz mono and 16-bit LE
+    /// * `tx`: a `TxHandle` obtained from an `M17App`
+    /// * `source`: address of transmission source
+    /// * `destination`: address of transmission destination
+    /// * `channel_access_number`: from 0 to 15, usually 0
     pub fn play(
         path: PathBuf,
         tx: TxHandle,
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cee5df0f30fe54ff68bd40981b0e2ba96befb11f 100644 (file)
@@ -0,0 +1,19 @@
+# m17codec2
+
+Part of the [M17 Rust Toolkit](https://octet-stream.net/p/m17rt/).
+
+This crate includes a modulator, demodulator, TNC, M17 data link parsing and encoding, KISS protocol handling, and other protocol utilities. It can be used to create an M17 transmitter or receiver, however you will have to connect everything together yourself. If possible, consider using the higher-level crate `m17app`.
+
+`m17core` is `no_std`, does not perform any heap allocations, and its protocol implementations are non-blocking and sans-I/O.
+
+You might be interested in using this crate directly for:
+
+* Developing on bare metal targets where `std` is not available or appropriate
+* Specialised M17 utilities or simulations
+
+There is an implied protocol between `SoftModulator`, `SoftDemodulator` and `SoftTnc`. For a full example see the implementation of `Soundmodem` in `m17app`.
+
+In brief: the rx path is that new samples will be given to `SoftDemodulator`. It may emit a frame, which should be delivered to `SoftTnc`. In turn, it may emit a KISS frame for the host.
+
+The tx path is a little more complicated. You must supply a ring buffer which is shared between the DAC consuming samples and the `SoftModulator` creating samples. The `SoftTnc` indicates when a transmission begins, then the flow of data is controlled by `SoftModulator` which will opportunistically draw new frames out of the TNC to keep the output buffer topped up. When the TNC indicates the end of the transmission, it will wait for the `SoftModulator` to indicate when tx will finish and PTT should be disengaged. While this is occurring, new stream frames should be delivered via `SoftTnc`'s KISS interface at an equal ratio to the output samples being read so that buffers do not overflow or underrun.
+
index 911d67371cf5f5273446992cd2d472cb4ac18b47..d3dd7ba087fe0c59b3cdcbf0e557c4d44405cb65 100755 (executable)
@@ -146,8 +146,6 @@ pub(crate) fn parse_packet(frame: &[f32] /* length 192 */) -> Option<PacketFrame
         Some(packet) => packet,
         None => return None,
     };
-    // TODO: the spec is inconsistent about which bit in packet[25] is EOF
-    // https://github.com/M17-Project/M17_spec/issues/147
     let final_frame = (packet[25] & 0x80) > 0;
     let number = (packet[25] >> 2) & 0x1f;
     let counter = if final_frame {
index eb4a4579607ba9ec9d59b8329b0d8ea154cef089..b97171870d336dfecd9067666bd844905f5595f7 100755 (executable)
@@ -1,3 +1,4 @@
+#![doc = include_str!("../README.md")]
 #![allow(clippy::needless_range_loop)]
 #![cfg_attr(not(test), no_std)]
 
index 3d4890ad857376aba09d28ad46630be608ee698f..43ad5acb3f543279ee7f34b9583121f6c4daf2e8 100644 (file)
@@ -333,6 +333,7 @@ impl SoftModulator {
             next_len: 0,
             next_read: 0,
             tx_delay_padding: 0,
+            // TODO: actually set this to false when we are worried about underrun
             update_idle: true,
             idle: true,
             calculate_tx_end: false,
@@ -384,7 +385,6 @@ impl Modulator for SoftModulator {
         capacity: usize,
         output_latency: usize,
     ) {
-        //log::debug!("modulator update_output_buffer {samples_to_play} {capacity} {output_latency}");
         self.output_latency = output_latency;
         self.buf_capacity = capacity;
         self.samples_in_buf = samples_to_play;
@@ -412,12 +412,9 @@ impl Modulator for SoftModulator {
             ModulatorFrame::Preamble { tx_delay } => {
                 // TODO: Stop assuming 48 kHz everywhere. 24 kHz should be fine too.
                 let tx_delay_samples = tx_delay as usize * 480;
-                // TxDelay and output latency have the same effect - account for whichever is bigger.
-                // We want our sound card DAC hitting preamble right when PTT fully engages.
-                // The modulator calls the shots here - TNC hands over Preamble and asserts PTT, then
-                // waits to be told when transmission will be complete. This estimate will not be
-                // made and delivered until we generate the EOT frame.
-                self.tx_delay_padding = tx_delay_samples.max(self.output_latency);
+                // Our output latency gives us a certain amount of unavoidable TxDelay
+                // So only introduce artificial delay if the requested TxDelay exceeds that
+                self.tx_delay_padding = tx_delay_samples.saturating_sub(self.output_latency);
 
                 // We should be starting from a filter_win of zeroes
                 // Transmission is effectively smeared by 80 taps and we'll capture that in EOT
index a64f367e70c1f152eafb1892c311ee663b36ec35..b46958c72f2dd3f85955bef3335a928bb2e6d408 100644 (file)
@@ -217,6 +217,8 @@ impl SoftTnc {
 
     pub fn set_now(&mut self, now_samples: u64) {
         self.now = now_samples;
+        // TODO: expose this to higher layer so we can schedule a precise delay
+        // rather than waiting for some soundcard I/O event
         if let State::TxEndingAtTime(time) = self.state {
             if now_samples >= time {
                 self.ptt = false;
index 3bd672785c359dc2e01e1cc288734a436325645d..b5708e95858a7e974b36ef8d9547f8e2065b2e31 100644 (file)
@@ -28,6 +28,8 @@ pub fn mod_test() {
 }
 
 fn main() {
-    env_logger::init();
+    env_logger::builder()
+        .format_timestamp(Some(env_logger::TimestampPrecision::Millis))
+        .init();
     mod_test();
 }
index fb6eda19368dc61ea73934c383a0a5be9b181171..916edf2f8773600678148457507d476dd670824b 100644 (file)
@@ -7,6 +7,7 @@ use m17core::protocol::PacketType;
 
 fn main() {
     let soundcard = Soundcard::new("plughw:CARD=Device,DEV=0").unwrap();
+    soundcard.set_tx_inverted(true);
     let ptt = SerialPtt::new("/dev/ttyUSB0", PttPin::Rts);
     let soundmodem = Soundmodem::new(soundcard.input(), soundcard.output(), ptt);
     let app = M17App::new(soundmodem);