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(
224 const_fn,
225 validate(greater_or_equal = 1),
226 derive(
227 Debug,
228 Clone,
229 Copy,
230 PartialEq,
231 Eq,
232 PartialOrd,
233 Ord,
234 TryFrom,
235 Into,
236 Display,
237 Serialize,
238 Deserialize
239 )
240)]
241pub struct TunableTorque(u8);
242
243impl From<TunableTorque> for NonZeroU8 {
244 fn from(torque: TunableTorque) -> Self {
245 let Some(value) = Self::new(torque.into_inner()) else {
246 unreachable!("tunable torque is non-zero");
247 };
248 value
249 }
250}
251
252pub(crate) mod optional_tunable_torque {
253 use super::TunableTorque;
254 use serde::{Deserialize, Deserializer, Serializer};
255
256 #[expect(
257 clippy::ref_option,
258 clippy::trivially_copy_pass_by_ref,
259 reason = "serde field serializers must receive the field by reference"
260 )]
261 pub(crate) fn serialize<S>(
262 torque: &Option<TunableTorque>,
263 serializer: S,
264 ) -> Result<S::Ok, S::Error>
265 where
266 S: Serializer,
267 {
268 serializer.serialize_u8(torque.map_or(0, TunableTorque::into_inner))
269 }
270
271 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<TunableTorque>, D::Error>
272 where
273 D: Deserializer<'de>,
274 {
275 let value = u8::deserialize(deserializer)?;
276 if value == 0 {
277 Ok(None)
278 } else {
279 TunableTorque::try_from(value)
280 .map(Some)
281 .map_err(serde::de::Error::custom)
282 }
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
292pub struct SmartShiftStatus {
293 pub mode: SmartShiftMode,
295 pub auto_disengage: SmartShiftAutoDisengage,
297 #[serde(with = "optional_tunable_torque")]
301 pub tunable_torque: Option<TunableTorque>,
302}
303
304impl From<crate::config::SmartShift> for SmartShiftStatus {
305 fn from(config: crate::config::SmartShift) -> Self {
306 Self {
307 mode: config.mode.into(),
308 auto_disengage: config.auto_disengage,
309 tunable_torque: config.tunable_torque,
310 }
311 }
312}
313
314impl From<SmartShiftStatus> for crate::config::SmartShift {
315 fn from(status: SmartShiftStatus) -> Self {
316 Self {
317 mode: status.mode.into(),
318 auto_disengage: status.auto_disengage,
319 tunable_torque: status.tunable_torque,
320 }
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn flipped_is_an_involution() {
330 assert_eq!(SmartShiftMode::Free.flipped(), SmartShiftMode::Ratchet);
331 assert_eq!(SmartShiftMode::Ratchet.flipped(), SmartShiftMode::Free);
332 assert_eq!(
333 SmartShiftMode::Free.flipped().flipped(),
334 SmartShiftMode::Free
335 );
336 }
337
338 #[test]
339 fn auto_disengage_reserves_zero_and_models_permanent_ratchet()
340 -> Result<(), SmartShiftThresholdError> {
341 let Err(_) = SmartShiftAutoDisengage::try_from(0) else {
342 panic!("zero is the write-only preserve sentinel");
343 };
344 assert_eq!(
345 SmartShiftAutoDisengage::try_from(16),
346 Ok(SmartShiftAutoDisengage::Threshold(
347 SmartShiftThreshold::try_new(16)?
348 ))
349 );
350 assert_eq!(
351 SmartShiftAutoDisengage::try_from(0xff),
352 Ok(SmartShiftAutoDisengage::Permanent)
353 );
354 Ok(())
355 }
356
357 #[test]
358 fn floating_thresholds_round_and_saturate_into_the_domain() {
359 assert_eq!(u8::from(SmartShiftThreshold::from_rounded(15.6)), 16);
360 assert_eq!(u8::from(SmartShiftThreshold::from_rounded(f32::NAN)), 1);
361 assert_eq!(
362 u8::from(SmartShiftThreshold::from_rounded(f32::NEG_INFINITY)),
363 1
364 );
365 assert_eq!(
366 u8::from(SmartShiftThreshold::from_rounded(f32::INFINITY)),
367 254
368 );
369 }
370}