Skip to main content

leodos_protocols/datalink/reliability/cop1/
fop.rs

1//! FOP-1 (Frame Operation Procedure) sender state machine.
2//!
3//! CCSDS 232.1-B-2 Section 5. Handles sending of TC transfer frames
4//! with go-back-N ARQ using CLCW feedback from FARM-1.
5//!
6//! Six states:
7//! - S1: Active, no pending init
8//! - S2: Active, retransmitting
9//! - S3: Active, initializing without BC frame
10//! - S4: Active, initializing with BC frame
11//! - S5: Active, initializing with unlock
12//! - S6: Initial (inactive)
13
14use heapless::Vec;
15
16use super::clcw::Clcw;
17
18/// Maximum actions per event.
19const MAX_ACTIONS: usize = 16;
20
21/// FOP-1 states per CCSDS 232.1-B-2 Table 5-1.
22#[derive(Debug, Copy, Clone, Eq, PartialEq)]
23pub enum FopState {
24    /// S1: active, transmitting AD frames normally.
25    Active,
26    /// S2: active, retransmitting (go-back-N triggered).
27    Retransmitting,
28    /// S3: initializing without BC frame.
29    InitNoBC,
30    /// S4: initializing with BC frame (Set V(R)).
31    InitWithSetVr,
32    /// S5: initializing with BC frame (Unlock).
33    InitWithUnlock,
34    /// S6: initial/inactive state.
35    Initial,
36}
37
38/// Events that drive the FOP-1 state machine.
39#[derive(Debug, Clone)]
40pub enum FopEvent<'a> {
41    /// Higher procedures request transfer of an AD FDU.
42    SendAd {
43        /// Data to send in a Type-AD frame.
44        fdu: &'a [u8],
45    },
46
47    /// Higher procedures request transfer of a BD FDU.
48    SendBd {
49        /// Data to send in a Type-BD frame.
50        fdu: &'a [u8],
51    },
52
53    /// A CLCW was received (from the return link).
54    ClcwReceived {
55        /// The received CLCW.
56        clcw: Clcw,
57    },
58
59    /// The retransmission timer T1 expired.
60    TimerExpired,
61
62    /// Management directive: Initiate AD Service (no CLCW).
63    InitAdNoClcw,
64
65    /// Management directive: Initiate AD Service with CLCW.
66    InitAdWithClcw,
67
68    /// Management directive: Initiate AD with Set V(R).
69    InitAdSetVr {
70        /// The V(R) value to set at the receiver.
71        vr: u8,
72    },
73
74    /// Management directive: Initiate AD with Unlock.
75    InitAdUnlock,
76
77    /// Management directive: Terminate AD Service.
78    Terminate,
79
80    /// Management directive: Resume AD Service.
81    Resume,
82}
83
84/// Actions that FOP-1 requests the driver to perform.
85#[derive(Debug, Copy, Clone, Eq, PartialEq)]
86pub enum FopAction {
87    /// Transmit a Type-AD frame with the given sequence number.
88    /// The driver should call [`FopMachine::get_fdu`] to get the data.
89    TransmitAd {
90        /// Frame Sequence Number N(S) to assign.
91        seq: u8,
92    },
93
94    /// Transmit a Type-BD frame.
95    TransmitBd,
96
97    /// Transmit a Type-BC Unlock frame.
98    TransmitBcUnlock,
99
100    /// Transmit a Type-BC Set V(R) frame.
101    TransmitBcSetVr {
102        /// The V(R) value to set.
103        vr: u8,
104    },
105
106    /// Start (or restart) timer T1 with the configured initial value.
107    StartTimer,
108
109    /// Stop timer T1.
110    StopTimer,
111
112    /// AD service has been accepted (init complete).
113    Accept,
114
115    /// The FDU was rejected (queue full, wrong state, etc).
116    Reject,
117
118    /// An alert condition: link has failed.
119    Alert,
120
121    /// AD service was successfully terminated.
122    Terminated,
123
124    /// An AD frame was acknowledged by the receiver.
125    Acknowledged {
126        /// Sequence number acknowledged.
127        seq: u8,
128    },
129}
130
131/// Collection of actions emitted by FOP-1.
132#[derive(Debug)]
133pub struct FopActions {
134    inner: Vec<FopAction, MAX_ACTIONS>,
135}
136
137impl FopActions {
138    /// Create a new empty actions collection.
139    pub fn new() -> Self {
140        Self { inner: Vec::new() }
141    }
142
143    fn push(&mut self, action: FopAction) {
144        let _ = self.inner.push(action);
145    }
146
147    fn clear(&mut self) {
148        self.inner.clear();
149    }
150
151    /// Iterate over the actions.
152    pub fn iter(&self) -> impl Iterator<Item = &FopAction> {
153        self.inner.iter()
154    }
155
156    /// Check if empty.
157    pub fn is_empty(&self) -> bool {
158        self.inner.is_empty()
159    }
160
161    /// Number of actions.
162    pub fn len(&self) -> usize {
163        self.inner.len()
164    }
165}
166
167impl Default for FopActions {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl<'a> IntoIterator for &'a FopActions {
174    type Item = &'a FopAction;
175    type IntoIter = core::slice::Iter<'a, FopAction>;
176
177    fn into_iter(self) -> Self::IntoIter {
178        self.inner.iter()
179    }
180}
181
182/// Configuration for FOP-1.
183#[derive(Debug, Clone)]
184#[derive(bon::Builder)]
185pub struct FopConfig {
186    /// FOP Sliding Window Width (K), 1..=255.
187    pub window_width: u8,
188    /// Maximum transmission attempts before alerting.
189    pub transmission_limit: u8,
190    /// Timeout type: 0 = timer off after each new ACK,
191    /// 1 = timer always running.
192    pub timeout_type: u8,
193}
194
195/// An FDU slot in the sent queue.
196#[derive(Clone)]
197struct FduSlot {
198    /// Whether this slot is occupied.
199    occupied: bool,
200    /// Frame Sequence Number assigned.
201    seq: u8,
202    /// Start offset in the data buffer.
203    offset: usize,
204    /// Length of the FDU data.
205    len: usize,
206    /// Number of times this frame has been transmitted.
207    transmission_count: u8,
208}
209
210impl Default for FduSlot {
211    fn default() -> Self {
212        Self {
213            occupied: false,
214            seq: 0,
215            offset: 0,
216            len: 0,
217            transmission_count: 0,
218        }
219    }
220}
221
222/// FOP-1 state machine.
223///
224/// Implements the sender side of COP-1 go-back-N ARQ.
225/// Completely synchronous — no I/O, no async.
226///
227/// # Type Parameters
228///
229/// * `WIN` — Maximum sent queue depth (sliding window).
230/// * `BUF` — Total FDU buffer size in bytes.
231pub struct FopMachine<const WIN: usize, const BUF: usize> {
232    state: FopState,
233    config: FopConfig,
234
235    /// V(S): Transmitter Frame Sequence Number.
236    vs: u8,
237    /// NN(R): Expected ACK frame sequence number (last N(R) seen).
238    nnr: u8,
239
240    /// Sent queue: frames transmitted but not yet acknowledged.
241    sent_queue: [FduSlot; WIN],
242    /// FDU data buffer.
243    data: [u8; BUF],
244    /// Current write position in data buffer.
245    write_pos: usize,
246
247    /// Transmission count for the current init BC frame.
248    bc_transmission_count: u8,
249    /// V(R) value for Set V(R) init directive.
250    set_vr_value: u8,
251
252    /// Pending BD FDU data range (offset, len) in data buffer.
253    pending_bd: Option<(usize, usize)>,
254}
255
256impl<const WIN: usize, const BUF: usize> FopMachine<WIN, BUF> {
257    /// Create a new FOP-1 state machine in the Initial (S6) state.
258    pub fn new(config: FopConfig) -> Self {
259        Self {
260            state: FopState::Initial,
261            config,
262            vs: 0,
263            nnr: 0,
264            sent_queue: core::array::from_fn(|_| FduSlot::default()),
265            data: [0u8; BUF],
266            write_pos: 0,
267            bc_transmission_count: 0,
268            set_vr_value: 0,
269            pending_bd: None,
270        }
271    }
272
273    /// Current FOP-1 state.
274    pub fn state(&self) -> FopState {
275        self.state
276    }
277
278    /// Current V(S).
279    pub fn vs(&self) -> u8 {
280        self.vs
281    }
282
283    /// Get FDU payload for a given sequence number.
284    pub fn get_fdu(&self, seq: u8) -> Option<&[u8]> {
285        for slot in &self.sent_queue {
286            if slot.occupied && slot.seq == seq {
287                return Some(&self.data[slot.offset..slot.offset + slot.len]);
288            }
289        }
290        None
291    }
292
293    /// Get the pending BD FDU payload (if any).
294    pub fn get_bd_fdu(&self) -> Option<&[u8]> {
295        self.pending_bd
296            .map(|(off, len)| &self.data[off..off + len])
297    }
298
299    /// Number of frames in the sent queue.
300    pub fn sent_count(&self) -> usize {
301        self.sent_queue.iter().filter(|s| s.occupied).count()
302    }
303
304    /// Process an event and emit actions.
305    pub fn handle(
306        &mut self,
307        event: FopEvent<'_>,
308        actions: &mut FopActions,
309    ) {
310        actions.clear();
311
312        match event {
313            FopEvent::SendAd { fdu } => {
314                self.handle_send_ad(fdu, actions);
315            }
316            FopEvent::SendBd { fdu } => {
317                self.handle_send_bd(fdu, actions);
318            }
319            FopEvent::ClcwReceived { clcw } => {
320                self.handle_clcw(clcw, actions);
321            }
322            FopEvent::TimerExpired => {
323                self.handle_timer_expired(actions);
324            }
325            FopEvent::InitAdNoClcw => {
326                self.handle_init_no_clcw(actions);
327            }
328            FopEvent::InitAdWithClcw => {
329                self.handle_init_with_clcw(actions);
330            }
331            FopEvent::InitAdSetVr { vr } => {
332                self.handle_init_set_vr(vr, actions);
333            }
334            FopEvent::InitAdUnlock => {
335                self.handle_init_unlock(actions);
336            }
337            FopEvent::Terminate => {
338                self.handle_terminate(actions);
339            }
340            FopEvent::Resume => {
341                self.handle_resume(actions);
342            }
343        }
344    }
345
346    /// Handle SendAd: queue an AD frame for transmission.
347    fn handle_send_ad(&mut self, fdu: &[u8], actions: &mut FopActions) {
348        match self.state {
349            FopState::Active | FopState::Retransmitting => {
350                let Some(slot_idx) = self.find_empty_slot() else {
351                    actions.push(FopAction::Reject);
352                    return;
353                };
354                let Some(offset) = self.buffer_fdu(fdu) else {
355                    actions.push(FopAction::Reject);
356                    return;
357                };
358
359                let seq = self.vs;
360                self.sent_queue[slot_idx] = FduSlot {
361                    occupied: true,
362                    seq,
363                    offset,
364                    len: fdu.len(),
365                    transmission_count: 0,
366                };
367                self.vs = self.vs.wrapping_add(1);
368
369                // Only transmit immediately in Active state
370                if self.state == FopState::Active {
371                    self.sent_queue[slot_idx].transmission_count = 1;
372                    actions.push(FopAction::TransmitAd { seq });
373                    actions.push(FopAction::StartTimer);
374                }
375            }
376            _ => {
377                actions.push(FopAction::Reject);
378            }
379        }
380    }
381
382    /// Handle SendBd: transmit a BD frame (bypass, no sequencing).
383    fn handle_send_bd(&mut self, fdu: &[u8], actions: &mut FopActions) {
384        // BD can be sent in any active state
385        let Some(offset) = self.buffer_fdu(fdu) else {
386            actions.push(FopAction::Reject);
387            return;
388        };
389        self.pending_bd = Some((offset, fdu.len()));
390        actions.push(FopAction::TransmitBd);
391    }
392
393    /// Handle CLCW received from the return link.
394    fn handle_clcw(&mut self, clcw: Clcw, actions: &mut FopActions) {
395        let nr = clcw.report_value();
396
397        match self.state {
398            FopState::Active => {
399                if clcw.lockout() {
400                    // Lockout → alert
401                    self.alert(actions);
402                    return;
403                }
404
405                // Remove acknowledged frames
406                self.remove_acknowledged(nr, actions);
407                self.nnr = nr;
408
409                if clcw.retransmit() {
410                    // Go-back-N: retransmit all from N(R)
411                    self.initiate_retransmission(actions);
412                    self.state = FopState::Retransmitting;
413                } else if self.sent_count() == 0 {
414                    actions.push(FopAction::StopTimer);
415                }
416            }
417            FopState::Retransmitting => {
418                if clcw.lockout() {
419                    self.alert(actions);
420                    return;
421                }
422
423                self.remove_acknowledged(nr, actions);
424                self.nnr = nr;
425
426                if !clcw.retransmit() {
427                    // Retransmit flag cleared → back to Active
428                    self.state = FopState::Active;
429                    if self.sent_count() == 0 {
430                        actions.push(FopAction::StopTimer);
431                    }
432                }
433            }
434            FopState::InitWithSetVr => {
435                if nr == self.set_vr_value
436                    && !clcw.lockout()
437                    && !clcw.retransmit()
438                {
439                    // Init succeeded
440                    self.vs = self.set_vr_value;
441                    self.nnr = nr;
442                    self.purge_sent_queue();
443                    self.state = FopState::Active;
444                    actions.push(FopAction::StopTimer);
445                    actions.push(FopAction::Accept);
446                }
447            }
448            FopState::InitWithUnlock => {
449                if !clcw.lockout() && !clcw.retransmit() {
450                    // Unlock accepted
451                    self.nnr = nr;
452                    self.vs = nr;
453                    self.purge_sent_queue();
454                    self.state = FopState::Active;
455                    actions.push(FopAction::StopTimer);
456                    actions.push(FopAction::Accept);
457                }
458            }
459            FopState::InitNoBC | FopState::Initial => {
460                // CLCW ignored in these states
461            }
462        }
463    }
464
465    /// Handle T1 timer expiration.
466    fn handle_timer_expired(&mut self, actions: &mut FopActions) {
467        match self.state {
468            FopState::Active | FopState::Retransmitting => {
469                // Check transmission limit on oldest unacked frame
470                let limit_exceeded = self.sent_queue.iter().any(|s| {
471                    s.occupied
472                        && s.transmission_count >= self.config.transmission_limit
473                });
474
475                if limit_exceeded {
476                    self.alert(actions);
477                } else {
478                    self.initiate_retransmission(actions);
479                    self.state = FopState::Retransmitting;
480                }
481            }
482            FopState::InitWithSetVr => {
483                self.bc_transmission_count += 1;
484                if self.bc_transmission_count >= self.config.transmission_limit
485                {
486                    self.alert(actions);
487                } else {
488                    actions.push(FopAction::TransmitBcSetVr {
489                        vr: self.set_vr_value,
490                    });
491                    actions.push(FopAction::StartTimer);
492                }
493            }
494            FopState::InitWithUnlock => {
495                self.bc_transmission_count += 1;
496                if self.bc_transmission_count >= self.config.transmission_limit
497                {
498                    self.alert(actions);
499                } else {
500                    actions.push(FopAction::TransmitBcUnlock);
501                    actions.push(FopAction::StartTimer);
502                }
503            }
504            FopState::InitNoBC | FopState::Initial => {}
505        }
506    }
507
508    /// Initiate AD service without CLCW (direct to Active).
509    fn handle_init_no_clcw(&mut self, actions: &mut FopActions) {
510        match self.state {
511            FopState::Initial => {
512                self.purge_sent_queue();
513                self.state = FopState::Active;
514                actions.push(FopAction::Accept);
515            }
516            _ => {
517                actions.push(FopAction::Reject);
518            }
519        }
520    }
521
522    /// Initiate AD service, wait for a matching CLCW.
523    fn handle_init_with_clcw(&mut self, actions: &mut FopActions) {
524        match self.state {
525            FopState::Initial => {
526                self.purge_sent_queue();
527                self.state = FopState::InitNoBC;
528                actions.push(FopAction::StartTimer);
529            }
530            _ => {
531                actions.push(FopAction::Reject);
532            }
533        }
534    }
535
536    /// Initiate AD service by sending BC Set V(R).
537    fn handle_init_set_vr(&mut self, vr: u8, actions: &mut FopActions) {
538        match self.state {
539            FopState::Initial => {
540                self.set_vr_value = vr;
541                self.bc_transmission_count = 1;
542                self.purge_sent_queue();
543                self.state = FopState::InitWithSetVr;
544                actions.push(FopAction::TransmitBcSetVr { vr });
545                actions.push(FopAction::StartTimer);
546            }
547            _ => {
548                actions.push(FopAction::Reject);
549            }
550        }
551    }
552
553    /// Initiate AD service by sending BC Unlock.
554    fn handle_init_unlock(&mut self, actions: &mut FopActions) {
555        match self.state {
556            FopState::Initial => {
557                self.bc_transmission_count = 1;
558                self.purge_sent_queue();
559                self.state = FopState::InitWithUnlock;
560                actions.push(FopAction::TransmitBcUnlock);
561                actions.push(FopAction::StartTimer);
562            }
563            _ => {
564                actions.push(FopAction::Reject);
565            }
566        }
567    }
568
569    /// Terminate AD service.
570    fn handle_terminate(&mut self, actions: &mut FopActions) {
571        actions.push(FopAction::StopTimer);
572        self.purge_sent_queue();
573        self.state = FopState::Initial;
574        actions.push(FopAction::Terminated);
575    }
576
577    /// Resume AD service (go back to Active from Initial).
578    fn handle_resume(&mut self, actions: &mut FopActions) {
579        match self.state {
580            FopState::Initial => {
581                self.state = FopState::Active;
582                actions.push(FopAction::Accept);
583            }
584            _ => {
585                actions.push(FopAction::Reject);
586            }
587        }
588    }
589
590    /// Remove all frames acknowledged by N(R).
591    fn remove_acknowledged(&mut self, nr: u8, actions: &mut FopActions) {
592        for slot in &mut self.sent_queue {
593            if !slot.occupied {
594                continue;
595            }
596            // Frame is acknowledged if its seq is "before" N(R)
597            // in the modulo-256 sequence space.
598            let dist = nr.wrapping_sub(slot.seq);
599            if dist > 0 && dist < 128 {
600                let seq = slot.seq;
601                slot.occupied = false;
602                actions.push(FopAction::Acknowledged { seq });
603            }
604        }
605    }
606
607    /// Go-back-N: retransmit all unacknowledged frames in order.
608    fn initiate_retransmission(&mut self, actions: &mut FopActions) {
609        // Collect occupied slots sorted by sequence number
610        let mut seqs: Vec<u8, WIN> = Vec::new();
611        for slot in &self.sent_queue {
612            if slot.occupied {
613                let _ = seqs.push(slot.seq);
614            }
615        }
616
617        // Sort by distance from NN(R) to get transmission order
618        // (closest to NN(R) first)
619        let nnr = self.nnr;
620        // Simple insertion sort (WIN is small)
621        for i in 1..seqs.len() {
622            let key = seqs[i];
623            let key_dist = key.wrapping_sub(nnr);
624            let mut j = i;
625            while j > 0 {
626                let prev_dist = seqs[j - 1].wrapping_sub(nnr);
627                if prev_dist <= key_dist {
628                    break;
629                }
630                seqs[j] = seqs[j - 1];
631                j -= 1;
632            }
633            seqs[j] = key;
634        }
635
636        for &seq in &seqs {
637            for slot in &mut self.sent_queue {
638                if slot.occupied && slot.seq == seq {
639                    slot.transmission_count += 1;
640                    break;
641                }
642            }
643            actions.push(FopAction::TransmitAd { seq });
644        }
645
646        if !seqs.is_empty() {
647            actions.push(FopAction::StartTimer);
648        }
649    }
650
651    /// Enter alert state: stop timer, purge queue, go to Initial.
652    fn alert(&mut self, actions: &mut FopActions) {
653        actions.push(FopAction::StopTimer);
654        self.purge_sent_queue();
655        self.state = FopState::Initial;
656        actions.push(FopAction::Alert);
657    }
658
659    /// Clear the sent queue and reset write position.
660    fn purge_sent_queue(&mut self) {
661        for slot in &mut self.sent_queue {
662            slot.occupied = false;
663        }
664        self.write_pos = 0;
665    }
666
667    /// Find an empty slot in the sent queue.
668    fn find_empty_slot(&self) -> Option<usize> {
669        self.sent_queue.iter().position(|s| !s.occupied)
670    }
671
672    /// Buffer an FDU, returning the offset into the data buffer.
673    fn buffer_fdu(&mut self, fdu: &[u8]) -> Option<usize> {
674        if self.write_pos + fdu.len() > BUF {
675            return None;
676        }
677        let offset = self.write_pos;
678        self.data[offset..offset + fdu.len()].copy_from_slice(fdu);
679        self.write_pos += fdu.len();
680        Some(offset)
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    fn make_config() -> FopConfig {
689        FopConfig::builder()
690            .window_width(4)
691            .transmission_limit(3)
692            .timeout_type(0)
693            .build()
694    }
695
696    #[test]
697    fn starts_in_initial_state() {
698        let fop: FopMachine<8, 1024> = FopMachine::new(make_config());
699        assert_eq!(fop.state(), FopState::Initial);
700    }
701
702    #[test]
703    fn init_no_clcw_activates() {
704        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
705        let mut actions = FopActions::new();
706
707        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
708
709        assert_eq!(fop.state(), FopState::Active);
710        assert!(actions.iter().any(|a| matches!(a, FopAction::Accept)));
711    }
712
713    #[test]
714    fn send_ad_in_active_state() {
715        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
716        let mut actions = FopActions::new();
717
718        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
719        fop.handle(
720            FopEvent::SendAd { fdu: &[1, 2, 3] },
721            &mut actions,
722        );
723
724        assert!(actions
725            .iter()
726            .any(|a| matches!(a, FopAction::TransmitAd { seq: 0 })));
727        assert_eq!(fop.vs(), 1);
728        assert_eq!(fop.sent_count(), 1);
729    }
730
731    #[test]
732    fn send_ad_rejected_in_initial() {
733        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
734        let mut actions = FopActions::new();
735
736        fop.handle(
737            FopEvent::SendAd { fdu: &[1, 2, 3] },
738            &mut actions,
739        );
740
741        assert!(actions.iter().any(|a| matches!(a, FopAction::Reject)));
742    }
743
744    #[test]
745    fn clcw_acknowledges_frames() {
746        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
747        let mut actions = FopActions::new();
748
749        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
750
751        // Send two frames
752        fop.handle(
753            FopEvent::SendAd { fdu: &[1] },
754            &mut actions,
755        );
756        fop.handle(
757            FopEvent::SendAd { fdu: &[2] },
758            &mut actions,
759        );
760        assert_eq!(fop.sent_count(), 2);
761
762        // CLCW with N(R)=2 acknowledges both
763        let mut clcw = Clcw::new();
764        clcw.set_report_value(2);
765        fop.handle(
766            FopEvent::ClcwReceived { clcw },
767            &mut actions,
768        );
769
770        assert_eq!(fop.sent_count(), 0);
771        assert!(actions
772            .iter()
773            .any(|a| matches!(a, FopAction::Acknowledged { seq: 0 })));
774        assert!(actions
775            .iter()
776            .any(|a| matches!(a, FopAction::Acknowledged { seq: 1 })));
777    }
778
779    #[test]
780    fn retransmit_flag_triggers_go_back_n() {
781        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
782        let mut actions = FopActions::new();
783
784        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
785        fop.handle(
786            FopEvent::SendAd { fdu: &[1] },
787            &mut actions,
788        );
789        fop.handle(
790            FopEvent::SendAd { fdu: &[2] },
791            &mut actions,
792        );
793
794        // CLCW with retransmit flag set, N(R)=0
795        let mut clcw = Clcw::new();
796        clcw.set_retransmit(true);
797        clcw.set_report_value(0);
798        fop.handle(
799            FopEvent::ClcwReceived { clcw },
800            &mut actions,
801        );
802
803        assert_eq!(fop.state(), FopState::Retransmitting);
804        // Should retransmit both frames
805        let transmit_count = actions
806            .iter()
807            .filter(|a| matches!(a, FopAction::TransmitAd { .. }))
808            .count();
809        assert_eq!(transmit_count, 2);
810    }
811
812    #[test]
813    fn lockout_clcw_causes_alert() {
814        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
815        let mut actions = FopActions::new();
816
817        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
818
819        let mut clcw = Clcw::new();
820        clcw.set_lockout(true);
821        fop.handle(
822            FopEvent::ClcwReceived { clcw },
823            &mut actions,
824        );
825
826        assert_eq!(fop.state(), FopState::Initial);
827        assert!(actions.iter().any(|a| matches!(a, FopAction::Alert)));
828    }
829
830    #[test]
831    fn timer_expired_retransmits() {
832        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
833        let mut actions = FopActions::new();
834
835        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
836        fop.handle(
837            FopEvent::SendAd { fdu: &[1] },
838            &mut actions,
839        );
840
841        fop.handle(FopEvent::TimerExpired, &mut actions);
842
843        assert_eq!(fop.state(), FopState::Retransmitting);
844        assert!(actions
845            .iter()
846            .any(|a| matches!(a, FopAction::TransmitAd { seq: 0 })));
847    }
848
849    #[test]
850    fn timer_expired_alerts_after_limit() {
851        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
852        let mut actions = FopActions::new();
853
854        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
855        fop.handle(
856            FopEvent::SendAd { fdu: &[1] },
857            &mut actions,
858        );
859
860        // Exhaust transmission limit (3)
861        for _ in 0..3 {
862            fop.handle(FopEvent::TimerExpired, &mut actions);
863        }
864
865        assert_eq!(fop.state(), FopState::Initial);
866        assert!(actions.iter().any(|a| matches!(a, FopAction::Alert)));
867    }
868
869    #[test]
870    fn init_with_unlock() {
871        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
872        let mut actions = FopActions::new();
873
874        fop.handle(FopEvent::InitAdUnlock, &mut actions);
875
876        assert_eq!(fop.state(), FopState::InitWithUnlock);
877        assert!(actions
878            .iter()
879            .any(|a| matches!(a, FopAction::TransmitBcUnlock)));
880
881        // Simulate successful CLCW response
882        let clcw = Clcw::new(); // lockout=false, retransmit=false
883        fop.handle(
884            FopEvent::ClcwReceived { clcw },
885            &mut actions,
886        );
887
888        assert_eq!(fop.state(), FopState::Active);
889        assert!(actions.iter().any(|a| matches!(a, FopAction::Accept)));
890    }
891
892    #[test]
893    fn init_with_set_vr() {
894        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
895        let mut actions = FopActions::new();
896
897        fop.handle(
898            FopEvent::InitAdSetVr { vr: 42 },
899            &mut actions,
900        );
901
902        assert_eq!(fop.state(), FopState::InitWithSetVr);
903        assert!(actions
904            .iter()
905            .any(|a| matches!(a, FopAction::TransmitBcSetVr { vr: 42 })));
906
907        // CLCW confirms V(R)=42
908        let mut clcw = Clcw::new();
909        clcw.set_report_value(42);
910        fop.handle(
911            FopEvent::ClcwReceived { clcw },
912            &mut actions,
913        );
914
915        assert_eq!(fop.state(), FopState::Active);
916        assert_eq!(fop.vs(), 42);
917    }
918
919    #[test]
920    fn terminate_resets_to_initial() {
921        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
922        let mut actions = FopActions::new();
923
924        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
925        fop.handle(
926            FopEvent::SendAd { fdu: &[1] },
927            &mut actions,
928        );
929
930        fop.handle(FopEvent::Terminate, &mut actions);
931
932        assert_eq!(fop.state(), FopState::Initial);
933        assert_eq!(fop.sent_count(), 0);
934        assert!(actions
935            .iter()
936            .any(|a| matches!(a, FopAction::Terminated)));
937    }
938
939    #[test]
940    fn bd_frame_can_be_sent() {
941        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
942        let mut actions = FopActions::new();
943
944        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
945        fop.handle(
946            FopEvent::SendBd { fdu: &[0xAA, 0xBB] },
947            &mut actions,
948        );
949
950        assert!(actions
951            .iter()
952            .any(|a| matches!(a, FopAction::TransmitBd)));
953        let bd_data = fop.get_bd_fdu().unwrap();
954        assert_eq!(bd_data, &[0xAA, 0xBB]);
955    }
956
957    #[test]
958    fn get_fdu_returns_correct_data() {
959        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
960        let mut actions = FopActions::new();
961
962        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
963        fop.handle(
964            FopEvent::SendAd { fdu: &[10, 20, 30] },
965            &mut actions,
966        );
967
968        let fdu = fop.get_fdu(0).unwrap();
969        assert_eq!(fdu, &[10, 20, 30]);
970    }
971
972    #[test]
973    fn wrapping_sequence_numbers() {
974        let mut fop: FopMachine<8, 1024> = FopMachine::new(make_config());
975        let mut actions = FopActions::new();
976
977        fop.handle(FopEvent::InitAdNoClcw, &mut actions);
978
979        // Set V(S) to 254 via init
980        fop.handle(FopEvent::Terminate, &mut actions);
981        fop.handle(
982            FopEvent::InitAdSetVr { vr: 254 },
983            &mut actions,
984        );
985        let mut clcw = Clcw::new();
986        clcw.set_report_value(254);
987        fop.handle(
988            FopEvent::ClcwReceived { clcw },
989            &mut actions,
990        );
991        assert_eq!(fop.vs(), 254);
992
993        // Send frames across the wrap boundary
994        fop.handle(
995            FopEvent::SendAd { fdu: &[1] },
996            &mut actions,
997        );
998        assert!(actions
999            .iter()
1000            .any(|a| matches!(a, FopAction::TransmitAd { seq: 254 })));
1001
1002        fop.handle(
1003            FopEvent::SendAd { fdu: &[2] },
1004            &mut actions,
1005        );
1006        assert!(actions
1007            .iter()
1008            .any(|a| matches!(a, FopAction::TransmitAd { seq: 255 })));
1009
1010        fop.handle(
1011            FopEvent::SendAd { fdu: &[3] },
1012            &mut actions,
1013        );
1014        assert!(actions
1015            .iter()
1016            .any(|a| matches!(a, FopAction::TransmitAd { seq: 0 })));
1017
1018        assert_eq!(fop.vs(), 1);
1019    }
1020}