"memchr",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
  "hashbrown",
 ]
 
+[[package]]
+name = "io-kit-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
+dependencies = [
+ "core-foundation-sys",
+ "mach2",
+]
+
 [[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
  "cpal",
  "log",
  "m17core",
+ "serialport",
 ]
 
 [[package]]
  "jni-sys",
 ]
 
+[[package]]
+name = "nix"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.3"
  "winapi-util",
 ]
 
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serialport"
+version = "4.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ecfc4858c2266c7695d8b8460bbd612fa81bd2e250f5f0dd16195e4b4f8b3d8"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "core-foundation",
+ "core-foundation-sys",
+ "io-kit-sys",
+ "mach2",
+ "nix",
+ "scopeguard",
+ "unescaper",
+ "winapi",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
  "winnow",
 ]
 
+[[package]]
+name = "unescaper"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815"
+dependencies = [
+ "thiserror",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.14"
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
 [[package]]
 name = "winapi-util"
 version = "0.1.9"
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
 [[package]]
 name = "windows"
 version = "0.54.0"
 
 cpal = "0.15.3"
 m17core = { path = "../m17core" }
 log = "0.4.22"
+serialport = {version = "4.7.0", default-features = false }
 
     }
 
     pub fn close(&self) {
+        // TODO: blocking function to indicate TNC has finished closing
+        // then we could call this in a signal handler to ensure PTT is dropped before quit
         let _ = self.event_tx.send(TncControlEvent::Close);
     }
 }
 
 pub mod adapter;
 pub mod app;
+pub mod serial;
 pub mod soundmodem;
 pub mod tnc;
 
--- /dev/null
+use serialport::SerialPort;
+
+use crate::soundmodem::Ptt;
+
+/// The pin on the serial port which is driving PTT
+pub enum PttPin {
+    // Ready To Send (RTS)
+    Rts,
+    // Data Terminal ready (DTR)
+    Dtr,
+}
+
+pub struct SerialPtt {
+    port: Box<dyn SerialPort>,
+    pin: PttPin,
+}
+
+impl SerialPtt {
+    pub fn available_ports() -> impl Iterator<Item = String> {
+        serialport::available_ports()
+            .unwrap_or_else(|_| vec![])
+            .into_iter()
+            .map(|i| i.port_name)
+    }
+
+    pub fn new(port_name: &str, pin: PttPin) -> Self {
+        // TODO: error handling
+        let port = serialport::new(port_name, 9600).open().unwrap();
+        Self { port, pin }
+    }
+}
+
+impl Ptt for SerialPtt {
+    fn ptt_on(&mut self) {
+        let _ = match self.pin {
+            PttPin::Rts => self.port.write_request_to_send(true),
+            PttPin::Dtr => self.port.write_data_terminal_ready(true),
+        };
+    }
+
+    fn ptt_off(&mut self) {
+        let _ = match self.pin {
+            PttPin::Rts => self.port.write_request_to_send(false),
+            PttPin::Dtr => self.port.write_data_terminal_ready(false),
+        };
+    }
+}
 
 }
 
 impl Soundmodem {
-    pub fn new_with_input_and_output<I: InputSource, O: OutputSink>(input: I, output: O) -> Self {
+    pub fn new<I: InputSource, O: OutputSink, P: Ptt>(input: I, output: O, ptt: P) -> Self {
         // must create TNC here
         let (event_tx, event_rx) = sync_channel(128);
         let (kiss_out_tx, kiss_out_rx) = sync_channel(128);
             kiss_out_tx,
             Box::new(input),
             Box::new(output),
+            Box::new(ptt),
         );
         Self {
             event_tx,
     kiss_out_tx: SyncSender<Arc<[u8]>>,
     input: Box<dyn InputSource>,
     output: Box<dyn OutputSink>,
+    mut ptt_driver: Box<dyn Ptt>,
 ) {
     std::thread::spawn(move || {
         // TODO: should be able to provide a custom Demodulator for a soundmodem
                     input.start(event_tx.clone());
                     output.start(event_tx.clone(), out_buffer.clone());
                 }
-                SoundmodemEvent::Close => break,
+                SoundmodemEvent::Close => {
+                    ptt_driver.ptt_off();
+                    break;
+                }
                 SoundmodemEvent::DidReadFromOutputBuffer { len, timestamp } => {
                     let (occupied, internal_latency) = {
                         let out_buffer = out_buffer.read().unwrap();
             let new_ptt = tnc.ptt();
             if new_ptt != ptt {
                 if new_ptt {
-                    // turn it on
+                    ptt_driver.ptt_on();
                 } else {
-                    // turn it off
+                    ptt_driver.ptt_off();
                 }
             }
             ptt = new_ptt;
         let _ = self.end_tx.lock().unwrap().take();
     }
 }
+
+pub trait Ptt: Send + 'static {
+    fn ptt_on(&mut self);
+    fn ptt_off(&mut self);
+}
+
+/// There is no PTT because this TNC will never make transmissions on a real radio.
+pub struct NullPtt;
+
+impl NullPtt {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl Ptt for NullPtt {
+    fn ptt_on(&mut self) {}
+    fn ptt_off(&mut self) {}
+}
 
     let stream = device
         .build_output_stream(
             &config.into(),
-            move |data: &mut [i16], info: &cpal::OutputCallbackInfo| {
+            move |data: &mut [i16], _info: &cpal::OutputCallbackInfo| {
                 output_cb(data, &state);
             },
             |e| {
 
 use m17app::app::M17App;
-use m17app::soundmodem::{InputRrcFile, InputSoundcard, NullOutputSink, Soundmodem};
+use m17app::soundmodem::{InputRrcFile, InputSoundcard, NullOutputSink, NullPtt, Soundmodem};
 use m17codec2::Codec2Adapter;
 use std::path::PathBuf;
 
     //let path = PathBuf::from("../../../Data/mymod-noisy.raw");
     let source = InputRrcFile::new(path);
     //let source = InputSoundcard::new();
-    let soundmodem = Soundmodem::new_with_input_and_output(source, NullOutputSink::new());
+    let soundmodem = Soundmodem::new(source, NullOutputSink::new(), NullPtt::new());
     let app = M17App::new(soundmodem);
     app.add_stream_adapter(Codec2Adapter::new());
     app.start();
 
 use m17app::app::M17App;
 use m17app::soundmodem::{
-    InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, OutputRrcFile, OutputSoundcard,
-    Soundmodem,
+    InputRrcFile, InputSoundcard, NullInputSource, NullOutputSink, NullPtt, OutputRrcFile,
+    OutputSoundcard, Soundmodem,
 };
 use m17codec2::{Codec2Adapter, WavePlayer};
 use std::path::PathBuf;
     //let out_path = PathBuf::from("../../../Data/mymod.rrc");
     //let output = OutputRrcFile::new(out_path);
     let output = OutputSoundcard::new();
-    let soundmodem = Soundmodem::new_with_input_and_output(NullInputSource::new(), output);
+    let soundmodem = Soundmodem::new(NullInputSource::new(), output, NullPtt::new());
     let app = M17App::new(soundmodem);
     app.start();
     std::thread::sleep(std::time::Duration::from_secs(1));