use std::{fmt, num::NonZeroU8};
use az::SaturatingAs;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use nutype::nutype;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, Serialize, Deserialize,
)]
#[repr(u8)]
pub enum SmartShiftMode {
Free = 1,
Ratchet = 2,
}
impl SmartShiftMode {
#[must_use]
pub fn flipped(self) -> Self {
match self {
Self::Free => Self::Ratchet,
Self::Ratchet => Self::Free,
}
}
}
impl From<crate::config::WheelMode> for SmartShiftMode {
fn from(mode: crate::config::WheelMode) -> Self {
match mode {
crate::config::WheelMode::Free => Self::Free,
crate::config::WheelMode::Ratchet => Self::Ratchet,
}
}
}
impl From<SmartShiftMode> for crate::config::WheelMode {
fn from(mode: SmartShiftMode) -> Self {
match mode {
SmartShiftMode::Free => Self::Free,
SmartShiftMode::Ratchet => Self::Ratchet,
}
}
}
#[nutype(
const_fn,
validate(greater_or_equal = 1, less_or_equal = 254),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
TryFrom,
Into,
Display,
Serialize,
Deserialize
)
)]
pub struct SmartShiftThreshold(u8);
impl SmartShiftThreshold {
#[must_use]
pub fn from_rounded(value: f32) -> Self {
let value = if value.is_nan() { 1.0 } else { value };
let raw = value.clamp(1.0, 254.0).round().saturating_as::<u8>();
let Ok(value) = Self::try_new(raw) else {
unreachable!("clamped SmartShift threshold is always valid");
};
value
}
}
impl From<SmartShiftThreshold> for f32 {
fn from(threshold: SmartShiftThreshold) -> Self {
Self::from(threshold.into_inner())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SmartShiftAutoDisengage {
Threshold(SmartShiftThreshold),
Permanent,
}
impl SmartShiftAutoDisengage {
#[must_use]
pub const fn is_permanent(self) -> bool {
matches!(self, Self::Permanent)
}
#[must_use]
pub const fn threshold(self) -> Option<SmartShiftThreshold> {
match self {
Self::Threshold(threshold) => Some(threshold),
Self::Permanent => None,
}
}
}
impl TryFrom<u8> for SmartShiftAutoDisengage {
type Error = SmartShiftThresholdError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value == u8::MAX {
Ok(Self::Permanent)
} else {
SmartShiftThreshold::try_from(value).map(Self::Threshold)
}
}
}
impl From<SmartShiftAutoDisengage> for u8 {
fn from(auto_disengage: SmartShiftAutoDisengage) -> Self {
match auto_disengage {
SmartShiftAutoDisengage::Threshold(threshold) => threshold.into_inner(),
SmartShiftAutoDisengage::Permanent => Self::MAX,
}
}
}
impl From<NonZeroU8> for SmartShiftAutoDisengage {
fn from(value: NonZeroU8) -> Self {
if value == NonZeroU8::MAX {
Self::Permanent
} else {
let Ok(threshold) = SmartShiftThreshold::try_new(value.get()) else {
unreachable!("non-zero SmartShift values below 255 are thresholds");
};
Self::Threshold(threshold)
}
}
}
impl From<SmartShiftAutoDisengage> for NonZeroU8 {
fn from(auto_disengage: SmartShiftAutoDisengage) -> Self {
match auto_disengage {
SmartShiftAutoDisengage::Threshold(threshold) => {
let Some(value) = Self::new(threshold.into_inner()) else {
unreachable!("SmartShift thresholds are non-zero");
};
value
}
SmartShiftAutoDisengage::Permanent => Self::MAX,
}
}
}
impl fmt::Display for SmartShiftAutoDisengage {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
u8::from(*self).fmt(formatter)
}
}
impl Serialize for SmartShiftAutoDisengage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8((*self).into())
}
}
impl<'de> Deserialize<'de> for SmartShiftAutoDisengage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Self::try_from(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
}
}
#[nutype(
const_fn,
validate(greater_or_equal = 1),
derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
TryFrom,
Into,
Display,
Serialize,
Deserialize
)
)]
pub struct TunableTorque(u8);
impl From<TunableTorque> for NonZeroU8 {
fn from(torque: TunableTorque) -> Self {
let Some(value) = Self::new(torque.into_inner()) else {
unreachable!("tunable torque is non-zero");
};
value
}
}
pub(crate) mod optional_tunable_torque {
use super::TunableTorque;
use serde::{Deserialize, Deserializer, Serializer};
#[expect(
clippy::ref_option,
clippy::trivially_copy_pass_by_ref,
reason = "serde field serializers must receive the field by reference"
)]
pub(crate) fn serialize<S>(
torque: &Option<TunableTorque>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8(torque.map_or(0, TunableTorque::into_inner))
}
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<TunableTorque>, D::Error>
where
D: Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
if value == 0 {
Ok(None)
} else {
TunableTorque::try_from(value)
.map(Some)
.map_err(serde::de::Error::custom)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SmartShiftStatus {
pub mode: SmartShiftMode,
pub auto_disengage: SmartShiftAutoDisengage,
#[serde(with = "optional_tunable_torque")]
pub tunable_torque: Option<TunableTorque>,
}
impl From<crate::config::SmartShift> for SmartShiftStatus {
fn from(config: crate::config::SmartShift) -> Self {
Self {
mode: config.mode.into(),
auto_disengage: config.auto_disengage,
tunable_torque: config.tunable_torque,
}
}
}
impl From<SmartShiftStatus> for crate::config::SmartShift {
fn from(status: SmartShiftStatus) -> Self {
Self {
mode: status.mode.into(),
auto_disengage: status.auto_disengage,
tunable_torque: status.tunable_torque,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flipped_is_an_involution() {
assert_eq!(SmartShiftMode::Free.flipped(), SmartShiftMode::Ratchet);
assert_eq!(SmartShiftMode::Ratchet.flipped(), SmartShiftMode::Free);
assert_eq!(
SmartShiftMode::Free.flipped().flipped(),
SmartShiftMode::Free
);
}
#[test]
fn auto_disengage_reserves_zero_and_models_permanent_ratchet()
-> Result<(), SmartShiftThresholdError> {
let Err(_) = SmartShiftAutoDisengage::try_from(0) else {
panic!("zero is the write-only preserve sentinel");
};
assert_eq!(
SmartShiftAutoDisengage::try_from(16),
Ok(SmartShiftAutoDisengage::Threshold(
SmartShiftThreshold::try_new(16)?
))
);
assert_eq!(
SmartShiftAutoDisengage::try_from(0xff),
Ok(SmartShiftAutoDisengage::Permanent)
);
Ok(())
}
#[test]
fn floating_thresholds_round_and_saturate_into_the_domain() {
assert_eq!(u8::from(SmartShiftThreshold::from_rounded(15.6)), 16);
assert_eq!(u8::from(SmartShiftThreshold::from_rounded(f32::NAN)), 1);
assert_eq!(
u8::from(SmartShiftThreshold::from_rounded(f32::NEG_INFINITY)),
1
);
assert_eq!(
u8::from(SmartShiftThreshold::from_rounded(f32::INFINITY)),
254
);
}
}