1use std::io;
2use std::time::{Duration, Instant};
3use usb_gadget::function::custom::Event;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum LinkState {
8 Offline,
9 Ready,
10 Online,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum LinkCommand {
16 Fatal,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum LinkOfflineReason {
23 Ep0Disable,
24 Ep0Unbind,
25 IoError,
26 LivenessTimeout,
27}
28
29pub struct LinkController {
36 state: LinkState,
37 last_status: Option<Instant>,
38 last_offline_reason: Option<LinkOfflineReason>,
39 liveness_timeout: Duration,
40 reopen_backoff: Duration,
41 reopen_backoff_max: Duration,
42 reopen_not_before: Option<Instant>,
43 pending_drop: bool,
44}
45
46impl LinkController {
47 pub fn new(liveness_timeout: Duration) -> Self {
49 Self {
50 state: LinkState::Offline,
51 last_status: None,
52 last_offline_reason: None,
53 liveness_timeout,
54 reopen_backoff: Duration::from_secs(1),
55 reopen_backoff_max: Duration::from_secs(30),
56 reopen_not_before: None,
57 pending_drop: false,
58 }
59 }
60
61 pub fn state(&self) -> LinkState {
63 self.state
64 }
65
66 pub fn last_offline_reason(&self) -> Option<LinkOfflineReason> {
68 self.last_offline_reason
69 }
70
71 pub fn on_ep0_event(&mut self, event: Event) {
73 match event {
74 Event::Bind | Event::Enable | Event::Resume => {
75 self.reset_reopen_backoff();
76 self.enter_ready();
77 }
78 Event::Disable => {
79 self.enter_offline(LinkOfflineReason::Ep0Disable);
80 }
81 Event::Unbind => {
82 self.enter_offline(LinkOfflineReason::Ep0Unbind);
83 }
84 Event::Suspend => {
85 self.state = LinkState::Ready;
88 self.pending_drop = false;
89 }
90 Event::SetupDeviceToHost(_) | Event::SetupHostToDevice(_) => { }
91 Event::Unknown(_) => {}
92 _ => {}
93 }
94 }
95
96 pub fn on_status_ping(&mut self) {
98 let now = Instant::now();
99 self.last_status = Some(now);
100 if matches!(self.state, LinkState::Offline) {
101 if let Some(not_before) = self.reopen_not_before {
102 if now < not_before {
103 return;
104 }
105 }
106 self.enter_ready();
108 }
109 if matches!(self.state, LinkState::Ready | LinkState::Online) {
110 self.state = LinkState::Online;
111 self.reset_reopen_backoff();
112 }
113 }
114
115 pub fn on_io_error(&mut self, _err: &io::Error) {
117 if matches!(self.state, LinkState::Offline) {
118 return;
119 }
120 self.reopen_not_before = Some(Instant::now() + self.reopen_backoff);
121 self.reopen_backoff = self
122 .reopen_backoff
123 .saturating_mul(2)
124 .min(self.reopen_backoff_max);
125 self.enter_offline(LinkOfflineReason::IoError);
126 }
127
128 pub fn tick(&mut self, now: Instant) {
130 if let Some(last) = self.last_status {
131 if now.saturating_duration_since(last) > self.liveness_timeout {
132 self.enter_offline(LinkOfflineReason::LivenessTimeout);
133 }
134 }
135 }
136
137 pub fn take_command(&mut self) -> Option<LinkCommand> {
139 if self.pending_drop {
140 self.pending_drop = false;
141 return Some(LinkCommand::Fatal);
142 }
143 None
144 }
145
146 fn enter_ready(&mut self) {
147 self.state = LinkState::Ready;
148 self.reopen_not_before = None;
149 self.last_offline_reason = None;
150 self.pending_drop = false;
151 }
152
153 fn enter_offline(&mut self, reason: LinkOfflineReason) {
154 self.state = LinkState::Offline;
155 self.last_status = None;
156 self.last_offline_reason = Some(reason);
157 self.pending_drop = true;
158 }
159
160 fn reset_reopen_backoff(&mut self) {
161 self.reopen_backoff = Duration::from_secs(1);
162 self.reopen_not_before = None;
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn transitions_on_events_and_status() {
172 let mut ctrl = LinkController::new(Duration::from_secs(5));
173 assert_eq!(ctrl.state(), LinkState::Offline);
174
175 ctrl.on_ep0_event(Event::Enable);
176 assert_eq!(ctrl.state(), LinkState::Ready);
177 assert_eq!(ctrl.take_command(), None);
178
179 ctrl.on_status_ping();
180 assert_eq!(ctrl.state(), LinkState::Online);
181
182 let err = io::Error::from_raw_os_error(libc::EPIPE);
183 ctrl.on_io_error(&err);
184 assert_eq!(ctrl.state(), LinkState::Offline);
185 assert_eq!(ctrl.last_offline_reason(), Some(LinkOfflineReason::IoError));
186 assert_eq!(ctrl.take_command(), Some(LinkCommand::Fatal));
187 }
188
189 #[test]
190 fn liveness_timeout_forces_offline() {
191 let mut ctrl = LinkController::new(Duration::from_millis(100));
192 ctrl.on_ep0_event(Event::Enable);
193 ctrl.take_command();
194 ctrl.on_status_ping();
195 let now = Instant::now() + Duration::from_millis(250);
196 ctrl.tick(now);
197 assert_eq!(ctrl.state(), LinkState::Offline);
198 assert_eq!(
199 ctrl.last_offline_reason(),
200 Some(LinkOfflineReason::LivenessTimeout)
201 );
202 assert_eq!(ctrl.take_command(), Some(LinkCommand::Fatal));
203 }
204}