Skip to main content

leodos_protocols/transport/srspp/
rto.rs

1/// Policy for computing retransmission timeout values.
2pub trait RtoPolicy {
3    /// Compute the retransmission timeout in ticks for the current time.
4    fn rto_ticks(&self, now_secs: u32) -> u32;
5}
6
7/// Fixed retransmission timeout that ignores orbital dynamics.
8pub struct FixedRto {
9    /// Constant timeout value in ticks.
10    rto_ticks: u32,
11}
12
13impl FixedRto {
14    /// Create a new fixed RTO policy with the given timeout in ticks.
15    pub fn new(rto_ticks: u32) -> Self {
16        Self { rto_ticks }
17    }
18}
19
20impl RtoPolicy for FixedRto {
21    fn rto_ticks(&self, _now_secs: u32) -> u32 {
22        self.rto_ticks
23    }
24}
25
26/// A ground station contact window defined by start and end times.
27#[derive(Debug, Clone)]
28pub struct ContactWindow {
29    /// Ground station identifier.
30    pub station_id: u8,
31    /// Window start time in mission-elapsed seconds.
32    pub start_secs: u32,
33    /// Window end time in mission-elapsed seconds (exclusive).
34    pub end_secs: u32,
35}
36
37/// Ordered schedule of ground station contact windows.
38pub struct ContactSchedule<const N: usize> {
39    /// Chronologically ordered list of contact windows.
40    windows: heapless::Vec<ContactWindow, N>,
41}
42
43impl<const N: usize> ContactSchedule<N> {
44    /// Create an empty contact schedule.
45    pub fn new() -> Self {
46        Self {
47            windows: heapless::Vec::new(),
48        }
49    }
50
51    /// Insert a contact window in chronological order.
52    pub fn add_window(&mut self, window: ContactWindow) -> Result<(), ContactWindow> {
53        let pos = self
54            .windows
55            .iter()
56            .position(|w| w.start_secs > window.start_secs)
57            .unwrap_or(self.windows.len());
58
59        self.windows.insert(pos, window).map_err(|e| e)
60    }
61
62    /// Check if the given time falls within any contact window.
63    pub fn in_window(&self, now_secs: u32) -> bool {
64        self.windows
65            .iter()
66            .any(|w| now_secs >= w.start_secs && now_secs < w.end_secs)
67    }
68
69    /// Return the next contact window starting after the given time.
70    pub fn next_window(&self, now_secs: u32) -> Option<&ContactWindow> {
71        self.windows.iter().find(|w| w.start_secs > now_secs)
72    }
73}
74
75/// RTO policy that adapts timeout based on orbital contact windows.
76pub struct OrbitAwareRto<const N: usize> {
77    /// RTO used during active ISL contact windows.
78    isl_rto_ticks: u32,
79    /// Extra margin added to the wait-for-window timeout.
80    margin_ticks: u32,
81    /// Ground station contact schedule.
82    schedule: ContactSchedule<N>,
83}
84
85impl<const N: usize> OrbitAwareRto<N> {
86    /// Create an orbit-aware RTO with ISL timeout, margin, and contact schedule.
87    pub fn new(isl_rto_ticks: u32, margin_ticks: u32, schedule: ContactSchedule<N>) -> Self {
88        Self {
89            isl_rto_ticks,
90            margin_ticks,
91            schedule,
92        }
93    }
94}
95
96impl<const N: usize> RtoPolicy for OrbitAwareRto<N> {
97    fn rto_ticks(&self, now_secs: u32) -> u32 {
98        if self.schedule.in_window(now_secs) {
99            return self.isl_rto_ticks;
100        }
101
102        match self.schedule.next_window(now_secs) {
103            Some(window) => {
104                let secs_until = window.start_secs - now_secs;
105                secs_until * 1000 + self.margin_ticks
106            }
107            None => self.isl_rto_ticks,
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_fixed_rto() {
118        let policy = FixedRto::new(500);
119        assert_eq!(policy.rto_ticks(0), 500);
120        assert_eq!(policy.rto_ticks(1000), 500);
121        assert_eq!(policy.rto_ticks(u32::MAX), 500);
122    }
123
124    #[test]
125    fn test_orbit_aware_in_window() {
126        let mut schedule = ContactSchedule::<4>::new();
127        schedule
128            .add_window(ContactWindow {
129                station_id: 1,
130                start_secs: 100,
131                end_secs: 200,
132            })
133            .unwrap();
134
135        let policy = OrbitAwareRto::new(50, 500, schedule);
136
137        assert_eq!(policy.rto_ticks(100), 50);
138        assert_eq!(policy.rto_ticks(150), 50);
139        assert_eq!(policy.rto_ticks(199), 50);
140    }
141
142    #[test]
143    fn test_orbit_aware_between_windows() {
144        let mut schedule = ContactSchedule::<4>::new();
145        schedule
146            .add_window(ContactWindow {
147                station_id: 1,
148                start_secs: 100,
149                end_secs: 200,
150            })
151            .unwrap();
152        schedule
153            .add_window(ContactWindow {
154                station_id: 2,
155                start_secs: 500,
156                end_secs: 600,
157            })
158            .unwrap();
159
160        let policy = OrbitAwareRto::new(50, 500, schedule);
161
162        assert_eq!(policy.rto_ticks(0), 100 * 1000 + 500);
163        assert_eq!(policy.rto_ticks(250), 250 * 1000 + 500);
164    }
165
166    #[test]
167    fn test_orbit_aware_no_future_windows() {
168        let mut schedule = ContactSchedule::<4>::new();
169        schedule
170            .add_window(ContactWindow {
171                station_id: 1,
172                start_secs: 100,
173                end_secs: 200,
174            })
175            .unwrap();
176
177        let policy = OrbitAwareRto::new(50, 500, schedule);
178
179        assert_eq!(policy.rto_ticks(300), 50);
180    }
181
182    #[test]
183    fn test_contact_schedule_queries() {
184        let mut schedule = ContactSchedule::<4>::new();
185        schedule
186            .add_window(ContactWindow {
187                station_id: 1,
188                start_secs: 100,
189                end_secs: 200,
190            })
191            .unwrap();
192        schedule
193            .add_window(ContactWindow {
194                station_id: 2,
195                start_secs: 300,
196                end_secs: 400,
197            })
198            .unwrap();
199
200        assert!(!schedule.in_window(50));
201        assert!(schedule.in_window(100));
202        assert!(schedule.in_window(150));
203        assert!(!schedule.in_window(200));
204        assert!(!schedule.in_window(250));
205        assert!(schedule.in_window(300));
206        assert!(schedule.in_window(350));
207        assert!(!schedule.in_window(400));
208
209        assert_eq!(schedule.next_window(0).unwrap().station_id, 1);
210        assert_eq!(schedule.next_window(150).unwrap().station_id, 2);
211        assert!(schedule.next_window(350).is_none());
212    }
213}