1use serde::{Deserialize, Serialize};
13
14use crate::freshness::{FreshnessGate, GateState, LockGate, LockState};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum OffloadMode {
20 VisionOnly,
23 SlamOnly,
26 Full,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct SessionStatus {
34 pub lock: LockState,
36 pub target: GateState,
38 pub pose: GateState,
40 pub commanding: bool,
43}
44
45#[derive(Debug, Clone)]
48pub struct OffloadSession {
49 mode: OffloadMode,
50 target_gate: FreshnessGate,
51 pose_gate: FreshnessGate,
52 lock: LockGate,
53}
54
55impl OffloadSession {
56 pub fn new(mode: OffloadMode, target_budget_ms: i64, pose_budget_ms: i64) -> Self {
58 Self {
59 mode,
60 target_gate: FreshnessGate::new(target_budget_ms),
61 pose_gate: FreshnessGate::new(pose_budget_ms),
62 lock: LockGate::new(),
63 }
64 }
65
66 pub fn mode(&self) -> OffloadMode {
68 self.mode
69 }
70
71 pub fn on_detection(&mut self, now_ms: i64) {
75 self.target_gate.record(now_ms);
76 }
77
78 pub fn on_pose(&mut self, now_ms: i64) {
81 self.pose_gate.record(now_ms);
82 }
83
84 pub fn set_link(&mut self, up: bool) {
86 self.target_gate.set_link(up);
87 self.pose_gate.set_link(up);
88 }
89
90 pub fn designate(&mut self) {
93 self.lock.lock();
94 }
95
96 pub fn drop_lock(&mut self) {
98 self.lock.unlock();
99 }
100
101 pub fn lock_state(&self) -> LockState {
103 self.lock.state()
104 }
105
106 fn streams_usable(&self, now_ms: i64) -> bool {
108 match self.mode {
109 OffloadMode::VisionOnly => self.target_gate.is_usable(now_ms),
110 OffloadMode::SlamOnly => self.pose_gate.is_usable(now_ms),
111 OffloadMode::Full => {
112 self.target_gate.is_usable(now_ms) && self.pose_gate.is_usable(now_ms)
113 }
114 }
115 }
116
117 pub fn tick(&mut self, now_ms: i64) -> SessionStatus {
122 let usable = self.streams_usable(now_ms);
123 self.lock.update(usable);
124 SessionStatus {
125 lock: self.lock.state(),
126 target: self.target_gate.state(now_ms),
127 pose: self.pose_gate.state(now_ms),
128 commanding: self.lock.is_locked() && usable,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn a_full_session_commands_when_locked_and_both_streams_fresh() {
141 let mut s = OffloadSession::new(OffloadMode::Full, 500, 500);
142 s.designate();
143 s.on_detection(1000);
144 s.on_pose(1000);
145 let status = s.tick(1100);
146 assert_eq!(status.lock, LockState::Locked);
147 assert!(status.commanding);
148 }
149
150 #[test]
151 fn a_full_session_stops_when_one_required_stream_goes_stale() {
152 let mut s = OffloadSession::new(OffloadMode::Full, 500, 500);
153 s.designate();
154 s.on_detection(1000);
155 s.on_pose(1000);
156 assert!(s.tick(1100).commanding);
157 s.on_detection(1500); let status = s.tick(1700); assert_eq!(status.pose, GateState::Stale);
161 assert_eq!(status.lock, LockState::Lost);
162 assert!(!status.commanding);
163 }
164
165 #[test]
166 fn a_link_drop_stops_the_session() {
167 let mut s = OffloadSession::new(OffloadMode::VisionOnly, 500, 500);
168 s.designate();
169 s.on_detection(1000);
170 assert!(s.tick(1100).commanding);
171 s.set_link(false);
172 let status = s.tick(1150);
173 assert_eq!(status.lock, LockState::Lost);
174 assert!(!status.commanding);
175 }
176
177 #[test]
178 fn a_session_never_auto_re_acquires_after_a_stale_drop() {
179 let mut s = OffloadSession::new(OffloadMode::VisionOnly, 500, 500);
180 s.designate();
181 s.on_detection(1000);
182 assert!(s.tick(1100).commanding);
183 assert_eq!(s.tick(2000).lock, LockState::Lost);
185 s.on_detection(2100);
187 s.on_detection(2200);
188 let status = s.tick(2250);
189 assert_eq!(status.target, GateState::Fresh);
190 assert_eq!(status.lock, LockState::Lost); assert!(!status.commanding);
192 s.designate();
194 s.on_detection(2300);
195 assert!(s.tick(2350).commanding);
196 }
197
198 #[test]
199 fn slam_only_gates_on_the_pose_stream() {
200 let mut s = OffloadSession::new(OffloadMode::SlamOnly, 500, 500);
201 s.designate();
202 s.on_detection(1000);
204 assert!(!s.tick(1100).commanding);
205 s.on_pose(1100);
206 assert!(s.tick(1200).commanding);
207 }
208
209 #[test]
210 fn a_session_never_auto_re_acquires_after_a_link_drop() {
211 let mut s = OffloadSession::new(OffloadMode::VisionOnly, 500, 500);
214 s.designate();
215 s.on_detection(1000);
216 assert!(s.tick(1100).commanding);
217 s.set_link(false);
218 assert_eq!(s.tick(1150).lock, LockState::Lost);
219 s.set_link(true);
221 s.on_detection(1200);
222 let status = s.tick(1250);
223 assert_eq!(status.target, GateState::Fresh);
224 assert_eq!(status.lock, LockState::Lost);
225 assert!(!status.commanding);
226 }
227
228 #[test]
229 fn a_full_session_holds_while_only_one_stream_is_fresh() {
230 let mut s = OffloadSession::new(OffloadMode::Full, 500, 500);
234 s.designate();
235 s.on_detection(1000); let status = s.tick(1100);
237 assert_eq!(status.lock, LockState::Locked); assert!(!status.commanding);
239 }
240
241 #[test]
242 fn an_un_designated_session_never_commands() {
243 let mut s = OffloadSession::new(OffloadMode::Full, 500, 500);
244 s.on_detection(1000);
245 s.on_pose(1000);
246 let status = s.tick(1100);
248 assert_eq!(status.lock, LockState::Unlocked);
249 assert!(!status.commanding);
250 }
251}