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