1use std::sync::Arc;
2
3use thiserror::Error;
4
5use autd3_rs_core::error::{EncodeError, LinkError};
6use autd3_rs_core::protocol::describe_device_error;
7
8use crate::firmware_version::FirmwareVersion;
9use crate::mirror::{BankLoop, SilencerAxis};
10use crate::telemetry::Telemetry;
11use autd3_rs_core::value::{PulseWidthError, SamplingConfigError, TransitionMode};
12
13#[derive(Clone)]
14pub struct LinkCause(Arc<dyn core::error::Error + Send + Sync>);
15
16impl LinkCause {
17 #[must_use]
18 pub fn new<E: core::error::Error + Send + Sync + 'static>(source: E) -> Self {
19 Self(Arc::new(source))
20 }
21}
22
23impl core::ops::Deref for LinkCause {
24 type Target = dyn core::error::Error + Send + Sync + 'static;
25
26 fn deref(&self) -> &Self::Target {
27 &*self.0
28 }
29}
30
31impl core::fmt::Debug for LinkCause {
32 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33 core::fmt::Debug::fmt(&*self.0, f)
34 }
35}
36
37impl core::fmt::Display for LinkCause {
38 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 core::fmt::Display::fmt(&*self.0, f)
40 }
41}
42
43#[derive(Debug, Error)]
44#[non_exhaustive]
45pub enum Error {
46 #[error("device {device} firmware error {code:#04x}: {}", describe_device_error(*code))]
47 DeviceError { device: usize, code: u8 },
48
49 #[error(
50 "device {device}: strict silencer {axis:?} completion {completion_steps} steps exceeds sampling divider {sampling_div}"
51 )]
52 SilencerConstraint {
53 device: usize,
54 axis: SilencerAxis,
55 completion_steps: u16,
56 sampling_div: u16,
57 },
58
59 #[error(
60 "device {device}: transition mode {transition_mode:?} is invalid for a {bank_loop:?} loop bank"
61 )]
62 TransitionConstraint {
63 device: usize,
64 transition_mode: TransitionMode,
65 bank_loop: BankLoop,
66 },
67
68 #[error(
69 "device {device} runs firmware {version}, which is outside the series supported by this SDK ({}.{}.x)",
70 FirmwareVersion::SUPPORTED_SERIES.0,
71 FirmwareVersion::SUPPORTED_SERIES.1
72 )]
73 UnsupportedFirmware {
74 device: usize,
75 version: FirmwareVersion,
76 },
77
78 #[error(
79 "device {device} rejected telemetry counter {counter:?}; its firmware does not know this counter"
80 )]
81 UnsupportedTelemetry { device: usize, counter: Telemetry },
82
83 #[error("ack timeout after {cycles} cycles")]
84 Timeout { cycles: u32 },
85
86 #[error("link error: {0}")]
87 Link(#[source] LinkCause),
88
89 #[error(transparent)]
90 DcSysTime(#[from] autd3_rs_core::value::DcSysTimeError),
91
92 #[error("invalid payload: {0}")]
93 InvalidPayload(PayloadError),
94
95 #[error(transparent)]
96 Encode(#[from] EncodeError),
97
98 #[error("client RT worker is no longer alive")]
99 RtClosed,
100
101 #[error("RT thread panicked")]
102 RtPanicked,
103}
104
105impl From<LinkError> for Error {
106 fn from(e: LinkError) -> Self {
107 Error::Link(LinkCause::new(e))
108 }
109}
110
111impl<E> From<E> for Error
112where
113 E: Into<PayloadError>,
114{
115 fn from(value: E) -> Self {
116 Error::InvalidPayload(value.into())
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Error)]
121#[non_exhaustive]
122pub enum PayloadError {
123 #[error("max_inflight must be <= {max}")]
124 MaxInflightTooLarge { max: usize },
125
126 #[error("link must expose 1..={max} devices, got {got}")]
127 DeviceCountOutOfRange { got: usize, max: usize },
128
129 #[error("geometry has {geometry} device(s) but link exposes {link}")]
130 GeometryDeviceMismatch { geometry: usize, link: usize },
131
132 #[error("expected {expected} datagram(s) (one per device), got {got}")]
133 DatagramCountMismatch { expected: usize, got: usize },
134
135 #[error("modulation size {size} out of range {min}..={max}")]
136 ModulationSizeOutOfRange { size: usize, min: usize, max: usize },
137
138 #[error("modulation data must not be empty")]
139 ModulationDataEmpty,
140
141 #[error("modulation offset {offset} must be even (word-write-only RAM)")]
142 ModulationOffsetNotEven { offset: usize },
143
144 #[error("modulation write [{offset}, {end}) exceeds buffer capacity {capacity}")]
145 ModulationWriteExceedsCapacity {
146 offset: usize,
147 end: usize,
148 capacity: usize,
149 },
150
151 #[error("foci must not be empty")]
152 FociEmpty,
153
154 #[error("foci write [{offset}, {end}) exceeds capacity {capacity}")]
155 FociWriteExceedsCapacity {
156 offset: usize,
157 end: usize,
158 capacity: usize,
159 },
160
161 #[error("silencer completion time {0:?} must be a multiple of the ultrasound period")]
162 SilencerCompletionTimeNotMultiple(core::time::Duration),
163
164 #[error("silencer completion time {0:?} is out of range (1..=65535 ultrasound periods)")]
165 SilencerCompletionTimeOutOfRange(core::time::Duration),
166
167 #[error("pattern size {size} must be >= {min}")]
168 PatternSizeTooSmall { size: usize, min: usize },
169
170 #[error("{count} patterns do not fit the {format} compression, which carries {max} per frame")]
171 PatternCountExceedsFormat {
172 count: usize,
173 format: &'static str,
174 max: usize,
175 },
176
177 #[error("a {size}-sample bank never advances its index, so it requires an infinite loop")]
178 FiniteLoopNeedsMultipleSamples { size: usize },
179
180 #[error("num_foci {num_foci} out of range 1..={max}")]
181 NumFociOutOfRange { num_foci: u8, max: u8 },
182
183 #[error("STM size {size} x num_foci {num_foci} exceeds capacity {capacity}")]
184 StmFociExceedCapacity {
185 size: usize,
186 num_foci: u8,
187 capacity: usize,
188 },
189
190 #[error("sound_speed must be >= 1")]
191 SoundSpeedZero,
192
193 #[error("STM size {size} out of range {min}..={max}")]
194 StmSizeOutOfRange { size: usize, min: usize, max: usize },
195
196 #[error("emissions has {len} entr(ies) but device {device} was requested")]
197 EmissionsDeviceOutOfRange { device: usize, len: usize },
198
199 #[error("device {device} has {got} transducer entr(ies) but {expected} are required")]
200 TransducerCountMismatch {
201 device: usize,
202 got: usize,
203 expected: usize,
204 },
205
206 #[error("device {device} pattern data ({len} byte(s)) exceeds frame capacity {capacity}")]
207 PatternWriteExceedsCapacity {
208 device: usize,
209 len: usize,
210 capacity: usize,
211 },
212
213 #[error("pattern STM index {index} out of range 0..{max}")]
214 PatternIndexOutOfRange { index: usize, max: usize },
215
216 #[error(transparent)]
217 SamplingConfig(#[from] SamplingConfigError),
218
219 #[error(transparent)]
220 PulseWidth(#[from] PulseWidthError),
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn chain(e: &Error) -> Vec<String> {
228 let mut out = Vec::new();
229 let mut cur: Option<&(dyn core::error::Error + 'static)> = core::error::Error::source(e);
230 while let Some(e) = cur {
231 out.push(e.to_string());
232 cur = e.source();
233 }
234 out
235 }
236
237 #[test]
238 fn a_link_error_keeps_its_source_when_it_becomes_a_client_error() {
239 let io = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
240 let e = Error::from(LinkError::with_source("failed to open the link", io));
241
242 assert_eq!(e.to_string(), "link error: failed to open the link");
243 assert_eq!(
244 chain(&e),
245 vec![
246 "failed to open the link".to_owned(),
247 std::io::Error::from(std::io::ErrorKind::PermissionDenied).to_string(),
248 ]
249 );
250
251 let link_error = core::error::Error::source(&e)
252 .expect("the cause must be reachable through source()")
253 .downcast_ref::<LinkError>()
254 .expect("the LinkError itself must survive the conversion");
255 assert_eq!(
256 core::error::Error::source(link_error)
257 .expect("the source must survive")
258 .downcast_ref::<std::io::Error>()
259 .map(std::io::Error::kind),
260 Some(std::io::ErrorKind::PermissionDenied)
261 );
262 }
263
264 #[test]
265 fn a_link_error_without_a_source_ends_the_chain() {
266 let e = Error::from(LinkError::new("the bus is gone"));
267
268 assert_eq!(e.to_string(), "link error: the bus is gone");
269 assert_eq!(chain(&e), vec!["the bus is gone".to_owned()]);
270 }
271}