1use bincode::Decode;
2use bincode::Encode;
3use cu29::prelude::*;
4use serde::Deserialize;
5use serde::Serialize;
6
7#[derive(
12 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
13)]
14#[serde(rename_all = "snake_case")]
15pub enum CuSessionCommand {
16 #[default]
18 AcquireControl,
19 ReleaseControl,
21 ClearFaults,
23 EnableDrives,
25 DisableDrives,
27}
28
29#[derive(
31 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
32)]
33#[serde(rename_all = "snake_case")]
34pub enum CuSessionResult {
35 #[default]
37 Idle,
38 Pending,
40 Acknowledged,
42 Rejected,
44}
45
46#[derive(
48 Default, Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect,
49)]
50#[serde(rename_all = "snake_case")]
51pub enum CuSessionRejection {
52 #[default]
54 None,
55 NotInControl,
57 Controller,
59}
60
61#[derive(Default, Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, Reflect)]
63pub struct CuSessionDetail {
64 bytes: [u8; 32],
65 len: u8,
66}
67
68impl CuSessionDetail {
69 pub fn new(text: &str) -> Self {
71 let mut take = text.len().min(32);
72 while !text.is_char_boundary(take) {
73 take -= 1;
74 }
75 let mut bytes = [0; 32];
76 bytes[..take].copy_from_slice(&text.as_bytes()[..take]);
77 Self {
78 bytes,
79 len: take as u8,
80 }
81 }
82
83 pub fn as_str(&self) -> &str {
85 core::str::from_utf8(&self.bytes[..usize::from(self.len.min(32))]).unwrap_or("")
86 }
87}
88
89#[derive(Default, Debug, Clone, PartialEq, Encode, Decode, Serialize, Deserialize, Reflect)]
91pub struct CuSessionState {
92 pub command: Option<CuSessionCommand>,
94 pub sequence: u32,
96 pub result: CuSessionResult,
98 pub rejection: CuSessionRejection,
100 pub detail: CuSessionDetail,
102 pub control_acquired: bool,
104 pub drives_enabled: bool,
106 pub drives_ready: bool,
109 pub motion_allowed: bool,
111 pub refused: u32,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum CuSessionDecision {
118 Send,
120 Refused,
122}
123
124#[derive(Default, Debug, Clone)]
129pub struct CuSessionTracker {
130 state: CuSessionState,
131 outgoing: Option<CuSessionCommand>,
132 in_transit: bool,
134 transit_cancelled: bool,
136 held_before_acquire: bool,
138 acquired_here: bool,
139 changed: bool,
140}
141
142impl CuSessionTracker {
143 pub fn state(&self) -> &CuSessionState {
145 &self.state
146 }
147
148 pub fn take_changed(&mut self) -> bool {
150 core::mem::take(&mut self.changed)
151 }
152
153 pub fn is_busy(&self) -> bool {
155 self.outgoing.is_some() || self.in_transit || self.state.result == CuSessionResult::Pending
156 }
157
158 pub fn request(&mut self, command: CuSessionCommand) -> CuSessionDecision {
160 self.changed = true;
161 if self.is_busy() {
162 self.state.refused = self.state.refused.saturating_add(1);
163 return CuSessionDecision::Refused;
164 }
165 if command != CuSessionCommand::AcquireControl && !self.state.control_acquired {
166 self.state.command = Some(command);
167 self.state.result = CuSessionResult::Rejected;
168 self.state.rejection = CuSessionRejection::NotInControl;
169 self.state.detail = CuSessionDetail::default();
170 return CuSessionDecision::Refused;
171 }
172 self.outgoing = Some(command);
173 CuSessionDecision::Send
174 }
175
176 pub fn take_outgoing(&mut self) -> Option<CuSessionCommand> {
179 let command = self.outgoing.take();
180 self.in_transit |= command.is_some();
181 command
182 }
183
184 pub fn mark_sent(&mut self, command: CuSessionCommand, sequence: u32) {
186 let lost_in_transit =
188 core::mem::take(&mut self.transit_cancelled) && !self.state.control_acquired;
189 self.in_transit = false;
190 if command == CuSessionCommand::AcquireControl {
191 self.held_before_acquire = self.state.control_acquired;
192 }
193 self.state.command = Some(command);
194 self.state.sequence = sequence;
195 self.state.result = CuSessionResult::Pending;
196 self.state.rejection = CuSessionRejection::None;
197 self.state.detail = CuSessionDetail::default();
198 if lost_in_transit && command == CuSessionCommand::ReleaseControl {
199 self.state.result = CuSessionResult::Acknowledged;
201 } else if lost_in_transit && needs_control(command) {
202 self.state.result = CuSessionResult::Rejected;
204 self.state.rejection = CuSessionRejection::NotInControl;
205 }
206 self.changed = true;
207 }
208
209 pub fn on_acknowledged(&mut self, sequence: u32) {
211 if !self.pending(sequence) {
212 return;
213 }
214 self.state.result = CuSessionResult::Acknowledged;
215 match self.state.command {
216 Some(CuSessionCommand::AcquireControl) => {
217 self.state.control_acquired = true;
218 self.acquired_here |= !self.held_before_acquire;
219 }
220 Some(CuSessionCommand::ReleaseControl) => self.lose_control(),
221 Some(CuSessionCommand::EnableDrives) => self.state.drives_enabled = true,
222 Some(CuSessionCommand::DisableDrives) => self.disable(),
223 Some(CuSessionCommand::ClearFaults) | None => {}
224 }
225 self.changed = true;
226 }
227
228 pub fn on_rejected(&mut self, sequence: u32, detail: &str) {
230 if !self.pending(sequence) {
231 return;
232 }
233 self.state.result = CuSessionResult::Rejected;
234 self.state.rejection = CuSessionRejection::Controller;
235 self.state.detail = CuSessionDetail::new(detail);
236 self.changed = true;
237 }
238
239 pub fn on_control_report(&mut self, in_control: bool) {
243 if self.state.result == CuSessionResult::Pending {
244 match (self.state.command, in_control) {
245 (Some(CuSessionCommand::AcquireControl), true) => {
246 self.state.result = CuSessionResult::Acknowledged;
247 self.acquired_here |= !self.held_before_acquire;
248 }
249 (Some(CuSessionCommand::ReleaseControl), false) => {
250 self.state.result = CuSessionResult::Acknowledged;
251 }
252 _ => {}
253 }
254 }
255 if in_control {
256 self.state.control_acquired = true;
257 } else {
258 self.lose_control();
259 }
260 self.changed = true;
261 }
262
263 pub fn on_drive_report(&mut self, drives_ready: bool, motion_allowed: bool) {
265 if (drives_ready, motion_allowed) != (self.state.drives_ready, self.state.motion_allowed) {
266 self.state.drives_ready = drives_ready;
267 self.state.motion_allowed = motion_allowed;
268 self.changed = true;
269 }
270 }
271
272 pub fn on_disconnected(&mut self) {
274 self.outgoing = None;
275 self.in_transit = false;
276 self.transit_cancelled = false;
277 if self.state.result == CuSessionResult::Pending {
278 self.state.result = CuSessionResult::Idle;
279 }
280 self.lose_control();
281 self.changed = true;
282 }
283
284 pub fn shutdown_step(&self) -> Option<CuSessionCommand> {
289 if !self.state.control_acquired {
290 None
291 } else if self.state.drives_enabled {
292 Some(CuSessionCommand::DisableDrives)
293 } else if self.acquired_here {
294 Some(CuSessionCommand::ReleaseControl)
295 } else {
296 None
297 }
298 }
299
300 fn pending(&self, sequence: u32) -> bool {
301 self.state.result == CuSessionResult::Pending && self.state.sequence == sequence
302 }
303
304 fn disable(&mut self) {
305 self.state.drives_enabled = false;
306 self.state.drives_ready = false;
307 self.state.motion_allowed = false;
308 }
309
310 fn lose_control(&mut self) {
313 self.state.control_acquired = false;
314 self.acquired_here = false;
315 self.held_before_acquire = false;
316 self.disable();
317 if self
318 .outgoing
319 .is_some_and(|command| command != CuSessionCommand::AcquireControl)
320 {
321 self.outgoing = None;
322 }
323 self.transit_cancelled |= self.in_transit;
324 if self.state.result == CuSessionResult::Pending
325 && self.state.command.is_some_and(needs_control)
326 {
327 self.state.result = CuSessionResult::Rejected;
328 self.state.rejection = CuSessionRejection::NotInControl;
329 }
330 }
331}
332
333fn needs_control(command: CuSessionCommand) -> bool {
335 !matches!(
336 command,
337 CuSessionCommand::AcquireControl | CuSessionCommand::ReleaseControl
338 )
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 fn send(tracker: &mut CuSessionTracker, command: CuSessionCommand, sequence: u32) {
346 assert_eq!(tracker.request(command), CuSessionDecision::Send);
347 let out = tracker.take_outgoing().unwrap();
348 assert_eq!(out, command);
349 tracker.mark_sent(out, sequence);
350 }
351
352 #[test]
353 fn test_refuses_without_control_and_while_busy() {
354 let mut tracker = CuSessionTracker::default();
355 assert_eq!(
356 tracker.request(CuSessionCommand::EnableDrives),
357 CuSessionDecision::Refused
358 );
359 assert_eq!(tracker.state().rejection, CuSessionRejection::NotInControl);
360 assert!(tracker.take_outgoing().is_none());
361 send(&mut tracker, CuSessionCommand::AcquireControl, 1);
362 assert_eq!(
363 tracker.request(CuSessionCommand::ClearFaults),
364 CuSessionDecision::Refused
365 );
366 assert_eq!(tracker.state().refused, 1);
367 assert_eq!(tracker.state().result, CuSessionResult::Pending);
369 tracker.on_control_report(true);
370 assert_eq!(tracker.state().result, CuSessionResult::Acknowledged);
371 }
372
373 #[test]
374 fn test_answers_match_sequence() {
375 let mut tracker = CuSessionTracker::default();
376 tracker.on_control_report(true);
377 send(&mut tracker, CuSessionCommand::EnableDrives, 5);
378 tracker.on_acknowledged(4);
379 assert_eq!(tracker.state().result, CuSessionResult::Pending);
380 tracker.on_acknowledged(5);
381 assert!(tracker.state().drives_enabled);
382 send(&mut tracker, CuSessionCommand::DisableDrives, 6);
383 tracker.on_rejected(6, "drives busy and this text is longer than thirty-two");
384 assert_eq!(tracker.state().rejection, CuSessionRejection::Controller);
385 assert_eq!(
386 tracker.state().detail.as_str(),
387 "drives busy and this text is lon"
388 );
389 assert!(tracker.state().drives_enabled);
390 }
391
392 #[test]
393 fn test_shutdown_releases_only_what_it_took() {
394 let mut tracker = CuSessionTracker::default();
395 tracker.on_control_report(true);
396 assert_eq!(tracker.shutdown_step(), None);
398 send(&mut tracker, CuSessionCommand::EnableDrives, 1);
399 tracker.on_acknowledged(1);
400 tracker.on_drive_report(true, true);
401 assert_eq!(
402 tracker.shutdown_step(),
403 Some(CuSessionCommand::DisableDrives)
404 );
405 send(&mut tracker, CuSessionCommand::DisableDrives, 2);
406 tracker.on_acknowledged(2);
407 assert!(!tracker.state().drives_ready && !tracker.state().motion_allowed);
408 assert_eq!(tracker.shutdown_step(), None);
409
410 let mut tracker = CuSessionTracker::default();
411 tracker.on_control_report(false);
412 send(&mut tracker, CuSessionCommand::AcquireControl, 1);
413 tracker.on_control_report(true);
414 assert_eq!(
415 tracker.shutdown_step(),
416 Some(CuSessionCommand::ReleaseControl)
417 );
418 send(&mut tracker, CuSessionCommand::ReleaseControl, 2);
419 tracker.on_control_report(false);
420 assert_eq!(tracker.state().result, CuSessionResult::Acknowledged);
421 assert_eq!(tracker.shutdown_step(), None);
422 }
423
424 #[test]
425 fn test_losing_control_cancels_privileged_commands() {
426 let mut tracker = CuSessionTracker::default();
427 tracker.on_control_report(true);
428 assert_eq!(
429 tracker.request(CuSessionCommand::EnableDrives),
430 CuSessionDecision::Send
431 );
432 tracker.on_control_report(false);
433 assert_eq!(tracker.take_outgoing(), None);
434
435 tracker.on_control_report(true);
436 send(&mut tracker, CuSessionCommand::EnableDrives, 3);
437 tracker.on_control_report(false);
438 assert_eq!(tracker.state().result, CuSessionResult::Rejected);
439 assert_eq!(tracker.state().rejection, CuSessionRejection::NotInControl);
440 tracker.on_acknowledged(3); assert!(!tracker.state().drives_enabled);
442 }
443
444 #[test]
445 fn test_control_lost_in_transit() {
446 let mut tracker = CuSessionTracker::default();
447 tracker.on_control_report(true);
448 tracker.request(CuSessionCommand::EnableDrives);
449 let command = tracker.take_outgoing().unwrap();
450 tracker.on_control_report(false);
451 tracker.mark_sent(command, 9);
452 assert_eq!(tracker.state().rejection, CuSessionRejection::NotInControl);
453 tracker.on_acknowledged(9);
454 assert!(!tracker.state().drives_enabled);
455 assert!(!tracker.is_busy());
456 }
457
458 #[test]
459 fn test_release_lost_in_transit_completes() {
460 let mut tracker = CuSessionTracker::default();
461 tracker.on_control_report(true);
462 tracker.request(CuSessionCommand::ReleaseControl);
463 let command = tracker.take_outgoing().unwrap();
464 tracker.on_control_report(false);
465 tracker.mark_sent(command, 4);
466 assert_eq!(tracker.state().result, CuSessionResult::Acknowledged);
467 assert!(!tracker.is_busy());
468 }
469
470 #[test]
471 fn test_control_regained_in_transit_keeps_release_pending() {
472 let mut tracker = CuSessionTracker::default();
473 tracker.on_control_report(true);
474 tracker.request(CuSessionCommand::ReleaseControl);
475 let command = tracker.take_outgoing().unwrap();
476 tracker.on_control_report(false);
477 tracker.on_control_report(true);
478 tracker.mark_sent(command, 5);
479 assert_eq!(tracker.state().result, CuSessionResult::Pending);
480 tracker.on_control_report(false);
481 assert_eq!(tracker.state().result, CuSessionResult::Acknowledged);
482 }
483
484 #[test]
485 fn test_busy_between_take_and_mark_sent() {
486 let mut tracker = CuSessionTracker::default();
487 tracker.on_control_report(true);
488 tracker.request(CuSessionCommand::ClearFaults);
489 let command = tracker.take_outgoing().unwrap();
490 assert!(tracker.is_busy());
491 assert_eq!(
492 tracker.request(CuSessionCommand::EnableDrives),
493 CuSessionDecision::Refused
494 );
495 tracker.mark_sent(command, 1);
496 assert_eq!(tracker.state().command, Some(CuSessionCommand::ClearFaults));
497 }
498
499 #[test]
500 fn test_acquire_while_already_held_is_not_released() {
501 for via_report in [false, true] {
502 let mut tracker = CuSessionTracker::default();
503 tracker.on_control_report(true);
504 send(&mut tracker, CuSessionCommand::AcquireControl, 1);
505 if via_report {
506 tracker.on_control_report(true);
507 } else {
508 tracker.on_acknowledged(1);
509 }
510 assert_eq!(tracker.state().result, CuSessionResult::Acknowledged);
511 assert_eq!(tracker.shutdown_step(), None);
512 }
513 }
514
515 #[test]
516 fn test_reacquire_keeps_ownership() {
517 let mut tracker = CuSessionTracker::default();
518 tracker.on_control_report(false);
519 send(&mut tracker, CuSessionCommand::AcquireControl, 1);
520 tracker.on_control_report(true);
521 send(&mut tracker, CuSessionCommand::AcquireControl, 2);
522 tracker.on_acknowledged(2);
523 assert_eq!(
524 tracker.shutdown_step(),
525 Some(CuSessionCommand::ReleaseControl)
526 );
527 }
528
529 #[test]
530 fn test_disconnect_clears_session() {
531 let mut tracker = CuSessionTracker::default();
532 tracker.on_control_report(true);
533 send(&mut tracker, CuSessionCommand::EnableDrives, 1);
534 tracker.on_disconnected();
535 assert!(!tracker.is_busy());
536 let state = tracker.state();
537 assert_eq!(state.result, CuSessionResult::Idle);
538 assert!(!state.control_acquired && !state.drives_enabled);
539 assert_eq!(tracker.shutdown_step(), None);
540 }
541
542 #[test]
543 fn test_command_ron_spelling() {
544 let command: CuSessionCommand = ron::from_str("enable_drives").unwrap();
545 assert_eq!(command, CuSessionCommand::EnableDrives);
546 }
547}