Skip to main content

hidpp/feature/
registry.rs

1//! Maintains a registry of well-known HID++2.0 features and their default
2//! implementations.
3
4use std::{
5    any::TypeId,
6    collections::HashMap,
7    sync::{Arc, LazyLock},
8};
9
10use super::Feature;
11use crate::{
12    channel::HidppChannel,
13    feature::{
14        CreatableFeature,
15        adjustable_dpi::AdjustableDpiFeature,
16        backlight::BacklightFeature,
17        battery_status::BatteryStatusFeature,
18        battery_voltage::BatteryVoltageFeature,
19        brightness_control::BrightnessControlFeature,
20        change_host::ChangeHostFeature,
21        color_led_effects::ColorLedEffectsFeature,
22        crown::CrownFeature,
23        device_friendly_name::DeviceFriendlyNameFeature,
24        device_information::DeviceInformationFeature,
25        device_type_and_name::DeviceTypeAndNameFeature,
26        disable_keys::DisableKeysFeature,
27        disable_keys_by_usage::DisableKeysByUsageFeature,
28        dual_platform::DualPlatformFeature,
29        equalizer::EqualizerFeature,
30        extended_dpi::ExtendedDpiFeature,
31        extended_report_rate::ExtendedReportRateFeature,
32        feature_set::FeatureSetFeature,
33        fn_inversion::{FnInversionMultiHostFeature, FnInversionWithDefaultStateFeature},
34        gestures2::Gestures2Feature,
35        haptic_feedback::HapticFeedbackFeature,
36        hires_wheel::HiResWheelFeature,
37        hosts_info::HostsInfoFeature,
38        illumination::IlluminationFeature,
39        mode_status::ModeStatusFeature,
40        mouse_pointer::MousePointerFeature,
41        multi_platform::MultiPlatformFeature,
42        per_key_lighting::PerKeyLightingFeature,
43        persistent_remappable_action::PersistentRemappableActionFeature,
44        report_rate::ReportRateFeature,
45        reprog_controls::ReprogControlsFeature,
46        rgb_effects::RgbEffectsFeature,
47        root::RootFeature,
48        sidetone::SidetoneFeature,
49        smartshift::SmartShiftFeature,
50        smartshift_enhanced::SmartShiftEnhancedFeature,
51        solar_dashboard::SolarDashboardFeature,
52        thumbwheel::ThumbwheelFeature,
53        touch_mouse_raw::TouchMouseRawFeature,
54        touchpad_raw_xy::TouchpadRawXyFeature,
55        unified_battery::UnifiedBatteryFeature,
56        vertical_scrolling::VerticalScrollingFeature,
57        wireless_device_status::WirelessDeviceStatusFeature,
58    },
59};
60
61/// Represents a function that creates a new dynamically sized feature
62/// implementation.
63pub type FeatureImplProducer =
64    fn(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> (TypeId, Arc<dyn Feature>);
65
66/// Represents a known feature implementation starting from a specific feature
67/// version.
68#[derive(Clone, Copy, Debug, Hash)]
69pub struct FeatureVersion {
70    /// The minimum feature version the implementation supports.
71    pub starting_version: u8,
72
73    /// A pointer to a function producing the feature implementation.
74    pub producer: FeatureImplProducer,
75}
76
77/// Represents a known HID++2.0 device feature.
78#[derive(Clone, Copy, Debug, Hash)]
79pub struct KnownFeature {
80    /// The name of the feature.
81    /// This is usually a slightly modified version of the name found in
82    /// Logitech's documentation.
83    pub name: &'static str,
84
85    /// A list of concrete implementations of the feature, each supporting the
86    /// feature starting from a specific version.
87    pub versions: &'static [FeatureVersion],
88}
89
90/// Looks up a feature by its ID.
91pub fn lookup(feature_id: u16) -> Option<KnownFeature> {
92    KNOWN_FEATURES.get(&feature_id).copied()
93}
94
95/// Looks up all implementations supporting a specific feature ID and version
96/// combination.
97#[must_use]
98pub fn lookup_version(feature_id: u16, feature_version: u8) -> Option<Vec<FeatureVersion>> {
99    lookup(feature_id).map(|feat| {
100        feat.versions
101            .iter()
102            .filter(|&ver| ver.starting_version <= feature_version)
103            .copied()
104            .collect::<Vec<FeatureVersion>>()
105    })
106}
107
108/// Creates a new feature with a dynamic return type.
109fn new_dyn<F: CreatableFeature>(
110    chan: Arc<HidppChannel>,
111    device_index: u8,
112    feature_index: u8,
113) -> (TypeId, Arc<dyn Feature>) {
114    (
115        TypeId::of::<F>(),
116        Arc::new(F::new(chan, device_index, feature_index)),
117    )
118}
119
120/// Builds [`KNOWN_FEATURES`]. Each row is `id "Name"` for a feature we only know
121/// by name, or `id "Name" => Impl, ...` to also register one or more default
122/// implementations through [`new_dyn`]. Listing several impls mirrors a feature
123/// that ships multiple versions, each contributing its own
124/// [`CreatableFeature::STARTING_VERSION`] in declaration order.
125///
126/// Each listed impl's [`CreatableFeature::ID`] is asserted at compile time to
127/// equal the row's `id` literal, so a typo can never register a feature's
128/// default implementation under the wrong wire id.
129macro_rules! known_features {
130    ( $( $id:literal $name:literal $( => $($feat:ty),+ )? ),* $(,)? ) => {
131        HashMap::from([ $(
132            ($id, KnownFeature { name: $name, versions: known_features!(@versions $id $( $($feat),+ )?) }),
133        )* ])
134    };
135    (@versions $id:literal) => { &[] };
136    (@versions $id:literal $($feat:ty),+) => {
137        &[$(FeatureVersion {
138            starting_version: {
139                const {
140                    assert!(
141                        <$feat>::ID == $id,
142                        "registry id must match the impl's CreatableFeature::ID"
143                    );
144                }
145                <$feat>::STARTING_VERSION
146            },
147            producer: new_dyn::<$feat>,
148        }),+]
149    };
150}
151
152static KNOWN_FEATURES: LazyLock<HashMap<u16, KnownFeature>> = LazyLock::new(|| {
153    known_features! {
154    0x0000 "Root" => RootFeature,
155    0x0001 "FeatureSet" => FeatureSetFeature,
156    0x0002 "FeatureInfo",
157    0x0003 "DeviceInformation" => DeviceInformationFeature,
158    0x0004 "UnitId",
159    0x0005 "DeviceTypeAndName" => DeviceTypeAndNameFeature,
160    0x0006 "DeviceGroups",
161    0x0007 "DeviceFriendlyName" => DeviceFriendlyNameFeature,
162    0x0008 "KeepAlive",
163    0x0020 "ConfigChange",
164    0x0021 "UniqueRandomId",
165    0x0030 "TargetSoftware",
166    0x0080 "WirelessSignalStrength",
167    0x00c0 "DfuControlLegacy",
168    0x00c1 "DfuControlUnsigned",
169    0x00c2 "DfuControlSigned",
170    0x00c3 "DfuControlBolt",
171    0x00d0 "Dfu",
172    0x00d1 "DfuResumable",
173    0x1000 "BatteryStatus" => BatteryStatusFeature,
174    0x1001 "BatteryVoltage" => BatteryVoltageFeature,
175    0x1004 "UnifiedBattery" => UnifiedBatteryFeature,
176    0x1010 "ChargingControl",
177    0x1300 "LedControl",
178    0x1800 "GenericTest",
179    0x1802 "DeviceReset",
180    0x1805 "OobState",
181    0x1806 "ConfigDeviceProps",
182    0x1814 "ChangeHost" => ChangeHostFeature,
183    0x1815 "HostsInfo" => HostsInfoFeature,
184    0x1981 "Backlight1",
185    0x1982 "Backlight2" => BacklightFeature,
186    0x1983 "Backlight3",
187    0x1990 "Illumination" => IlluminationFeature,
188    0x19b0 "HapticFeedback" => HapticFeedbackFeature,
189    // Reverse-engineered name observed in MX Master 4 metadata; no public HID++
190    // definition is available, so it remains intentionally unimplemented.
191    0x19c0 "ForceSensingButton",
192    0x1a00 "PresenterControl",
193    0x1a01 "Sensor3D",
194    0x1b00 "ReprogControls",
195    0x1b01 "ReprogControls2",
196    0x1b02 "ReprogControls3",
197    0x1b03 "ReprogControls4",
198    0x1b04 "ReprogControls5" => ReprogControlsFeature,
199    0x1bc0 "ReportHidUsages",
200    0x1c00 "PersistentRemappableAction" => PersistentRemappableActionFeature,
201    0x1d4b "WirelessDeviceStatus" => WirelessDeviceStatusFeature,
202    0x1df0 "RemainingPairings",
203    0x1f1f "FirmwareProperties",
204    0x1f20 "AdcMeasurement",
205    0x2001 "SwapLeftRightButton",
206    0x2005 "ButtonSwapCancel",
207    0x2006 "PointerAxesOrientation",
208    0x2100 "VerticalScrolling" => VerticalScrollingFeature,
209    0x2110 "SmartShiftWheel" => SmartShiftFeature,
210    0x2111 "SmartShiftWheelEnhanced" => SmartShiftEnhancedFeature,
211    0x2120 "HighResolutionScrolling",
212    0x2121 "HiResWheel" => HiResWheelFeature,
213    0x2130 "RatchetWheel",
214    0x2150 "Thumbwheel" => ThumbwheelFeature,
215    0x2200 "MousePointer" => MousePointerFeature,
216    0x2201 "AdjustableDpi" => AdjustableDpiFeature,
217    0x2202 "ExtendedAdjustableDpi" => ExtendedDpiFeature,
218    0x2205 "PointerMotionScaling",
219    0x2230 "SensorAngleSnapping",
220    0x2240 "SurfaceTuning",
221    0x2250 "XyStats",
222    0x2251 "WheelStats",
223    0x2400 "HybridTrackingEngine",
224    0x40a0 "FnInversion",
225    0x40a2 "FnInversionWithDefaultState" => FnInversionWithDefaultStateFeature,
226    0x40a3 "FnInversionForMultiHostDevices" => FnInversionMultiHostFeature,
227    0x4100 "Encryption",
228    0x4220 "LockKeyState",
229    0x4301 "SolarKeyboardDashboard" => SolarDashboardFeature,
230    0x4520 "KeyboardLayout",
231    0x4521 "DisableKeys" => DisableKeysFeature,
232    0x4522 "DisableKeysByUsage" => DisableKeysByUsageFeature,
233    0x4530 "DualPlatform" => DualPlatformFeature,
234    0x4531 "MultiPlatform" => MultiPlatformFeature,
235    0x4540 "KeyboardInternationalLayouts",
236    0x4600 "Crown" => CrownFeature,
237    0x6010 "TouchpadFwItems",
238    0x6011 "TouchpadSwItems",
239    0x6012 "TouchpadWin8FwItems",
240    0x6020 "TapEnable",
241    0x6021 "TapEnableExtended",
242    0x6030 "CursorBallistic",
243    0x6040 "TouchpadResolutionDivider",
244    0x6100 "TouchpadRawXy" => TouchpadRawXyFeature,
245    0x6110 "TouchMouseRawTouchPoints" => TouchMouseRawFeature,
246    0x6120 "BtTouchMouseSettings",
247    0x6500 "Gestures1",
248    0x6501 "Gestures2" => Gestures2Feature,
249    0x8010 "GamingGKeys",
250    0x8020 "GamingMKeys",
251    0x8030 "MacroRecord",
252    0x8040 "BrightnessControl" => BrightnessControlFeature,
253    0x8060 "AdjustableReportRate" => ReportRateFeature,
254    0x8061 "ExtendedAdjustableReportRate" => ExtendedReportRateFeature,
255    0x8070 "ColorLedEffects" => ColorLedEffectsFeature,
256    0x8071 "RgbEffects" => RgbEffectsFeature,
257    0x8080 "PerKeyLighting",
258    0x8081 "PerKeyLighting2" => PerKeyLightingFeature,
259    0x8090 "ModeStatus" => ModeStatusFeature,
260    0x8100 "OnboardProfiles",
261    0x8110 "MouseButtonFilter",
262    0x8111 "LatencyMonitoring",
263    0x8120 "GamingAttachments",
264    0x8123 "ForceFeedback",
265    0x8300 "Sidetone" => SidetoneFeature,
266    0x8310 "Equalizer" => EqualizerFeature,
267    0x8320 "HeadsetOut",
268    }
269});
270
271#[cfg(test)]
272mod tests {
273    use std::collections::HashMap;
274
275    use super::{FeatureVersion, KnownFeature, new_dyn};
276    use crate::feature::{CreatableFeature, feature_set::FeatureSetFeature, root::RootFeature};
277
278    #[test]
279    fn macro_registers_one_version_per_listed_impl() {
280        // The `=> A, B` form keeps the original table's ability to register
281        // several versioned implementations under a single feature id. Every
282        // row's id must match its impls' real `CreatableFeature::ID`, so the
283        // two-impl row lists `FeatureSetFeature` twice under its own id
284        // (0x0001) rather than pairing it with an unrelated feature — no
285        // current registry entry actually ships two version-gated impls
286        // under one id, but the macro's counting behaviour is the same.
287        let map: HashMap<u16, KnownFeature> = known_features! {
288            0x0002 "NameOnly",
289            0x0000 "OneImpl" => RootFeature,
290            0x0001 "TwoImpls" => FeatureSetFeature, FeatureSetFeature,
291        };
292
293        assert_eq!(map[&0x0002].versions.len(), 0);
294        assert_eq!(map[&0x0000].versions.len(), 1);
295        assert_eq!(map[&0x0001].versions.len(), 2);
296    }
297}