diff --git a/.cargo/config.toml b/.cargo/config.toml
index 0687b20267f03d161ac856f7d088f1a2d8ae7956..de8eca6872180644052c69f949fd82647b52b12b 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,6 +1,7 @@
 [target.thumbv7em-none-eabihf]
 rustflags = [
   "-C", "link-arg=-Tlink.x",
+  "-C", "linker=flip-link",
 ]
 
 [build]
diff --git a/Cargo.lock b/Cargo.lock
index 8c4eb877ad13c12890c82754f1cb5d3cce3cd8fb..0b0da39fe408847709c63c8435d0e34f3b1989dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -306,7 +306,7 @@ dependencies = [
 [[package]]
 name = "ham-cats"
 version = "0.1.0"
-source = "git+https://gitlab.scd31.com/cats/ham-cats/#d32d0a72f3e5633a25cc6495b2fe7daed139fc84"
+source = "git+https://gitlab.scd31.com/cats/ham-cats/#97ff918c4e3aaa6e15dadbaa1ab9c12b1fdb5655"
 dependencies = [
  "arrayvec",
  "bitvec",
@@ -413,11 +413,12 @@ checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
 
 [[package]]
 name = "nmea"
-version = "0.4.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fea2ae8b47e91d2e007edeeaad399231258b67fdbeecebec0cc2ccd6c275771a"
+checksum = "53d5ae624a2d6d1f119eb99fbc433dcaa660e283e3abdfa67e3094418978a9fb"
 dependencies = [
  "arrayvec",
+ "cfg-if",
  "chrono",
  "heapless",
  "nom",
diff --git a/Cargo.toml b/Cargo.toml
index c0645686eb0bccae4d594ec44fc49b2c633b49c3..a888f86cf670cf5757cdc4d649c2ffc01aa21f53 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,8 @@ rf4463 = { git = "https://gitlab.scd31.com/stephen/rf4463-lib" }
 systick-monotonic = "1.0.1"
 ham-cats = { git = "https://gitlab.scd31.com/cats/ham-cats/" }
 half = { version = "2.3.1", default-features = false }
-nmea = { version = "0.4.0", default-features = false }
+# TODO can get rid of some of these features
+nmea = { version = "0.6.0", default-features = false, features = ["VTG", "GGA", "RMC", "GNS", "GLL"] }
 arrayvec = { version = "0.7.4", default-features = false }
 ushell = "0.3.6"
 usbd-serial = "0.1.1"
diff --git a/README.md b/README.md
index 4ee18fee291fe8fd7029a42208b79f58b4a70d42..c621ef79412dbe8d418640bdc5e294a2a63cc5a3 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,7 @@ git clone https://gitlab.scd31.com/cats/mobile-transceiver-software
 cd mobile-transceiver-software
 git pull # make sure we're up to date
 rustup target add thumbv7em-none-eabihf # add our board architecture, if needed
+cargo install flip-link # needed for building
 cargo install cargo-dfu # needed for flashing
 ```
 
diff --git a/src/main.rs b/src/main.rs
index 02a60c258d90dfbed98c71b8375ee4d70a883077..eb29d54a2d075007b975194916016a140c2e2b4e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,6 +28,7 @@ mod app {
     };
     use half::f16;
     use ham_cats::{
+        buffer::Buffer,
         packet::Packet,
         whisker::{Gps, Route},
     };
@@ -225,7 +226,7 @@ mod app {
         )
     }
 
-    #[task(priority = 2, local = [], shared = [red, radio, config, status])]
+    #[task(priority = 3, local = [], shared = [red, radio, config, status])]
     fn radio_tick(mut ctx: radio_tick::Context) {
         (ctx.shared.radio, ctx.shared.config).lock(|r, c| {
             r.as_mut()
@@ -263,7 +264,8 @@ mod app {
         };
 
         if should_transmit {
-            let mut cats: Packet<MAX_PACKET_LEN> = Packet::default();
+            let mut buf = [0; MAX_PACKET_LEN];
+            let mut cats = Packet::new(&mut buf);
 
             config.lock(|config| {
                 cats.add_identification(ham_cats::whisker::Identification {
@@ -292,18 +294,14 @@ mod app {
                 .unwrap();
             }
 
-            let x = cats.fully_encode().unwrap();
-
             let buf = ctx.local.buf;
-            buf[..x.len()].copy_from_slice(&x);
-
-            let tx_buf = &buf[..x.len()];
+            let mut x = Buffer::new_empty(buf);
+            cats.fully_encode(&mut x).unwrap();
 
-            ctx.shared.radio.lock(|radio| {
-                radio
-                    .as_mut()
+            (ctx.shared.radio, config).lock(|r, c| {
+                r.as_mut()
                     .unwrap()
-                    .tx(&mut ctx.shared.red, tx_buf)
+                    .tx(&mut ctx.shared.red, &x, Some((&c.callsign, c.ssid)))
                     .unwrap();
             });
         }
@@ -313,7 +311,7 @@ mod app {
         }
     }
 
-    #[task(priority = 9, local = [green, usb_detect], shared = [red, status])]
+    #[task(priority = 3, local = [green, usb_detect], shared = [red, status])]
     fn led_handler(mut ctx: led_handler::Context) {
         led_handler::spawn_after(LED_BLINK_RATE.millis()).unwrap();
 
diff --git a/src/radio.rs b/src/radio.rs
index 3817e00eb74f5a62dd01ac7678c61286b19bff77..f1a6a7fc8e74a94bdb6c5da9d8f303a9ecd4fe06 100644
--- a/src/radio.rs
+++ b/src/radio.rs
@@ -1,4 +1,5 @@
 use ham_cats::{
+    buffer::Buffer,
     packet::Packet,
     whisker::{Route, RouteNode},
 };
@@ -39,8 +40,7 @@ impl<'a> RadioManager<'a> {
         // sets us up for the default CATS frequency, 430.500 MHz
         radio.set_channel(20);
 
-        // perpetual mode
-        radio.start_rx(None, true).ok()?;
+        radio.start_rx(None, false).ok()?;
 
         Some(Self {
             radio,
@@ -51,11 +51,19 @@ impl<'a> RadioManager<'a> {
 
     // call me every 20-ish ms
     // technically needs to be every 100ms, tops
+    // digipeats only if ident is Some,
+    // otherwise the packet is discarded
     pub fn tick<P: OutputPin, M: Mutex<T = P>>(
         &mut self,
         led: &mut M,
         ident: Option<(&str, u8)>,
     ) -> Result<(), TransferError<spi::Error>> {
+        if self.radio.is_idle() {
+            self.radio
+                .start_rx(None, false)
+                .map_err(TransferError::SpiError)?;
+        }
+
         self.radio.interrupt(Some(self.buf), None)?;
 
         if let Some(data) = self
@@ -65,25 +73,32 @@ impl<'a> RadioManager<'a> {
         {
             if self.enable_digipeating {
                 if let Some((callsign, ssid)) = ident {
-                    if let Ok(packet) = Packet::fully_decode(data.data()) {
+                    let mut buf = [0; MAX_PACKET_LEN];
+                    if let Ok(packet) = Packet::fully_decode(data.data(), &mut buf) {
                         self.handle_packet_rx(led, packet, callsign, ssid);
-                    };
+                    }
                 }
             }
 
-            // Only needed if the radio gets confused
             self.radio
-                .start_rx(None, true)
+                .start_rx(None, false)
                 .map_err(TransferError::SpiError)?;
         }
 
         Ok(())
     }
 
-    pub fn tx<P: OutputPin, M: Mutex<T = P>>(&mut self, led: &mut M, data: &[u8]) -> Option<()> {
+    // digipeats only if ident is Some,
+    // otherwise the rx'd packet is discarded
+    pub fn tx<P: OutputPin, M: Mutex<T = P>>(
+        &mut self,
+        led: &mut M,
+        data: &[u8],
+        ident: Option<(&str, u8)>,
+    ) -> Option<()> {
         // don't want to transmit on top of a packet
         while self.radio.is_busy_rxing().ok()? {
-            self.tick(led, None).ok()?;
+            self.tick(led, ident).ok()?;
         }
 
         led.lock(|l| l.set_high().ok());
@@ -94,15 +109,9 @@ impl<'a> RadioManager<'a> {
         }
         led.lock(|l| l.set_low().ok());
 
-        self.radio
-            .start_rx(None, true)
-            .map_err(TransferError::SpiError)
-            .ok();
-
         Some(())
     }
 
-    // Not great - lots of copies, and therefore stack usage here
     fn handle_packet_rx<P: OutputPin, M: Mutex<T = P>>(
         &mut self,
         led: &mut M,
@@ -115,8 +124,10 @@ impl<'a> RadioManager<'a> {
                 return;
             }
 
-            if let Ok(buf) = packet.fully_encode() {
-                self.tx(led, &buf);
+            let mut buf = [0; MAX_PACKET_LEN];
+            let mut buf = Buffer::new_empty(&mut buf);
+            if packet.fully_encode(&mut buf).is_ok() {
+                self.tx(led, &buf, None);
             }
         }
     }