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