Skip to main content

rns_net/interface/
kiss_iface.rs

1//! KISS interface with flow control and TNC configuration.
2//!
3//! Matches Python `KISSInterface.py` — opens a serial port,
4//! sends TNC configuration commands, handles KISS framing with
5//! optional flow control.
6
7use std::collections::VecDeque;
8use std::io::{self, Read, Write};
9use std::os::fd::AsRawFd;
10use std::sync::{Arc, Mutex};
11use std::thread;
12use std::time::{Duration, Instant};
13
14use rns_core::transport::types::InterfaceId;
15
16use crate::event::{Event, EventSender};
17use crate::interface::{
18    lock_or_recover, poll_readable, wait_while_active, ListenerControl, UnavailableWriter, Writer,
19};
20use crate::kiss;
21use crate::serial::{Parity, SerialConfig, SerialPort};
22
23/// Configuration for a KISS interface.
24#[derive(Debug, Clone)]
25pub struct KissIfaceConfig {
26    pub name: String,
27    pub port: String,
28    pub speed: u32,
29    pub data_bits: u8,
30    pub parity: Parity,
31    pub stop_bits: u8,
32    pub preamble: u16,                // ms, default 350
33    pub txtail: u16,                  // ms, default 20
34    pub persistence: u8,              // 0-255, default 64
35    pub slottime: u16,                // ms, default 20
36    pub flow_control: bool,           // default false
37    pub beacon_interval: Option<u32>, // seconds
38    pub beacon_data: Option<Vec<u8>>, // padded to 15 bytes
39    pub interface_id: InterfaceId,
40    pub ax25_source: Option<Ax25Address>,
41    pub interface_type_name: String,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Ax25Address {
46    pub callsign: String,
47    pub ssid: u8,
48}
49
50impl Default for KissIfaceConfig {
51    fn default() -> Self {
52        KissIfaceConfig {
53            name: String::new(),
54            port: String::new(),
55            speed: 9600,
56            data_bits: 8,
57            parity: Parity::None,
58            stop_bits: 1,
59            preamble: 350,
60            txtail: 20,
61            persistence: 64,
62            slottime: 20,
63            flow_control: false,
64            beacon_interval: None,
65            beacon_data: None,
66            interface_id: InterfaceId(0),
67            ax25_source: None,
68            interface_type_name: "KISSInterface".into(),
69        }
70    }
71}
72
73/// Shared flow-control state between writer and reader threads.
74struct FlowState {
75    ready: bool,
76    queue: VecDeque<Vec<u8>>,
77    lock_time: Instant,
78}
79
80/// Writer that sends KISS-framed data over a serial port.
81/// Handles flow control: when enabled, queues packets until CMD_READY.
82struct KissWriter {
83    confirmed_pending: Option<Vec<u8>>,
84    file: std::fs::File,
85    flow_control: bool,
86    flow_state: Arc<Mutex<FlowState>>,
87    ax25_source: Option<Ax25Address>,
88}
89
90impl Writer for KissWriter {
91    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
92        if !self.flow_control {
93            return self.send_frame(data);
94        }
95        let data = self
96            .ax25_source
97            .as_ref()
98            .map(|source| super::ax25_kiss::encode_ui_frame(source, data))
99            .unwrap_or_else(|| data.to_vec());
100        self.confirmed_pending = Some(kiss::frame(&data));
101        self.flush()
102    }
103
104    fn flush(&mut self) -> io::Result<()> {
105        if self.confirmed_pending.is_none() {
106            return Ok(());
107        }
108        let mut state = lock_or_recover(&self.flow_state, "kiss flow state");
109        if !state.ready || !state.queue.is_empty() {
110            return Err(io::ErrorKind::WouldBlock.into());
111        }
112        state.ready = false;
113        state.lock_time = Instant::now();
114        drop(state);
115        self.file.write_all(&self.confirmed_pending.take().unwrap())
116    }
117    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
118        let data = self
119            .ax25_source
120            .as_ref()
121            .map(|source| super::ax25_kiss::encode_ui_frame(source, data))
122            .unwrap_or_else(|| data.to_vec());
123        if self.flow_control {
124            let mut state = lock_or_recover(&self.flow_state, "kiss flow state");
125            if state.ready {
126                state.ready = false;
127                state.lock_time = Instant::now();
128                drop(state);
129                self.file.write_all(&kiss::frame(&data))
130            } else {
131                state.queue.push_back(data);
132                Ok(())
133            }
134        } else {
135            self.file.write_all(&kiss::frame(&data))
136        }
137    }
138}
139
140/// Start the KISS interface. Opens the port, configures TNC, spawns reader thread.
141pub fn start(config: KissIfaceConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
142    start_with_control(config, tx).map(|(writer, _)| writer)
143}
144
145fn start_with_control(
146    config: KissIfaceConfig,
147    tx: EventSender,
148) -> io::Result<(Box<dyn Writer>, ListenerControl)> {
149    let serial_config = SerialConfig {
150        path: config.port.clone(),
151        baud: config.speed,
152        data_bits: config.data_bits,
153        parity: config.parity,
154        stop_bits: config.stop_bits,
155    };
156
157    let control = ListenerControl::new();
158    let flow_state = Arc::new(Mutex::new(FlowState {
159        ready: true,
160        queue: VecDeque::new(),
161        lock_time: Instant::now(),
162    }));
163    let port = match SerialPort::open(&serial_config) {
164        Ok(port) => port,
165        Err(error) => {
166            log::warn!(
167                "[{}] could not open KISS port {}: {}; retrying",
168                config.name,
169                config.port,
170                error
171            );
172            spawn_initial_reconnect(config, tx, flow_state, control.clone())?;
173            return Ok((Box::new(UnavailableWriter), control));
174        }
175    };
176    let reader_file = port.reader()?;
177    let mut writer_file = port.writer()?;
178    let flow_writer_file = port.writer()?;
179
180    let id = config.interface_id;
181
182    // Initial 2-second delay for TNC initialization (matches Python)
183    thread::sleep(Duration::from_secs(2));
184
185    // Send TNC configuration commands
186    if let Err(error) = configure_tnc(&mut writer_file, &config) {
187        log::warn!(
188            "[{}] initial TNC configuration failed: {}; retrying",
189            config.name,
190            error
191        );
192        drop(reader_file);
193        drop(writer_file);
194        drop(flow_writer_file);
195        spawn_initial_reconnect(config, tx, flow_state, control.clone())?;
196        return Ok((Box::new(UnavailableWriter), control));
197    }
198
199    let _ = tx.send(Event::InterfaceUp(id, None, None));
200
201    let reader_flow_state = flow_state.clone();
202
203    // Spawn reader thread
204    let reader_config = config.clone();
205    let reader_control = control.clone();
206    thread::Builder::new()
207        .name(format!("kiss-reader-{}", id.0))
208        .spawn(move || {
209            reader_loop(
210                reader_file,
211                flow_writer_file,
212                id,
213                reader_config,
214                tx,
215                reader_flow_state,
216                reader_control,
217            );
218        })?;
219
220    Ok((
221        Box::new(KissWriter {
222            confirmed_pending: None,
223            file: writer_file,
224            flow_control: config.flow_control,
225            flow_state,
226            ax25_source: config.ax25_source.clone(),
227        }),
228        control,
229    ))
230}
231
232fn spawn_initial_reconnect(
233    config: KissIfaceConfig,
234    tx: EventSender,
235    flow_state: Arc<Mutex<FlowState>>,
236    control: ListenerControl,
237) -> io::Result<()> {
238    thread::Builder::new()
239        .name(format!("kiss-reconnect-{}", config.interface_id.0))
240        .spawn(move || {
241            if let Some((reader, flow_writer)) = reconnect(&config, &tx, &flow_state, &control) {
242                let id = config.interface_id;
243                reader_loop(reader, flow_writer, id, config, tx, flow_state, control);
244            }
245        })?;
246    Ok(())
247}
248
249/// Send TNC configuration commands via KISS.
250/// Matches Python `KISSInterface.configure_device()`.
251fn configure_tnc(writer: &mut std::fs::File, config: &KissIfaceConfig) -> io::Result<()> {
252    log::info!("[{}] configuring KISS interface parameters", config.name);
253
254    // Preamble: value is ms/10, clamped to 0-255
255    let preamble_val = (config.preamble / 10).min(255) as u8;
256    writer.write_all(&kiss::command_frame(kiss::CMD_TXDELAY, &[preamble_val]))?;
257
258    // TX tail: value is ms/10, clamped to 0-255
259    let txtail_val = (config.txtail / 10).min(255) as u8;
260    writer.write_all(&kiss::command_frame(kiss::CMD_TXTAIL, &[txtail_val]))?;
261
262    // Persistence: raw value, clamped to 0-255
263    writer.write_all(&kiss::command_frame(kiss::CMD_P, &[config.persistence]))?;
264
265    // Slot time: value is ms/10, clamped to 0-255
266    let slottime_val = (config.slottime / 10).min(255) as u8;
267    writer.write_all(&kiss::command_frame(kiss::CMD_SLOTTIME, &[slottime_val]))?;
268
269    // Flow control: send CMD_READY with 0x01 (matches Python setFlowControl)
270    writer.write_all(&kiss::command_frame(kiss::CMD_READY, &[0x01]))?;
271
272    log::info!("[{}] KISS interface configured", config.name);
273    Ok(())
274}
275
276/// Reader loop: reads from serial, KISS-decodes, dispatches events.
277/// Also handles flow control unlocking and beacon transmission.
278fn reader_loop(
279    mut reader: std::fs::File,
280    mut flow_writer: std::fs::File,
281    id: InterfaceId,
282    config: KissIfaceConfig,
283    tx: EventSender,
284    flow_state: Arc<Mutex<FlowState>>,
285    control: ListenerControl,
286) {
287    let mut decoder = kiss::Decoder::new();
288    let mut buf = [0u8; 4096];
289    let mut first_tx: Option<Instant> = None;
290
291    loop {
292        if control.should_stop() {
293            return;
294        }
295        match poll_readable(reader.as_raw_fd(), Duration::from_millis(200)) {
296            Ok(false) => continue,
297            Ok(true) => {}
298            Err(e) => {
299                if control.should_stop() {
300                    return;
301                }
302                log::warn!("[{}] KISS poll error: {}", config.name, e);
303                let _ = tx.send(Event::InterfaceDown(id));
304                match reconnect(&config, &tx, &flow_state, &control) {
305                    Some((new_reader, new_flow_writer)) => {
306                        reader = new_reader;
307                        flow_writer = new_flow_writer;
308                        decoder = kiss::Decoder::new();
309                        continue;
310                    }
311                    None => return,
312                }
313            }
314        }
315        match reader.read(&mut buf) {
316            Ok(0) => {
317                if control.should_stop() {
318                    return;
319                }
320                log::warn!("[{}] KISS port closed", config.name);
321                let _ = tx.send(Event::InterfaceDown(id));
322                match reconnect(&config, &tx, &flow_state, &control) {
323                    Some((new_reader, new_flow_writer)) => {
324                        reader = new_reader;
325                        flow_writer = new_flow_writer;
326                        decoder = kiss::Decoder::new();
327                        continue;
328                    }
329                    None => return,
330                }
331            }
332            Ok(n) => {
333                for event in decoder.feed(&buf[..n]) {
334                    match event {
335                        kiss::KissEvent::DataFrame(data) => {
336                            let data = if config.ax25_source.is_some() {
337                                match super::ax25_kiss::decode_ui_frame(&data) {
338                                    Some(data) => data.to_vec(),
339                                    None => continue,
340                                }
341                            } else {
342                                data
343                            };
344                            if tx
345                                .send(Event::Frame {
346                                    interface_id: id,
347                                    data,
348                                    rssi: None,
349                                    snr: None,
350                                })
351                                .is_err()
352                            {
353                                return;
354                            }
355                        }
356                        kiss::KissEvent::Ready => {
357                            process_queue(&flow_state, &mut flow_writer, &mut first_tx, &config);
358                        }
359                    }
360                }
361            }
362            Err(e) => {
363                if control.should_stop() {
364                    return;
365                }
366                log::warn!("[{}] KISS read error: {}", config.name, e);
367                let _ = tx.send(Event::InterfaceDown(id));
368                match reconnect(&config, &tx, &flow_state, &control) {
369                    Some((new_reader, new_flow_writer)) => {
370                        reader = new_reader;
371                        flow_writer = new_flow_writer;
372                        decoder = kiss::Decoder::new();
373                        continue;
374                    }
375                    None => return,
376                }
377            }
378        }
379
380        // Flow control timeout check
381        if config.flow_control {
382            let state = lock_or_recover(&flow_state, "kiss flow state");
383            if !state.ready && state.lock_time.elapsed() > Duration::from_secs(5) {
384                drop(state);
385                log::warn!("[{}] unlocking flow control due to timeout", config.name);
386                process_queue(&flow_state, &mut flow_writer, &mut first_tx, &config);
387            }
388        }
389
390        // Beacon check
391        if let (Some(interval), Some(ref beacon_data)) =
392            (config.beacon_interval, &config.beacon_data)
393        {
394            if let Some(first) = first_tx {
395                if first.elapsed() > Duration::from_secs(interval as u64) {
396                    log::debug!("[{}] transmitting beacon data", config.name);
397                    // Pad to minimum 15 bytes
398                    let mut frame = beacon_data.clone();
399                    while frame.len() < 15 {
400                        frame.push(0x00);
401                    }
402                    let _ = flow_writer.write_all(&kiss::frame(&frame));
403                    first_tx = None;
404                }
405            }
406        }
407    }
408}
409
410/// Process the flow control queue: send next queued packet, mark ready.
411fn process_queue(
412    flow_state: &Arc<Mutex<FlowState>>,
413    writer: &mut std::fs::File,
414    first_tx: &mut Option<Instant>,
415    _config: &KissIfaceConfig,
416) {
417    let mut state = lock_or_recover(flow_state, "kiss flow state");
418    if let Some(data) = state.queue.pop_front() {
419        state.ready = false;
420        state.lock_time = Instant::now();
421        drop(state);
422        let _ = writer.write_all(&kiss::frame(&data));
423        if first_tx.is_none() {
424            *first_tx = Some(Instant::now());
425        }
426    } else {
427        state.ready = true;
428    }
429}
430
431/// Attempt to reconnect the serial port.
432fn reconnect(
433    config: &KissIfaceConfig,
434    tx: &EventSender,
435    flow_state: &Arc<Mutex<FlowState>>,
436    control: &ListenerControl,
437) -> Option<(std::fs::File, std::fs::File)> {
438    loop {
439        if !wait_while_active(control, Duration::from_secs(5)) {
440            return None;
441        }
442        log::info!(
443            "[{}] attempting to reconnect KISS port {}...",
444            config.name,
445            config.port
446        );
447
448        let serial_config = SerialConfig {
449            path: config.port.clone(),
450            baud: config.speed,
451            data_bits: config.data_bits,
452            parity: config.parity,
453            stop_bits: config.stop_bits,
454        };
455
456        match SerialPort::open(&serial_config) {
457            Ok(port) => {
458                match (port.reader(), port.writer(), port.writer()) {
459                    (Ok(reader), Ok(mut cfg_writer), Ok(flow_writer)) => {
460                        // 2-second init delay
461                        if !wait_while_active(control, Duration::from_secs(2)) {
462                            return None;
463                        }
464                        if let Err(e) = configure_tnc(&mut cfg_writer, config) {
465                            log::warn!("[{}] TNC config failed: {}", config.name, e);
466                            continue;
467                        }
468                        // Reset flow state
469                        let mut state = lock_or_recover(flow_state, "kiss flow state");
470                        state.ready = true;
471                        state.queue.clear();
472                        drop(state);
473
474                        let new_writer: Box<dyn Writer> = Box::new(KissWriter {
475                            confirmed_pending: None,
476                            file: cfg_writer,
477                            flow_control: config.flow_control,
478                            flow_state: flow_state.clone(),
479                            ax25_source: config.ax25_source.clone(),
480                        });
481                        let _ = tx.send(Event::InterfaceUp(
482                            config.interface_id,
483                            Some(new_writer),
484                            None,
485                        ));
486                        log::info!("[{}] KISS port reconnected", config.name);
487                        return Some((reader, flow_writer));
488                    }
489                    _ => {
490                        log::warn!("[{}] failed to get handles from serial port", config.name);
491                    }
492                }
493            }
494            Err(e) => {
495                log::warn!("[{}] KISS reconnect failed: {}", config.name, e);
496            }
497        }
498    }
499}
500
501// --- Factory implementation ---
502
503use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
504use rns_core::transport::types::InterfaceInfo;
505use std::collections::HashMap;
506
507/// Factory for `KISSInterface`.
508pub struct KissFactory;
509
510impl InterfaceFactory for KissFactory {
511    fn type_name(&self) -> &str {
512        "KISSInterface"
513    }
514
515    fn default_ifac_size(&self) -> usize {
516        8
517    }
518
519    fn parse_config(
520        &self,
521        name: &str,
522        id: InterfaceId,
523        params: &HashMap<String, String>,
524    ) -> Result<Box<dyn InterfaceConfigData>, String> {
525        let port = params
526            .get("port")
527            .cloned()
528            .ok_or_else(|| "KISSInterface requires 'port'".to_string())?;
529
530        let speed = params
531            .get("speed")
532            .and_then(|v| v.parse().ok())
533            .unwrap_or(9600u32);
534
535        let data_bits = params
536            .get("databits")
537            .and_then(|v| v.parse().ok())
538            .unwrap_or(8u8);
539
540        let parity = params
541            .get("parity")
542            .map(|v| match v.to_lowercase().as_str() {
543                "e" | "even" => crate::serial::Parity::Even,
544                "o" | "odd" => crate::serial::Parity::Odd,
545                _ => crate::serial::Parity::None,
546            })
547            .unwrap_or(crate::serial::Parity::None);
548
549        let stop_bits = params
550            .get("stopbits")
551            .and_then(|v| v.parse().ok())
552            .unwrap_or(1u8);
553
554        let preamble = params
555            .get("preamble")
556            .and_then(|v| v.parse().ok())
557            .unwrap_or(350u16);
558
559        let txtail = params
560            .get("txtail")
561            .and_then(|v| v.parse().ok())
562            .unwrap_or(20u16);
563
564        let persistence = params
565            .get("persistence")
566            .and_then(|v| v.parse().ok())
567            .unwrap_or(64u8);
568
569        let slottime = params
570            .get("slottime")
571            .and_then(|v| v.parse().ok())
572            .unwrap_or(20u16);
573
574        let flow_control = params
575            .get("flow_control")
576            .and_then(|v| crate::config::parse_bool_pub(v))
577            .unwrap_or(false);
578
579        let beacon_interval = params
580            .get("id_interval")
581            .or_else(|| params.get("beacon_interval"))
582            .and_then(|v| v.parse().ok());
583
584        let beacon_data = params
585            .get("id_callsign")
586            .or_else(|| params.get("beacon_data"))
587            .map(|v| v.as_bytes().to_vec());
588
589        Ok(Box::new(KissIfaceConfig {
590            name: name.to_string(),
591            port,
592            speed,
593            data_bits,
594            parity,
595            stop_bits,
596            preamble,
597            txtail,
598            persistence,
599            slottime,
600            flow_control,
601            beacon_interval,
602            beacon_data,
603            interface_id: id,
604            ax25_source: None,
605            interface_type_name: "KISSInterface".into(),
606        }))
607    }
608
609    fn start(
610        &self,
611        config: Box<dyn InterfaceConfigData>,
612        ctx: StartContext,
613    ) -> std::io::Result<StartResult> {
614        let kiss_config = *config
615            .into_any()
616            .downcast::<KissIfaceConfig>()
617            .map_err(|_| {
618                std::io::Error::new(std::io::ErrorKind::InvalidData, "wrong config type")
619            })?;
620
621        let id = kiss_config.interface_id;
622        let name = kiss_config.name.clone();
623        let interface_type_name = kiss_config.interface_type_name.clone();
624
625        let info = InterfaceInfo {
626            id,
627            name,
628            mode: ctx.mode,
629            gravity: ctx.gravity,
630            recursive_prs: ctx.recursive_prs,
631            announces_from_internal: ctx.announces_from_internal,
632            announces_to_internal: ctx.announces_to_internal,
633            out_capable: true,
634            in_capable: true,
635            bitrate: Some(1200),
636            airtime_profile: None,
637            announce_rate_target: None,
638            announce_rate_grace: 0,
639            announce_rate_penalty: 0.0,
640            announce_cap: rns_core::constants::ANNOUNCE_CAP,
641            is_local_client: false,
642            wants_tunnel: false,
643            tunnel_id: None,
644            mtu: rns_core::constants::MTU as u32,
645            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
646            ia_freq: 0.0,
647            ip_freq: 0.0,
648            op_freq: 0.0,
649            op_samples: 0,
650            started: crate::time::now(),
651        };
652
653        let (writer, control) = start_with_control(kiss_config, ctx.tx)?;
654
655        Ok(StartResult::Simple {
656            control: Some(control),
657            id,
658            info,
659            writer,
660            interface_type_name,
661        })
662    }
663}
664
665#[cfg(test)]
666mod tests {
667    use super::*;
668    use crate::serial::open_pty_pair;
669    use std::os::unix::io::{AsRawFd, FromRawFd};
670    use std::sync::mpsc;
671
672    /// Helper: poll an fd for reading with timeout (ms).
673    fn poll_read(fd: i32, timeout_ms: i32) -> bool {
674        let mut pfd = libc::pollfd {
675            fd,
676            events: libc::POLLIN,
677            revents: 0,
678        };
679        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
680        ret > 0
681    }
682
683    #[test]
684    fn kiss_data_roundtrip() {
685        let (master_fd, slave_fd) = open_pty_pair().unwrap();
686        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
687        let mut slave_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
688
689        // Write a KISS data frame to the master side
690        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
691        let framed = kiss::frame(&payload);
692        master_file.write_all(&framed).unwrap();
693        master_file.flush().unwrap();
694
695        // Read from slave using KISS decoder
696        assert!(poll_read(slave_file.as_raw_fd(), 2000));
697
698        let mut decoder = kiss::Decoder::new();
699        let mut buf = [0u8; 4096];
700        let n = slave_file.read(&mut buf).unwrap();
701        let events = decoder.feed(&buf[..n]);
702        assert_eq!(events.len(), 1);
703        assert_eq!(events[0], kiss::KissEvent::DataFrame(payload));
704    }
705
706    #[test]
707    fn detach_stops_idle_kiss_reader_without_reconnect() {
708        let (master_fd, slave_fd) = open_pty_pair().unwrap();
709        let master = unsafe { std::fs::File::from_raw_fd(master_fd) };
710        let flow_writer = unsafe { std::fs::File::from_raw_fd(libc::dup(slave_fd)) };
711        let slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
712        let (tx, rx) = crate::event::channel();
713        let control = ListenerControl::new();
714        let reader_control = control.clone();
715        let flow_state = Arc::new(Mutex::new(FlowState {
716            ready: true,
717            queue: VecDeque::new(),
718            lock_time: Instant::now(),
719        }));
720        let config = KissIfaceConfig {
721            name: "detach-test".into(),
722            interface_id: InterfaceId(72),
723            ..Default::default()
724        };
725
726        let reader = thread::spawn(move || {
727            reader_loop(
728                slave,
729                flow_writer,
730                InterfaceId(72),
731                config,
732                tx,
733                flow_state,
734                reader_control,
735            )
736        });
737        thread::sleep(Duration::from_millis(50));
738        control.request_stop();
739        reader.join().unwrap();
740        drop(master);
741        assert!(rx.try_recv().is_err());
742    }
743
744    #[test]
745    fn missing_kiss_port_recovers_after_it_appears() {
746        let tempdir = tempfile::tempdir().unwrap();
747        let port_path = tempdir.path().join("kiss-port");
748        let (tx, rx) = crate::event::channel();
749        let config = KissIfaceConfig {
750            name: "late-kiss".into(),
751            port: port_path.display().to_string(),
752            interface_id: InterfaceId(74),
753            ..Default::default()
754        };
755
756        let (_writer, control) = start_with_control(config, tx).unwrap();
757        assert!(rx.try_recv().is_err());
758
759        let (master_fd, slave_fd) = open_pty_pair().unwrap();
760        let master = unsafe { std::fs::File::from_raw_fd(master_fd) };
761        let slave_path = std::fs::read_link(format!("/proc/self/fd/{slave_fd}")).unwrap();
762        std::os::unix::fs::symlink(slave_path, &port_path).unwrap();
763        let slave = unsafe { std::fs::File::from_raw_fd(slave_fd) };
764
765        let event = rx.recv_timeout(Duration::from_secs(10)).unwrap();
766        assert!(matches!(
767            event,
768            Event::InterfaceUp(InterfaceId(74), Some(_), None)
769        ));
770        control.request_stop();
771        drop(slave);
772        drop(master);
773    }
774
775    #[test]
776    fn kiss_writer_frames() {
777        let (master_fd, slave_fd) = open_pty_pair().unwrap();
778
779        let writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
780        let flow_state = Arc::new(Mutex::new(FlowState {
781            ready: true,
782            queue: VecDeque::new(),
783            lock_time: Instant::now(),
784        }));
785
786        let mut writer = KissWriter {
787            confirmed_pending: None,
788            file: writer_file,
789            flow_control: false,
790            flow_state,
791            ax25_source: None,
792        };
793
794        let payload = vec![0xC0, 0xDB, 0x01]; // includes bytes that need KISS escaping
795        writer.send_frame(&payload).unwrap();
796
797        // Read from master
798        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
799        assert!(poll_read(master_file.as_raw_fd(), 2000));
800
801        let expected = kiss::frame(&payload);
802        let mut buf = [0u8; 256];
803        let n = master_file.read(&mut buf).unwrap();
804        assert_eq!(&buf[..n], &expected[..]);
805    }
806
807    #[test]
808    fn kiss_config_commands() {
809        use std::time::Instant;
810
811        let (master_fd, slave_fd) = open_pty_pair().unwrap();
812
813        let mut writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
814        let config = KissIfaceConfig {
815            preamble: 350,
816            txtail: 20,
817            persistence: 64,
818            slottime: 20,
819            ..Default::default()
820        };
821
822        configure_tnc(&mut writer_file, &config).unwrap();
823
824        // Read all commands from master
825        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
826        let deadline = Instant::now() + Duration::from_secs(2);
827        let mut data = Vec::new();
828        let mut buf = [0u8; 1024];
829        while Instant::now() < deadline {
830            let remaining_ms = deadline
831                .saturating_duration_since(Instant::now())
832                .as_millis()
833                .min(i32::MAX as u128) as i32;
834            if remaining_ms <= 0 || !poll_read(master_file.as_raw_fd(), remaining_ms) {
835                break;
836            }
837
838            let n = master_file.read(&mut buf).unwrap();
839            if n == 0 {
840                break;
841            }
842            data.extend_from_slice(&buf[..n]);
843
844            let have_all = data.windows(4).any(|w| {
845                w[0] == kiss::FEND && w[1] == kiss::CMD_TXDELAY && w[2] == 35 && w[3] == kiss::FEND
846            }) && data.windows(4).any(|w| {
847                w[0] == kiss::FEND && w[1] == kiss::CMD_TXTAIL && w[2] == 2 && w[3] == kiss::FEND
848            }) && data.windows(4).any(|w| {
849                w[0] == kiss::FEND && w[1] == kiss::CMD_P && w[2] == 64 && w[3] == kiss::FEND
850            }) && data.windows(4).any(|w| {
851                w[0] == kiss::FEND && w[1] == kiss::CMD_SLOTTIME && w[2] == 2 && w[3] == kiss::FEND
852            });
853            if have_all {
854                break;
855            }
856        }
857
858        // Should contain TXDELAY command: FEND, CMD_TXDELAY, value, FEND
859        // preamble: 350/10 = 35
860        assert!(
861            data.windows(4).any(|w| w[0] == kiss::FEND
862                && w[1] == kiss::CMD_TXDELAY
863                && w[2] == 35
864                && w[3] == kiss::FEND),
865            "should contain TXDELAY command"
866        );
867
868        // TXTAIL: 20/10 = 2
869        assert!(
870            data.windows(4).any(|w| w[0] == kiss::FEND
871                && w[1] == kiss::CMD_TXTAIL
872                && w[2] == 2
873                && w[3] == kiss::FEND),
874            "should contain TXTAIL command"
875        );
876
877        // Persistence: 64
878        assert!(
879            data.windows(4).any(|w| w[0] == kiss::FEND
880                && w[1] == kiss::CMD_P
881                && w[2] == 64
882                && w[3] == kiss::FEND),
883            "should contain P command"
884        );
885
886        // Slottime: 20/10 = 2
887        assert!(
888            data.windows(4).any(|w| w[0] == kiss::FEND
889                && w[1] == kiss::CMD_SLOTTIME
890                && w[2] == 2
891                && w[3] == kiss::FEND),
892            "should contain SLOTTIME command"
893        );
894    }
895
896    #[test]
897    fn kiss_flow_control_lock() {
898        let flow_state = Arc::new(Mutex::new(FlowState {
899            ready: true,
900            queue: VecDeque::new(),
901            lock_time: Instant::now(),
902        }));
903
904        let (master_fd, slave_fd) = open_pty_pair().unwrap();
905        let writer_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
906
907        let mut writer = KissWriter {
908            confirmed_pending: None,
909            file: writer_file,
910            flow_control: true,
911            flow_state: flow_state.clone(),
912            ax25_source: None,
913        };
914
915        // First send should go through (ready=true) and lock
916        writer.send_frame(b"hello").unwrap();
917        assert!(!flow_state.lock().unwrap().ready);
918
919        // Second send should be queued (ready=false)
920        writer.send_frame(b"world").unwrap();
921        assert_eq!(flow_state.lock().unwrap().queue.len(), 1);
922
923        // Simulate CMD_READY: process_queue
924        let mut flow_writer = unsafe { std::fs::File::from_raw_fd(libc::dup(master_fd)) };
925        let mut first_tx = None;
926        let config = KissIfaceConfig::default();
927        process_queue(&flow_state, &mut flow_writer, &mut first_tx, &config);
928
929        // Queue should be empty now (dequeued "world"), but ready=false because it sent
930        assert_eq!(flow_state.lock().unwrap().queue.len(), 0);
931        assert!(!flow_state.lock().unwrap().ready);
932
933        // Process again with empty queue: should set ready=true
934        process_queue(&flow_state, &mut flow_writer, &mut first_tx, &config);
935        assert!(flow_state.lock().unwrap().ready);
936
937        // Clean up
938        unsafe { libc::close(master_fd) };
939    }
940
941    #[test]
942    fn kiss_flow_control_timeout() {
943        let flow_state = Arc::new(Mutex::new(FlowState {
944            ready: false,
945            queue: VecDeque::new(),
946            lock_time: Instant::now() - Duration::from_secs(6), // already timed out
947        }));
948
949        // Check that the timeout condition triggers
950        let state = flow_state.lock().unwrap();
951        assert!(!state.ready);
952        assert!(state.lock_time.elapsed() > Duration::from_secs(5));
953    }
954
955    #[test]
956    fn confirmed_kiss_frame_waits_for_ready_and_flushes_once() {
957        let (master_fd, slave_fd) = open_pty_pair().unwrap();
958        let mut master = unsafe { std::fs::File::from_raw_fd(master_fd) };
959        let state = Arc::new(Mutex::new(FlowState {
960            ready: false,
961            queue: VecDeque::new(),
962            lock_time: Instant::now(),
963        }));
964        let mut writer = KissWriter {
965            confirmed_pending: None,
966            file: unsafe { std::fs::File::from_raw_fd(slave_fd) },
967            flow_control: true,
968            flow_state: state.clone(),
969            ax25_source: None,
970        };
971        assert_eq!(
972            writer
973                .send_frame_confirmed(b"confirmed")
974                .unwrap_err()
975                .kind(),
976            io::ErrorKind::WouldBlock
977        );
978        assert!(state.lock().unwrap().queue.is_empty());
979        assert!(!poll_read(master.as_raw_fd(), 20));
980        state.lock().unwrap().ready = true;
981        Writer::flush(&mut writer).unwrap();
982        let mut actual = vec![0; kiss::frame(b"confirmed").len()];
983        master.read_exact(&mut actual).unwrap();
984        assert_eq!(actual, kiss::frame(b"confirmed"));
985        Writer::flush(&mut writer).unwrap();
986        assert!(!poll_read(master.as_raw_fd(), 20));
987    }
988
989    #[test]
990    fn kiss_fragmented() {
991        let (master_fd, slave_fd) = open_pty_pair().unwrap();
992        let mut master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
993        let slave_file = unsafe { std::fs::File::from_raw_fd(slave_fd) };
994
995        let payload = vec![0x01, 0x02, 0x03, 0x04, 0x05];
996        let framed = kiss::frame(&payload);
997        let mid = framed.len() / 2;
998
999        // Spawn reader thread first (it will block waiting for data)
1000        let (tx, rx) = mpsc::channel::<kiss::KissEvent>();
1001        let reader_thread = thread::spawn(move || {
1002            let mut reader = slave_file;
1003            let mut decoder = kiss::Decoder::new();
1004            let mut buf = [0u8; 4096];
1005
1006            loop {
1007                match reader.read(&mut buf) {
1008                    Ok(n) if n > 0 => {
1009                        for event in decoder.feed(&buf[..n]) {
1010                            let _ = tx.send(event.clone());
1011                            if matches!(event, kiss::KissEvent::DataFrame(_)) {
1012                                return;
1013                            }
1014                        }
1015                    }
1016                    _ => return,
1017                }
1018            }
1019        });
1020
1021        // Write first half
1022        master_file.write_all(&framed[..mid]).unwrap();
1023        master_file.flush().unwrap();
1024
1025        thread::sleep(Duration::from_millis(50));
1026
1027        // Write second half
1028        master_file.write_all(&framed[mid..]).unwrap();
1029        master_file.flush().unwrap();
1030
1031        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
1032        assert_eq!(event, kiss::KissEvent::DataFrame(payload));
1033
1034        let _ = reader_thread.join();
1035    }
1036}