1use core::{convert::Infallible, fmt::Debug, num::NonZeroU16, time::Duration};
2
3use crate::{
4 common::{Freq, Hz, ULTRASOUND_FREQ},
5 utils::float::is_integer,
6};
7
8#[derive(Debug, PartialEq, Copy, Clone)]
9pub enum SamplingConfigError {
11 DivideInvalid,
13 FreqInvalid(Freq<u32>),
15 FreqInvalidF(Freq<f32>),
17 PeriodInvalid(Duration),
19 FreqOutOfRange(Freq<u32>, Freq<u32>, Freq<u32>),
21 FreqOutOfRangeF(Freq<f32>, Freq<f32>, Freq<f32>),
23 PeriodOutOfRange(Duration, Duration, Duration),
25}
26
27impl core::fmt::Display for SamplingConfigError {
29 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
30 match self {
31 SamplingConfigError::DivideInvalid => write!(f, "Sampling divide must not be zero"),
32 SamplingConfigError::FreqInvalid(freq) => {
33 write!(
34 f,
35 "Sampling frequency ({:?}) must divide the ultrasound frequency",
36 freq
37 )
38 }
39 SamplingConfigError::FreqInvalidF(freq) => {
40 write!(
41 f,
42 "Sampling frequency ({:?}) must divide the ultrasound frequency",
43 freq
44 )
45 }
46 SamplingConfigError::PeriodInvalid(period) => {
47 write!(
48 f,
49 "Sampling period ({:?}) must be a multiple of the ultrasound period",
50 period
51 )
52 }
53 SamplingConfigError::FreqOutOfRange(freq, min, max) => {
54 write!(
55 f,
56 "Sampling frequency ({:?}) is out of range ([{:?}, {:?}])",
57 freq, min, max
58 )
59 }
60 SamplingConfigError::FreqOutOfRangeF(freq, min, max) => {
61 write!(
62 f,
63 "Sampling frequency ({:?}) is out of range ([{:?}, {:?}])",
64 freq, min, max
65 )
66 }
67 SamplingConfigError::PeriodOutOfRange(period, min, max) => {
68 write!(
69 f,
70 "Sampling period ({:?}) is out of range ([{:?}, {:?}])",
71 period, min, max
72 )
73 }
74 }
75 }
76}
77
78impl core::error::Error for SamplingConfigError {}
79
80impl From<Infallible> for SamplingConfigError {
81 fn from(_: Infallible) -> Self {
82 unreachable!()
83 }
84}
85#[derive(Copy, Clone, Debug, PartialEq)]
89pub struct Nearest<T: Copy + Clone + Debug + PartialEq>(pub T);
90
91#[derive(Clone, Copy)]
93pub enum SamplingConfig {
94 #[doc(hidden)]
95 Divide(NonZeroU16),
96 #[doc(hidden)]
97 Freq(Freq<f32>),
98 #[doc(hidden)]
99 Period(core::time::Duration),
100 #[doc(hidden)]
101 FreqNearest(Nearest<Freq<f32>>),
102 #[doc(hidden)]
103 PeriodNearest(Nearest<core::time::Duration>),
104}
105
106impl PartialEq for SamplingConfig {
107 fn eq(&self, other: &Self) -> bool {
108 match (self.divide(), other.divide()) {
109 (Ok(lhs), Ok(rhs)) => lhs == rhs,
110 _ => false,
111 }
112 }
113}
114
115impl core::fmt::Debug for SamplingConfig {
116 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
117 match self {
118 SamplingConfig::Divide(div) => write!(f, "SamplingConfig::Divide({div})"),
119 SamplingConfig::Freq(freq) => write!(f, "SamplingConfig::Freq({freq:?})"),
120 SamplingConfig::Period(period) => write!(f, "SamplingConfig::Period({period:?})"),
121 SamplingConfig::FreqNearest(nearest) => {
122 write!(f, "SamplingConfig::FreqNearest({nearest:?})")
123 }
124 SamplingConfig::PeriodNearest(nearest) => {
125 write!(f, "SamplingConfig::PeriodNearest({nearest:?})")
126 }
127 }
128 }
129}
130
131impl From<NonZeroU16> for SamplingConfig {
132 fn from(value: NonZeroU16) -> Self {
133 Self::Divide(value)
134 }
135}
136
137impl From<Freq<f32>> for SamplingConfig {
138 fn from(value: Freq<f32>) -> Self {
139 Self::Freq(value)
140 }
141}
142
143impl From<core::time::Duration> for SamplingConfig {
144 fn from(value: core::time::Duration) -> Self {
145 Self::Period(value)
146 }
147}
148
149impl SamplingConfig {
150 pub const FREQ_40K: Self = SamplingConfig::Freq(Freq { freq: 40000. });
152 pub const FREQ_4K: Self = SamplingConfig::Freq(Freq { freq: 4000. });
154
155 #[must_use]
157 pub fn new(value: impl Into<SamplingConfig>) -> Self {
158 value.into()
159 }
160
161 pub fn divide(&self) -> Result<u16, SamplingConfigError> {
165 match *self {
166 SamplingConfig::Divide(div) => Ok(div.get()),
167 SamplingConfig::Freq(freq) => {
168 let freq_max = ULTRASOUND_FREQ.hz() as f32 * Hz;
169 let freq_min = freq_max / u16::MAX as f32;
170 if !(freq_min..=freq_max).contains(&freq) {
171 return Err(SamplingConfigError::FreqOutOfRangeF(
172 freq, freq_min, freq_max,
173 ));
174 }
175 let divide = ULTRASOUND_FREQ.hz() as f32 / freq.hz();
176 if !is_integer(divide as _) {
177 return Err(SamplingConfigError::FreqInvalidF(freq));
178 }
179 Ok(divide as _)
180 }
181 SamplingConfig::Period(duration) => {
182 use crate::common::ULTRASOUND_PERIOD;
183
184 let period_min = ULTRASOUND_PERIOD;
185 let period_max = core::time::Duration::from_micros(
186 u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64,
187 );
188 if !(period_min..=period_max).contains(&duration) {
189 return Err(SamplingConfigError::PeriodOutOfRange(
190 duration, period_min, period_max,
191 ));
192 }
193 if duration.as_nanos() % ULTRASOUND_PERIOD.as_nanos() != 0 {
194 return Err(SamplingConfigError::PeriodInvalid(duration));
195 }
196 Ok((duration.as_nanos() / ULTRASOUND_PERIOD.as_nanos()) as _)
197 }
198 SamplingConfig::FreqNearest(nearest) => Ok(((ULTRASOUND_FREQ.hz() as f32
199 / nearest.0.hz())
200 .clamp(1.0, u16::MAX as f32))
201 .round() as u16),
202 SamplingConfig::PeriodNearest(nearest) => {
203 use crate::common::ULTRASOUND_PERIOD;
204
205 Ok(((nearest.0.as_nanos() + ULTRASOUND_PERIOD.as_nanos() / 2)
206 / ULTRASOUND_PERIOD.as_nanos())
207 .clamp(1, u16::MAX as u128) as u16)
208 }
209 }
210 }
211
212 pub fn freq(&self) -> Result<Freq<f32>, SamplingConfigError> {
214 Ok(ULTRASOUND_FREQ.hz() as f32 / self.divide()? as f32 * Hz)
215 }
216
217 pub fn period(&self) -> Result<core::time::Duration, SamplingConfigError> {
219 Ok(crate::common::ULTRASOUND_PERIOD * self.divide()? as u32)
220 }
221}
222
223impl SamplingConfig {
224 #[must_use]
226 pub const fn into_nearest(self) -> SamplingConfig {
227 match self {
228 SamplingConfig::Freq(freq) => SamplingConfig::FreqNearest(Nearest(freq)),
229 SamplingConfig::Period(period) => SamplingConfig::PeriodNearest(Nearest(period)),
230 _ => self,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use crate::common::{Hz, kHz};
238
239 use crate::common::ULTRASOUND_PERIOD;
240 use core::time::Duration;
241
242 use super::*;
243
244 #[rstest::rstest]
245 #[case(Ok(1), NonZeroU16::MIN)]
246 #[case(Ok(u16::MAX), NonZeroU16::MAX)]
247 #[case(Ok(1), 40000. * Hz)]
248 #[case(Ok(10), 4000. * Hz)]
249 #[case(Err(SamplingConfigError::FreqInvalidF((ULTRASOUND_FREQ.hz() as f32 - 1.) * Hz)), (ULTRASOUND_FREQ.hz() as f32 - 1.) * Hz)]
250 #[case(Err(SamplingConfigError::FreqOutOfRangeF(0. * Hz, ULTRASOUND_FREQ.hz() as f32 * Hz / u16::MAX as f32, ULTRASOUND_FREQ.hz() as f32 * Hz)), 0. * Hz)]
251 #[case(Err(SamplingConfigError::FreqOutOfRangeF(40000. * Hz + 1. * Hz, ULTRASOUND_FREQ.hz() as f32 * Hz / u16::MAX as f32, ULTRASOUND_FREQ.hz() as f32 * Hz)), 40000. * Hz + 1. * Hz)]
252 #[case(Ok(1), Duration::from_micros(25))]
253 #[case(Ok(10), Duration::from_micros(250))]
254 #[case(Err(SamplingConfigError::PeriodInvalid(Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) - Duration::from_nanos(1))), Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) - Duration::from_nanos(1))]
255 #[case(Err(SamplingConfigError::PeriodOutOfRange(ULTRASOUND_PERIOD / 2, ULTRASOUND_PERIOD, Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64))), ULTRASOUND_PERIOD / 2)]
256 #[case(Err(SamplingConfigError::PeriodOutOfRange(Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) * 2, ULTRASOUND_PERIOD, Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64))), Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) * 2)]
257 fn divide(
258 #[case] expect: Result<u16, SamplingConfigError>,
259 #[case] value: impl Into<SamplingConfig>,
260 ) {
261 assert_eq!(expect, SamplingConfig::new(value).divide());
262 }
263
264 #[rstest::rstest]
265 #[case(Ok(40000. * Hz), NonZeroU16::MIN)]
266 #[case(Ok(0.61036086 * Hz), NonZeroU16::MAX)]
267 #[case(Ok(40000. * Hz), 40000. * Hz)]
268 #[case(Ok(4000. * Hz), 4000. * Hz)]
269 #[case(Ok(40000. * Hz), Duration::from_micros(25))]
270 #[case(Ok(4000. * Hz), Duration::from_micros(250))]
271 fn freq(
272 #[case] expect: Result<Freq<f32>, SamplingConfigError>,
273 #[case] value: impl Into<SamplingConfig>,
274 ) {
275 assert_eq!(expect, SamplingConfig::new(value).freq());
276 }
277
278 #[rstest::rstest]
279 #[case(Ok(Duration::from_micros(25)), NonZeroU16::MIN)]
280 #[case(Ok(Duration::from_micros(1638375)), NonZeroU16::MAX)]
281 #[case(Ok(Duration::from_micros(25)), 40000. * Hz)]
282 #[case(Ok(Duration::from_micros(250)), 4000. * Hz)]
283 #[case(Ok(Duration::from_micros(25)), Duration::from_micros(25))]
284 #[case(Ok(Duration::from_micros(250)), Duration::from_micros(250))]
285 fn period(
286 #[case] expect: Result<Duration, SamplingConfigError>,
287 #[case] value: impl Into<SamplingConfig>,
288 ) {
289 assert_eq!(expect, SamplingConfig::new(value).period());
290 }
291
292 #[rstest::rstest]
293 #[case::min(u16::MAX, (40000. / u16::MAX as f32) * Hz)]
294 #[case::max(1, 40000. * Hz)]
295 #[case::not_supported_max(1, (ULTRASOUND_FREQ.hz() as f32 - 1.) * Hz)]
296 #[case::out_of_range_min(u16::MAX, 0. * Hz)]
297 #[case::out_of_range_max(1, 40000. * Hz + 1. * Hz)]
298 fn from_freq_nearest(#[case] expected: u16, #[case] freq: Freq<f32>) {
299 assert_eq!(
300 Ok(expected),
301 SamplingConfig::new(freq).into_nearest().divide()
302 );
303 }
304
305 #[rstest::rstest]
306 #[case::min(1, ULTRASOUND_PERIOD)]
307 #[case::max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64))]
308 #[case::not_supported_max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) - Duration::from_nanos(1))]
309 #[case::out_of_range_min(1, ULTRASOUND_PERIOD / 2)]
310 #[case::out_of_range_max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ULTRASOUND_PERIOD.as_micros() as u64) * 2)]
311 fn from_period_nearest(#[case] expected: u16, #[case] p: Duration) {
312 assert_eq!(Ok(expected), SamplingConfig::new(p).into_nearest().divide());
313 }
314
315 #[rstest::rstest]
316 #[case(
317 SamplingConfig::Divide(NonZeroU16::MIN),
318 SamplingConfig::Divide(NonZeroU16::MIN)
319 )]
320 #[case(SamplingConfig::FreqNearest(Nearest(1. * Hz)), SamplingConfig::Freq(1. * Hz))]
321 #[case(
322 SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1))),
323 SamplingConfig::Period(Duration::from_micros(1))
324 )]
325 #[case(SamplingConfig::FreqNearest(Nearest(1. * Hz)), SamplingConfig::FreqNearest(Nearest(1. * Hz)))]
326 #[case(
327 SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1))),
328 SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1)))
329 )]
330 #[test]
331 fn into_nearest(#[case] expect: SamplingConfig, #[case] config: SamplingConfig) {
332 assert_eq!(expect, config.into_nearest());
333 }
334
335 #[rstest::rstest]
336 #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::FREQ_40K)]
337 #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::new(NonZeroU16::MIN))]
338 #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::new(40. * kHz))]
339 #[case(
340 true,
341 SamplingConfig::FREQ_40K,
342 SamplingConfig::new(core::time::Duration::from_micros(25))
343 )]
344 #[case(false, SamplingConfig::new(41. * kHz), SamplingConfig::new(41. * kHz))]
345 #[test]
346 fn partial_eq(#[case] expect: bool, #[case] lhs: SamplingConfig, #[case] rhs: SamplingConfig) {
347 assert_eq!(expect, lhs == rhs);
348 }
349
350 #[rstest::rstest]
351 #[case("SamplingConfig::Divide(1)", SamplingConfig::Divide(NonZeroU16::MIN))]
352 #[case("SamplingConfig::Freq(1 Hz)", SamplingConfig::Freq(1. * Hz))]
353 #[case(
354 "SamplingConfig::Period(1µs)",
355 SamplingConfig::Period(Duration::from_micros(1))
356 )]
357 #[case("SamplingConfig::FreqNearest(Nearest(1 Hz))", SamplingConfig::FreqNearest(Nearest(1. * Hz)))]
358 #[case(
359 "SamplingConfig::PeriodNearest(Nearest(1µs))",
360 SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1)))
361 )]
362 #[test]
363 fn debug(#[case] expect: &str, #[case] config: SamplingConfig) {
364 assert_eq!(expect, format!("{config:?}"));
365 }
366}