1use std::{fmt, num::NonZeroU8};
15
16use az::SaturatingAs;
17use num_enum::{IntoPrimitive, TryFromPrimitive};
18use nutype::nutype;
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21#[derive(
31 Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
32)]
33#[repr(u8)]
34pub enum SmartShiftMode {
35 Free = 1,
37 Ratchet = 2,
39}
40
41impl SmartShiftMode {
42 #[must_use]
45 pub fn flipped(self) -> Self {
46 match self {
47 Self::Free => Self::Ratchet,
48 Self::Ratchet => Self::Free,
49 }
50 }
51}
52
53impl From<crate::config::WheelMode> for SmartShiftMode {
58 fn from(mode: crate::config::WheelMode) -> Self {
59 match mode {
60 crate::config::WheelMode::Free => Self::Free,
61 crate::config::WheelMode::Ratchet => Self::Ratchet,
62 }
63 }
64}
65
66impl From<SmartShiftMode> for crate::config::WheelMode {
67 fn from(mode: SmartShiftMode) -> Self {
68 match mode {
69 SmartShiftMode::Free => Self::Free,
70 SmartShiftMode::Ratchet => Self::Ratchet,
71 }
72 }
73}
74
75#[nutype(
80 const_fn,
81 validate(greater_or_equal = 1, less_or_equal = 254),
82 derive(
83 Debug,
84 Clone,
85 Copy,
86 PartialEq,
87 Eq,
88 PartialOrd,
89 Ord,
90 TryFrom,
91 Into,
92 Display,
93 Serialize,
94 Deserialize
95 )
96)]
97pub struct SmartShiftThreshold(u8);
98
99impl SmartShiftThreshold {
100 #[must_use]
102 pub fn from_rounded(value: f32) -> Self {
103 let value = if value.is_nan() { 1.0 } else { value };
104 let raw = value.clamp(1.0, 254.0).round().saturating_as::<u8>();
105 let Ok(value) = Self::try_new(raw) else {
106 unreachable!("clamped SmartShift threshold is always valid");
107 };
108 value
109 }
110}
111
112impl From<SmartShiftThreshold> for f32 {
113 fn from(threshold: SmartShiftThreshold) -> Self {
114 Self::from(threshold.into_inner())
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum SmartShiftAutoDisengage {
124 Threshold(SmartShiftThreshold),
126 Permanent,
128}
129
130impl SmartShiftAutoDisengage {
131 #[must_use]
133 pub const fn is_permanent(self) -> bool {
134 matches!(self, Self::Permanent)
135 }
136
137 #[must_use]
139 pub const fn threshold(self) -> Option<SmartShiftThreshold> {
140 match self {
141 Self::Threshold(threshold) => Some(threshold),
142 Self::Permanent => None,
143 }
144 }
145}
146
147impl TryFrom<u8> for SmartShiftAutoDisengage {
148 type Error = SmartShiftThresholdError;
149
150 fn try_from(value: u8) -> Result<Self, Self::Error> {
151 if value == u8::MAX {
152 Ok(Self::Permanent)
153 } else {
154 SmartShiftThreshold::try_from(value).map(Self::Threshold)
155 }
156 }
157}
158
159impl From<SmartShiftAutoDisengage> for u8 {
160 fn from(auto_disengage: SmartShiftAutoDisengage) -> Self {
161 match auto_disengage {
162 SmartShiftAutoDisengage::Threshold(threshold) => threshold.into_inner(),
163 SmartShiftAutoDisengage::Permanent => Self::MAX,
164 }
165 }
166}
167
168impl From<NonZeroU8> for SmartShiftAutoDisengage {
169 fn from(value: NonZeroU8) -> Self {
170 if value == NonZeroU8::MAX {
171 Self::Permanent
172 } else {
173 let Ok(threshold) = SmartShiftThreshold::try_new(value.get()) else {
174 unreachable!("non-zero SmartShift values below 255 are thresholds");
175 };
176 Self::Threshold(threshold)
177 }
178 }
179}
180
181impl From<SmartShiftAutoDisengage> for NonZeroU8 {
182 fn from(auto_disengage: SmartShiftAutoDisengage) -> Self {
183 match auto_disengage {
184 SmartShiftAutoDisengage::Threshold(threshold) => {
185 let Some(value) = Self::new(threshold.into_inner()) else {
186 unreachable!("SmartShift thresholds are non-zero");
187 };
188 value
189 }
190 SmartShiftAutoDisengage::Permanent => Self::MAX,
191 }
192 }
193}
194
195impl fmt::Display for SmartShiftAutoDisengage {
196 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197 u8::from(*self).fmt(formatter)
198 }
199}
200
201impl Serialize for SmartShiftAutoDisengage {
202 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
203 where
204 S: Serializer,
205 {
206 serializer.serialize_u8((*self).into())
207 }
208}
209
210impl<'de> Deserialize<'de> for SmartShiftAutoDisengage {
211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
212 where
213 D: Deserializer<'de>,
214 {
215 Self::try_from(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
216 }
217}
218
219#[nutype(
226 const_fn,
227 validate(greater_or_equal = 1, less_or_equal = 100),
228 derive(
229 Debug,
230 Clone,
231 Copy,
232 PartialEq,
233 Eq,
234 PartialOrd,
235 Ord,
236 TryFrom,
237 Into,
238 Display,
239 Serialize,
240 Deserialize
241 )
242)]
243pub struct TunableTorque(u8);
244
245impl From<TunableTorque> for NonZeroU8 {
246 fn from(torque: TunableTorque) -> Self {
247 let Some(value) = Self::new(torque.into_inner()) else {
248 unreachable!("tunable torque is non-zero");
249 };
250 value
251 }
252}
253
254pub(crate) mod optional_tunable_torque {
255 use super::TunableTorque;
256 use serde::{Deserialize, Deserializer, Serializer};
257
258 #[expect(
259 clippy::ref_option,
260 clippy::trivially_copy_pass_by_ref,
261 reason = "serde field serializers must receive the field by reference"
262 )]
263 pub(crate) fn serialize<S>(
264 torque: &Option<TunableTorque>,
265 serializer: S,
266 ) -> Result<S::Ok, S::Error>
267 where
268 S: Serializer,
269 {
270 serializer.serialize_u8(torque.map_or(0, TunableTorque::into_inner))
271 }
272
273 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<TunableTorque>, D::Error>
274 where
275 D: Deserializer<'de>,
276 {
277 let value = u8::deserialize(deserializer)?;
278 if value == 0 {
279 Ok(None)
280 } else {
281 TunableTorque::try_from(value)
282 .map(Some)
283 .map_err(serde::de::Error::custom)
284 }
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub struct SmartShiftStatus {
295 pub mode: SmartShiftMode,
297 pub auto_disengage: SmartShiftAutoDisengage,
299 #[serde(with = "optional_tunable_torque")]
303 pub tunable_torque: Option<TunableTorque>,
304}
305
306impl From<crate::config::SmartShift> for SmartShiftStatus {
307 fn from(config: crate::config::SmartShift) -> Self {
308 Self {
309 mode: config.mode.into(),
310 auto_disengage: config.auto_disengage,
311 tunable_torque: config.tunable_torque,
312 }
313 }
314}
315
316impl From<SmartShiftStatus> for crate::config::SmartShift {
317 fn from(status: SmartShiftStatus) -> Self {
318 Self {
319 mode: status.mode.into(),
320 auto_disengage: status.auto_disengage,
321 tunable_torque: status.tunable_torque,
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn flipped_is_an_involution() {
332 assert_eq!(SmartShiftMode::Free.flipped(), SmartShiftMode::Ratchet);
333 assert_eq!(SmartShiftMode::Ratchet.flipped(), SmartShiftMode::Free);
334 assert_eq!(
335 SmartShiftMode::Free.flipped().flipped(),
336 SmartShiftMode::Free
337 );
338 }
339
340 #[test]
341 fn auto_disengage_reserves_zero_and_models_permanent_ratchet()
342 -> Result<(), SmartShiftThresholdError> {
343 let Err(_) = SmartShiftAutoDisengage::try_from(0) else {
344 panic!("zero is the write-only preserve sentinel");
345 };
346 assert_eq!(
347 SmartShiftAutoDisengage::try_from(16),
348 Ok(SmartShiftAutoDisengage::Threshold(
349 SmartShiftThreshold::try_new(16)?
350 ))
351 );
352 assert_eq!(
353 SmartShiftAutoDisengage::try_from(0xff),
354 Ok(SmartShiftAutoDisengage::Permanent)
355 );
356 Ok(())
357 }
358
359 #[test]
360 fn floating_thresholds_round_and_saturate_into_the_domain() {
361 assert_eq!(u8::from(SmartShiftThreshold::from_rounded(15.6)), 16);
362 assert_eq!(u8::from(SmartShiftThreshold::from_rounded(f32::NAN)), 1);
363 assert_eq!(
364 u8::from(SmartShiftThreshold::from_rounded(f32::NEG_INFINITY)),
365 1
366 );
367 assert_eq!(
368 u8::from(SmartShiftThreshold::from_rounded(f32::INFINITY)),
369 254
370 );
371 }
372
373 #[test]
374 fn tunable_torque_is_a_percentage() {
375 let Err(_) = TunableTorque::try_new(0) else {
376 panic!("zero is the write-only do-not-change sentinel");
377 };
378 assert_eq!(TunableTorque::try_new(1).map(u8::from), Ok(1));
379 assert_eq!(TunableTorque::try_new(100).map(u8::from), Ok(100));
380 let Err(_) = TunableTorque::try_new(101) else {
381 panic!("torque above 100% of the maximum force is not a level");
382 };
383 }
384}