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