# HID++ Reference (/hidpp)



HID++ is Logitech's vendor protocol for configuring mice, keyboards, and
receivers over USB HID. OpenLogi speaks it directly — the same protocol Options+
uses — through the [`hidpp`](https://crates.io/crates/hidpp) Rust crate.

## Two generations [#two-generations]

* **HID++ 1.0** — a fixed set of numbered **registers**. Used mainly by older
  Unifying receivers for pairing and device enumeration.
* **HID++ 2.0** — a discoverable set of **features**, each identified by a 16-bit
  ID (written `0xZZZZ`, e.g. `0x2201`). This is what modern devices use.

## Feature discovery [#feature-discovery]

A 2.0 device exposes its capabilities through two root features:

* **[`0x0000` root](/hidpp/features/x0000-root)** — resolves a feature ID to
  its runtime *feature index*.
* **[`0x0001` featureSet](/hidpp/features/x0001-featureset)** — enumerates
  every feature the device implements.

From there, OpenLogi queries individual features: device information, battery,
DPI, SmartShift, and so on. See the [feature index](/hidpp/features).

## Identity [#identity]

Two things name a device on the wire: a receiver by its USB vendor/product ID
(see [Receivers](/hidpp/receivers)), and a device by the model id it reports
through `0x0003` — every one of which is listed in the
[model id index](/hidpp/model-ids).

<Callout type="info" title="Attribution & copyright">
  This HID++ reference is an **original summary written for the OpenLogi
  project**. It describes Logitech's HID++ 1.0 / 2.0 protocol as understood from
  publicly available documentation and the
  [`hidpp`](https://crates.io/crates/hidpp) implementation, and reproduces no
  Logitech document verbatim. “Logitech”, “Unifying”, “Logi Bolt” and “Options+”
  are trademarks of Logitech; OpenLogi is an independent project and is not
  affiliated with, authorized, or endorsed by Logitech. The material is provided
  “as is”, without warranty of any kind.
</Callout>


# Model IDs (/hidpp/model-ids)





A **Model ID** is a string Logitech's tools use to identify a product. OpenLogi
uses it to store per-device configuration and locate device renders and hotspot
data.

## Model ID structure [#model-id-structure]

Devices report two kinds of identity data through
[`0x0003 deviceInformation`](/hidpp/features/x0003-device-information):

* `model_id`: up to three PIDs, one for each transport enabled by the firmware.
  Unused entries are `0000`.
* `extended_model_id`: one byte used for production attributes such as the
  colour batch.

`model_id` supports these transports:

* `usb`: wired USB
* `equad`: the RF protocol used by Unifying receivers
* `btle`: Bluetooth Low Energy, also used by Bolt receivers
* `bt`: Bluetooth Classic

`openlogi list` displays both fields under each paired device:

```text
     model_ids=[b042,0000,0000] ext=02 serial=— unit_id=00010203 transports=usb+equad
```

The registry's Model ID joins the extended byte and the first PID:

```text
model id = {extended_model_id:x}{model_id[0]:04x}
```

For example, `ext=02` and `model_ids=[b042,…]` produce `2b042`.

<ModelIdAnatomy />

Four-digit Model IDs contain only the PID. Older asset depots and products that
the registry identifies only by USB PID usually use this format. Examples
include receivers (`c548`), webcams (`085e`), and G-series audio devices
(`0a9b`).

### Match by the last four digits [#match-by-the-last-four-digits]

The extended value in the registry does not always match the value reported by
the device. For example, the MX Master 4 is listed as `2b042`, but the
device reports `ext=01`.

The same product can also report a different PID for each transport. The
MX Master 3S reports `b034` over Bluetooth Low Energy and `b043`
through a receiver. The registry lists both PIDs.

OpenLogi first matches the full `ext + pid`, then the trailing four-digit PID.
If neither matches, it uses the firmware codename to find the product name in
the registry. Search this page by the four-digit PID reported by the device
rather than relying on the extended value.

## Index [#index]

Each row represents one product in the registry and includes all its Model IDs.
A Model ID is not a unique key. The same G915 has old and new asset depots, and
a racing wheel base's PID is repeated in the entries for compatible rims. A few
IDs therefore appear on more than one row.

Some products have no HID++ identity. The registry uses the asset depot name in
place of an ID, as it does with `g29`. This page keeps those original values.

<ModelIdIndex />

The same data is available at [`/model-ids.json`](/model-ids.json), in the same
order as the table. Each item has the shape
`{ ids, name, kind, depot, render }`, so scripts can read it directly. `depot`
is the device's asset directory on
[assets.openlogi.org](https://assets.openlogi.org), and `render` is its main
image.

## Limitations [#limitations]

* **The data comes from the asset registry.** This page includes only products
  for which Logitech has published assets, including some G-series wheels,
  pedals, and headsets. Products without public assets do not appear here, but
  they may still have valid Model IDs.
* **Only IDs published by the registry are included.** A device can report up
  to three PIDs, but most asset depots publish only one. The PID reported by a
  device may therefore be missing from this page. Searching by the last four
  digits avoids mismatches caused only by a different extended value.
* **Receivers are listed as separate products.** The table identifies each one
  by its USB PID. See [Receivers](/hidpp/receivers) for how OpenLogi communicates
  with them.
* **Being listed does not imply support.** OpenLogi opens any Logitech HID
  interface with an HID++ vendor-defined collection, so an unlisted device may
  still work with OpenLogi. Conversely, OpenLogi does not configure the headsets
  and microphones listed here. See [Supported devices](/docs/supported-devices)
  for the features exposed by each product.


# Receivers (/hidpp/receivers)



A *receiver* is the USB dongle that wireless Logitech devices pair to. OpenLogi
detects the receiver on its USB/HID channel, then enumerates the devices paired
to it.

Unlike device features (which use HID++ 2.0), receivers are driven through
**HID++ 1.0 registers** and are always addressed at device index `0xFF`
(`RECEIVER_DEVICE_INDEX`). They are identified purely by their USB vendor/product
ID; `openlogi-hidpp`'s `receiver::detect()` matches the channel's VID/PID
against the known sets and returns a `Receiver`.

<Callout type="info" title="Implementation status">
  Receiver support in the vendored `hidpp` crate is deliberately conservative;
  public documentation is scarce. **Logi Bolt** is the most complete (developed and
  tested against real hardware); **Unifying** covers discovery and enumeration but
  its pairing/management surface is thinner and welcomes hardware testing.
</Callout>

## Detection [#detection]

| Receiver   | VID:PID                                            | Transport                            |
| ---------- | -------------------------------------------------- | ------------------------------------ |
| Logi Bolt  | `046D:C548`                                        | HID++ 1.0 registers (BLE-based)      |
| Unifying   | `046D:C52B`, `046D:C532`, `046D:C537`, `046D:C539` | HID++ 1.0 registers (Unifying / DJ)  |
| Lightspeed | `046D:C53F`, `046D:C547`                           | Unifying registers, G-series dongles |

`receiver::detect(chan)` returns `Some(Receiver)` for a known dongle, or `None`
otherwise.

The **Lightspeed** dongles bundled with G-series devices answer the same HID++
1.0 registers as Unifying, so they are detected, enumerated, routed, and paired
through the Unifying code path; only the user-facing name differs
(`Lightspeed Receiver`). `C53F` is the nano receiver of wireless mice such as the
G305, verified against a G305 (paired device wpid `0x4074`); `C547` ships with
newer devices such as the G915 keyboard and the G502 X LIGHTSPEED, verified
against a G915 (wpid `0x407c`). `C539` is the Lightspeed gaming receiver, listed
with the Unifying PIDs because it is routed as one. `C537` is the Nano receiver
bundled with the G602; it answers the same enumeration and pairing-information
registers, so it routes as a Unifying receiver.

## Common API [#common-api]

The `Receiver` enum wraps the concrete receiver kinds and exposes the shared
surface:

| Item                                          | Description                                                             |
| --------------------------------------------- | ----------------------------------------------------------------------- |
| `Receiver::Bolt(_)` / `Receiver::Unifying(_)` | The detected receiver kind.                                             |
| `name()`                                      | Human-readable name (e.g. "Logi Bolt Receiver").                        |
| `get_unique_id()`                             | A string that uniquely identifies this receiver (serial or equivalent). |
| `RECEIVER_DEVICE_INDEX` (`0xFF`)              | The device index used to address the receiver on the channel.           |

Errors surface as `ReceiverError`: `UnknownReceiver`, or a wrapped HID++ 1.0
`Protocol` error.

## Logi Bolt [#logi-bolt]

The current Logitech receiver. Bolt is **BLE-based**, pairs up to **6 devices**,
and authenticates new devices with a **passkey** before pairing. `bolt::Receiver`
exposes:

| Function                                                  | Purpose                                                                 |
| --------------------------------------------------------- | ----------------------------------------------------------------------- |
| `count_pairings()`                                        | Number of currently paired devices (offline ones included).             |
| `collect_paired_devices()`                                | Enumerate all paired devices (`Vec<DeviceConnection>`).                 |
| `get_device_pairing_information(index)`                   | Pairing info for one slot (`DevicePairingInformation`).                 |
| `get_device_codename(index)`                              | The device's codename / name.                                           |
| `get_notification_state()` / `set_notification_state(..)` | Read / enable receiver notifications.                                   |
| `trigger_device_arrival()`                                | Re-fire `DeviceConnection` events for all paired devices (enumeration). |
| `discover_devices(timeout)` / `cancel_device_discovery()` | Start / stop discovery of pairable devices (≤ 60 s).                    |
| `pair_device(slot, address, authentication, entropy)`     | Begin pairing a discovered device.                                      |
| `unpair_device(index)`                                    | Remove a pairing.                                                       |
| `listen()`                                                | Subscribe to receiver `Event`s.                                         |

### Pairing flow [#pairing-flow]

1. `discover_devices(Some(secs))` — the receiver emits
   `Event::DeviceDiscoveryDeviceDetails` (carrying the device `address`, `kind`,
   `wpid`, `authentication`) and `Event::DeviceDiscoveryDeviceName` for each
   nearby device.
2. `pair_device(slot, address, authentication, entropy)` — `entropy` sets passkey
   complexity (for mice, the number of left/right clicks the user must enter).
3. The receiver completes pairing and emits `Event::DeviceConnection`.

### Events (`bolt::Event`) [#events-boltevent]

| Variant                                                                         | When                                                                       |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `DeviceConnection(DeviceConnection)`                                            | A device connects / disconnects (requires wireless notifications enabled). |
| `DeviceDiscoveryStatus { discovery_enabled }`                                   | Discovery mode toggles.                                                    |
| `DeviceDiscoveryDeviceDetails { counter, kind, wpid, address, authentication }` | A device is discovered (details + address required to pair).               |
| `DeviceDiscoveryDeviceName { .. }`                                              | The discovered device's name.                                              |

`DeviceKind` covers `Keyboard`, `Mouse`, `Numpad`, `Presenter`, `Remote`,
`Trackball`, `Touchpad`, `Tablet`, `Gamepad`, `Joystick`, `Headset` (plus
`Unknown`).

## Unifying [#unifying]

The previous-generation receiver. Unifying pairs up to **6 devices** over the
proprietary Unifying / DJ protocol; once addressed by their slot index, paired
devices speak HID++ 2.0. `unifying::Receiver` exposes:

| Function                                | Purpose                                                                                            |
| --------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `count_pairings()`                      | Number of paired devices (offline ones included).                                                  |
| `get_receiver_info()`                   | Receiver info, including `pairing_slots` (`ReceiverInfo`).                                         |
| `get_device_pairing_information(index)` | Pairing info for one slot (`DevicePairingInformation`).                                            |
| `trigger_device_arrival()`              | Re-broadcast connection events for every paired slot, offline ones included (startup enumeration). |
| `get_unique_id()`                       | Receiver serial / unique id.                                                                       |
| `listen()`                              | Subscribe to receiver `Event`s.                                                                    |

## The `0x41` connection notification [#the-0x41-connection-notification]

Both receivers report device state through the same unsolicited HID++ 1.0
notification, sub-id `0x41`. It fires when a paired device connects or
disconnects, and `trigger_device_arrival()` makes the receiver re-broadcast it
for **every paired slot** — a slot whose device is powered off or out of range
still gets one, marked offline. The notification's device index is the device's
pairing slot, and the payload is:

| Byte | Content                        |
| ---- | ------------------------------ |
| 0    | Protocol type (eQUAD variant). |
| 1    | **Device-info byte** (below).  |
| 2–3  | Wireless PID, little-endian.   |

The device-info byte packs the device kind and four status flags:

| Bits | Mask   | Meaning                                                                                                                  |
| ---- | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| 0–3  | `0x0F` | Device kind (`0x1` keyboard, `0x2` mouse, …).                                                                            |
| 4    | `0x10` | **Software present** — the host declared driver software via the `SOFTWARE_PRESENT` notification flag (register `0x00`). |
| 5    | `0x20` | **Link encrypted** — the eQUAD radio link uses the encryption key established at pairing.                                |
| 6    | `0x40` | **Link not established** — set means the device is *offline*. Inverted sense.                                            |
| 7    | `0x80` | A payload packet follows.                                                                                                |

Bits 5 and 4 look alike but describe different worlds. Bit 5 is a property of
the 2.4 GHz link itself: whether traffic between device and receiver is
encrypted, decided at pairing time. It matters for security — unencrypted links
can be sniffed and injected over the air (the 2016 MouseJack attacks targeted
exactly these), which is why Solaar warns after pairing when the bit is clear.
Bolt links are always encrypted by design. Bit 4 says nothing about the radio:
it echoes a flag the *host* wrote into register `0x00` to declare "software is
controlling part of device behaviour", which device firmware uses to decide,
for example, whether gesture buttons emit diverted reports or fall back to
built-in behaviour.

<Callout type="warn" title="Bit 4 is not encryption">
  The two bits are easy to conflate: OpenLogi's vendored `hidpp` fork read bit 4
  as the encryption flag until it was checked against Solaar's decoder, and the
  layout is identical on Unifying and Bolt — there is no per-receiver variation
  to account for.
</Callout>

A captured example, short message payload `04 62 69 40`: protocol `0x04`
(eQUAD), device-info `0x62` = `0110 0010` — bit 6 set (offline), bit 5 set
(link encrypted at pairing), bit 4 clear, kind `0x2` (mouse) — and wpid
`0x4069`, an MX Master 2S. That is what an offline slot's re-broadcast looks
like: identity intact, link down.

## Enumerating paired devices (Rust) [#enumerating-paired-devices-rust]

```rust
use std::sync::Arc;

use hidpp::{
    channel::HidppChannel,
    receiver::{self, Receiver},
};

// chan: Arc<HidppChannel> bound to the receiver's HID interface.
let Some(rx) = receiver::detect(Arc::clone(&chan)) else {
    return; // not a known Logitech receiver
};
println!("{} — {}", rx.name(), rx.get_unique_id().await?);

match rx {
    Receiver::Bolt(bolt) => {
        for dev in bolt.collect_paired_devices().await? {
            // dev: bolt::DeviceConnection — one paired device
            let _ = dev;
        }
    }
    Receiver::Unifying(uni) => {
        let events = uni.listen();
        uni.trigger_device_arrival().await?; // re-fires DeviceConnection events
        while let Ok(event) = events.recv().await {
            // handle unifying::Event::DeviceConnection { .. }
            let _ = event;
            break;
        }
    }
    _ => {}
}
```

## No receiver [#no-receiver]

Bluetooth-direct and wired devices enumerate as their own inventory; no
receiver involved. See [Connect a device](/docs/connect-device).


# Changelog (/docs/changelog)



{/* AUTO-GENERATED by scripts/generate-changelog.mjs — do not edit by hand. */}

Every OpenLogi release, generated from the project's [GitHub Releases](https://github.com/AprilNEA/OpenLogi/releases). Release notes are written in English.

## v0.6.27 [#v0627]

*Released August 14, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.27)

#### What's Changed [#whats-changed]

* fix(ui): prevent middle and thumb wheel popover flicker by @the-long-ride in [#559](https://github.com/AprilNEA/OpenLogi/pull/559)
* fix(hook): release macOS tap after accessibility revocation by @yuki-yano in [#578](https://github.com/AprilNEA/OpenLogi/pull/578)
* feat(hid,hidpp): read battery over BatteryVoltage (0x1001) by @bobaoapae in [#575](https://github.com/AprilNEA/OpenLogi/pull/575)
* feat(hid): recognise Lightspeed receiver 046d:c547 (G915, G502 X) by @bobaoapae in [#574](https://github.com/AprilNEA/OpenLogi/pull/574)
* feat: capture the MX Master 4 haptic panel as a first-class control by @tagawa0525 in [#565](https://github.com/AprilNEA/OpenLogi/pull/565)
* fix(macos): prevent corrupted small app icons by @LJAYi in [#570](https://github.com/AprilNEA/OpenLogi/pull/570)
* fix(hid): widen the Bolt per-slot probe budget for high-latency USB paths by @tagawa0525 in [#562](https://github.com/AprilNEA/OpenLogi/pull/562)
* feat(gui): back navigation via the mouse's back button and Alt+Left by @tagawa0525 in [#563](https://github.com/AprilNEA/OpenLogi/pull/563)
* Revive the Nix flake for Linux, without the cargoHash churn that led to #262 by @tagawa0525 in [#491](https://github.com/AprilNEA/OpenLogi/pull/491)
* feat(hid): persist the immutable probe cache across restarts by @tagawa0525 in [#564](https://github.com/AprilNEA/OpenLogi/pull/564)
* feat(gui): add capability-driven actions ring by @jericho0521 in [#528](https://github.com/AprilNEA/OpenLogi/pull/528)
* feat(core): support stable Windows app selectors by @markus41 in [#572](https://github.com/AprilNEA/OpenLogi/pull/572)
* ci(nix): cache store paths with magic-nix-cache by @davidbudnick in [#580](https://github.com/AprilNEA/OpenLogi/pull/580)
* feat: per-slot custom labels for the Actions Ring by @isleofgreg in [#584](https://github.com/AprilNEA/OpenLogi/pull/584)
* fix(gui): redraw the Actions Ring on hover changes by @isleofgreg in [#585](https://github.com/AprilNEA/OpenLogi/pull/585)
* feat(agent): add a hardware-free mock agent for GUI development by @AprilNEA in [#568](https://github.com/AprilNEA/OpenLogi/pull/568)
* refactor: per-button gesture mode, replacing the one-owner lock by @tagawa0525 in [#566](https://github.com/AprilNEA/OpenLogi/pull/566)
* fix(agent): implement the Actions Ring IPC surface in the mock agent by @isleofgreg in [#587](https://github.com/AprilNEA/OpenLogi/pull/587)
* fix: Actions Ring haptic reliability — coalescing, feature cache, firmware arming, deadlock guards by @isleofgreg in [#590](https://github.com/AprilNEA/OpenLogi/pull/590)
* feat(agent): pressing the ring trigger again dismisses the Actions Ring by @isleofgreg in [#592](https://github.com/AprilNEA/OpenLogi/pull/592)
* fix(hid): detect and recover dead-delivery HID channels by @isleofgreg in [#589](https://github.com/AprilNEA/OpenLogi/pull/589)
* feat(gui): dismiss the Actions Ring on a click outside it by @isleofgreg in [#591](https://github.com/AprilNEA/OpenLogi/pull/591)
* fix(gui): open the Actions Ring on the display containing the cursor by @isleofgreg in [#588](https://github.com/AprilNEA/OpenLogi/pull/588)
* chore: release v0.6.27 by @aprilnea\[bot] in [#579](https://github.com/AprilNEA/OpenLogi/pull/579)

#### New Contributors [#new-contributors]

* @bobaoapae made their first contribution in [#575](https://github.com/AprilNEA/OpenLogi/pull/575)
* @LJAYi made their first contribution in [#570](https://github.com/AprilNEA/OpenLogi/pull/570)
* @jericho0521 made their first contribution in [#528](https://github.com/AprilNEA/OpenLogi/pull/528)
* @markus41 made their first contribution in [#572](https://github.com/AprilNEA/OpenLogi/pull/572)
* @isleofgreg made their first contribution in [#584](https://github.com/AprilNEA/OpenLogi/pull/584)

**Full Changelog**: [v0.6.26...v0.6.27](https://github.com/AprilNEA/OpenLogi/compare/v0.6.26...v0.6.27)

## v0.6.26 [#v0626]

*Released August 11, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.26)

#### What's Changed [#whats-changed-1]

* fix(macos): add camera hardened-runtime entitlement by @davidbudnick in [#557](https://github.com/AprilNEA/OpenLogi/pull/557)
* chore: release v0.6.26 by @aprilnea\[bot] in [#558](https://github.com/AprilNEA/OpenLogi/pull/558)

**Full Changelog**: [v0.6.25...v0.6.26](https://github.com/AprilNEA/OpenLogi/compare/v0.6.25...v0.6.26)

## v0.6.25 [#v0625]

*Released August 11, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.25)

#### What's Changed [#whats-changed-2]

* ci(release): keep release job on a branch by @davidbudnick in [#543](https://github.com/AprilNEA/OpenLogi/pull/543)
* fix(gui,assets): fit the Keys tab to legacy keyboard assets (G513) by @davidbudnick in [#544](https://github.com/AprilNEA/OpenLogi/pull/544)
* fix(hidpp): retry lost feature-table reads during enumeration by @kohjunhao in [#469](https://github.com/AprilNEA/OpenLogi/pull/469)
* feat: add MX Master 2S (3S) thumb wheel bindings by @the-long-ride in [#525](https://github.com/AprilNEA/OpenLogi/pull/525)
* feat(hid): recognise Lightspeed nano receivers (G-series, e.g. G305) by @kiwimaker in [#388](https://github.com/AprilNEA/OpenLogi/pull/388)
* fix(i18n): skip Crowdin English fill-in and restore locale parity by @davidbudnick in [#551](https://github.com/AprilNEA/OpenLogi/pull/551)
* feat: per-device capture — every online device gets its own session, bindings, and settings by @tagawa0525 in [#419](https://github.com/AprilNEA/OpenLogi/pull/419)
* fix(ci): merge Crowdin downloads into locale catalogs by @davidbudnick in [#553](https://github.com/AprilNEA/OpenLogi/pull/553)
* fix(gui,camera,xtask): make Camera permission grantable on macOS by @davidbudnick in [#550](https://github.com/AprilNEA/OpenLogi/pull/550)
* fix(hook): capture keyboard events on Windows by @davidbudnick in [#548](https://github.com/AprilNEA/OpenLogi/pull/548)
* fix(i18n): add camera permission locale keys by @davidbudnick in [#554](https://github.com/AprilNEA/OpenLogi/pull/554)
* chore: release v0.6.25 by @aprilnea\[bot] in [#545](https://github.com/AprilNEA/OpenLogi/pull/545)

#### New Contributors [#new-contributors-1]

* @kohjunhao made their first contribution in [#469](https://github.com/AprilNEA/OpenLogi/pull/469)
* @the-long-ride made their first contribution in [#525](https://github.com/AprilNEA/OpenLogi/pull/525)
* @kiwimaker made their first contribution in [#388](https://github.com/AprilNEA/OpenLogi/pull/388)

**Full Changelog**: [v0.6.24...v0.6.25](https://github.com/AprilNEA/OpenLogi/compare/v0.6.24...v0.6.25)

## v0.6.24 [#v0624]

*Released August 10, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.24)

#### What's Changed [#whats-changed-3]

* fix(i18n): complete Crowdin synchronization by @AprilNEA in [#508](https://github.com/AprilNEA/OpenLogi/pull/508)
* fix(agent): reuse inventory channels for input capture by @safonin in [#522](https://github.com/AprilNEA/OpenLogi/pull/522)
* ci(release): pin tags and fill changelog by @davidbudnick in [#533](https://github.com/AprilNEA/OpenLogi/pull/533)
* Back and Forward mouse buttons now work in Safari and Chrome by @b0x42 in [#363](https://github.com/AprilNEA/OpenLogi/pull/363)
* fix(agent): route hardware operations through inventory channels by @safonin in [#532](https://github.com/AprilNEA/OpenLogi/pull/532)
* feat(battery): support legacy 0x1000 BatteryStatus and its charging quirk by @laofun in [#312](https://github.com/AprilNEA/OpenLogi/pull/312)
* feat(backlight): support HID++ 0x1982 by @kirgene in [#470](https://github.com/AprilNEA/OpenLogi/pull/470)
* feat(camera): add Logitech webcam support by @davidbudnick in [#531](https://github.com/AprilNEA/OpenLogi/pull/531)
* Feat/linked host switching by @LCozatl in [#479](https://github.com/AprilNEA/OpenLogi/pull/479)
* fix(hook): never wedge system pointer input by @davidbudnick in [#534](https://github.com/AprilNEA/OpenLogi/pull/534)
* feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell) by @recchia in [#191](https://github.com/AprilNEA/OpenLogi/pull/191)
* feat: keyboard F-row key remapping and fn-lock over HID++ by @Sankew in [#395](https://github.com/AprilNEA/OpenLogi/pull/395)
* feat(hid): add standalone Litra light support by @tiaringhio in [#513](https://github.com/AprilNEA/OpenLogi/pull/513)
* feat: add function key remapper by @MichaelDanCurtis in [#344](https://github.com/AprilNEA/OpenLogi/pull/344)
* ci(i18n): fix Crowdin translation branch push by @davidbudnick in [#535](https://github.com/AprilNEA/OpenLogi/pull/535)
* docs: harden agent local-gate and push rules by @davidbudnick in [#536](https://github.com/AprilNEA/OpenLogi/pull/536)
* feat(hid): recognize Lightspeed receiver (046d:c539) as Unifying-compatible by @Abnersouza7 in [#510](https://github.com/AprilNEA/OpenLogi/pull/510)
* ci(i18n): Crowdin sync for non-English only by @davidbudnick in [#538](https://github.com/AprilNEA/OpenLogi/pull/538)
* ci(i18n): seed Crowdin per language before download by @davidbudnick in [#540](https://github.com/AprilNEA/OpenLogi/pull/540)
* fix(agent): reapply volatile settings after macOS resume by @LuciusChen in [#506](https://github.com/AprilNEA/OpenLogi/pull/506)
* fix(linux): grant uaccess on Logitech input event nodes by @Xabierland in [#530](https://github.com/AprilNEA/OpenLogi/pull/530)
* fix(agent): rearm control capture after device reconnect by @Phecda in [#450](https://github.com/AprilNEA/OpenLogi/pull/450)
* fix(hidpp): keep events when a field carries an unknown enum value by @bugprone in [#432](https://github.com/AprilNEA/OpenLogi/pull/432)
* refactor: split hub modules to cut merge conflicts by @davidbudnick in [#542](https://github.com/AprilNEA/OpenLogi/pull/542)
* fix(agent): prefer online device for input capture by @Phecda in [#453](https://github.com/AprilNEA/OpenLogi/pull/453)
* fix(agent-core): retry volatile DPI re-apply on cold boot by @iamshakibali in [#449](https://github.com/AprilNEA/OpenLogi/pull/449)
* chore: release v0.6.24 by @aprilnea\[bot] in [#509](https://github.com/AprilNEA/OpenLogi/pull/509)

#### New Contributors [#new-contributors-2]

* @safonin made their first contribution in [#522](https://github.com/AprilNEA/OpenLogi/pull/522)
* @b0x42 made their first contribution in [#363](https://github.com/AprilNEA/OpenLogi/pull/363)
* @kirgene made their first contribution in [#470](https://github.com/AprilNEA/OpenLogi/pull/470)
* @LCozatl made their first contribution in [#479](https://github.com/AprilNEA/OpenLogi/pull/479)
* @Sankew made their first contribution in [#395](https://github.com/AprilNEA/OpenLogi/pull/395)
* @tiaringhio made their first contribution in [#513](https://github.com/AprilNEA/OpenLogi/pull/513)
* @MichaelDanCurtis made their first contribution in [#344](https://github.com/AprilNEA/OpenLogi/pull/344)
* @Abnersouza7 made their first contribution in [#510](https://github.com/AprilNEA/OpenLogi/pull/510)
* @LuciusChen made their first contribution in [#506](https://github.com/AprilNEA/OpenLogi/pull/506)
* @Xabierland made their first contribution in [#530](https://github.com/AprilNEA/OpenLogi/pull/530)
* @bugprone made their first contribution in [#432](https://github.com/AprilNEA/OpenLogi/pull/432)
* @iamshakibali made their first contribution in [#449](https://github.com/AprilNEA/OpenLogi/pull/449)

**Full Changelog**: [v0.6.23...v0.6.24](https://github.com/AprilNEA/OpenLogi/compare/v0.6.23...v0.6.24)

## v0.6.23 [#v0623]

*Released August 3, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.23)

#### What's Changed [#whats-changed-4]

* ci(build): run PR installer builds only with needs: build label by @AprilNEA in [#443](https://github.com/AprilNEA/OpenLogi/pull/443)
* fix(hid): declare Win32\_System\_IO feature for windows-sys WriteFile by @Stanley5249 in [#458](https://github.com/AprilNEA/OpenLogi/pull/458)
* fix(windows): resolve the update check and fix the unreadable Update-failed badge by @davidbudnick in [#437](https://github.com/AprilNEA/OpenLogi/pull/437)
* chore(deps): bump quinn-proto from 0.11.14 to 0.11.16 by @dependabot\[bot] in [#476](https://github.com/AprilNEA/OpenLogi/pull/476)
* fix: allow local dev without devenv by @davidbudnick in [#486](https://github.com/AprilNEA/OpenLogi/pull/486)
* fix(deps): drop vulnerable opentelemetry\_sdk by @davidbudnick in [#487](https://github.com/AprilNEA/OpenLogi/pull/487)
* fix(gui): refresh the macOS device menu when inventory changes by @Phecda in [#456](https://github.com/AprilNEA/OpenLogi/pull/456)
* fix(gui): reject transient direct device identities by @Phecda in [#451](https://github.com/AprilNEA/OpenLogi/pull/451)
* ci: speed up pipelines with shared rust-cache by @davidbudnick in [#488](https://github.com/AprilNEA/OpenLogi/pull/488)
* fix(hook): report Windows pointer motion by differencing the cursor point by @Stanley5249 in [#473](https://github.com/AprilNEA/OpenLogi/pull/473)
* fix(tray): stop the Windows tray launching the CLI instead of the GUI by @Stanley5249 in [#460](https://github.com/AprilNEA/OpenLogi/pull/460)
* fix(hid): stop intermittent SmartShift InvalidArgument by @davidbudnick in [#489](https://github.com/AprilNEA/OpenLogi/pull/489)
* fix(hook): grab only relative pointer devices, never touchpads or pointing sticks by @kesleyfort in [#401](https://github.com/AprilNEA/OpenLogi/pull/401)
* fix: divert thumbwheel for rotation rebinds even without single-tap capability by @tagawa0525 in [#415](https://github.com/AprilNEA/OpenLogi/pull/415)
* chore: release v0.6.23 by @aprilnea\[bot] in [#442](https://github.com/AprilNEA/OpenLogi/pull/442)
* feat(gui): polish device and pointer visuals by @AprilNEA in [#495](https://github.com/AprilNEA/OpenLogi/pull/495)
* refactor(gui): use semantic pointer controls by @AprilNEA in [#496](https://github.com/AprilNEA/OpenLogi/pull/496)
* feat(windows): enable in-app MSI updates via gpui-updater v0.0.6 by @davidbudnick in [#504](https://github.com/AprilNEA/OpenLogi/pull/504)
* fix(hid,gui): stop the device list flapping on transient probe failures by @davidbudnick in [#490](https://github.com/AprilNEA/OpenLogi/pull/490)
* feat(gui): show SmartShift write feedback by @AprilNEA in [#498](https://github.com/AprilNEA/OpenLogi/pull/498)
* fix(i18n): localize offline and pairing states by @AprilNEA in [#499](https://github.com/AprilNEA/OpenLogi/pull/499)
* fix(gui): expose device controls to accessibility by @AprilNEA in [#500](https://github.com/AprilNEA/OpenLogi/pull/500)
* feat(gui): clarify pairing failures and offline editing by @AprilNEA in [#501](https://github.com/AprilNEA/OpenLogi/pull/501)
* fix(gui): keep selected gallery device in view by @AprilNEA in [#502](https://github.com/AprilNEA/OpenLogi/pull/502)
* fix(gui): improve card contrast and depth by @AprilNEA in [#507](https://github.com/AprilNEA/OpenLogi/pull/507)
* ci(i18n): harden Crowdin translation sync by @davidbudnick in [#505](https://github.com/AprilNEA/OpenLogi/pull/505)

#### New Contributors [#new-contributors-3]

* @Stanley5249 made their first contribution in [#458](https://github.com/AprilNEA/OpenLogi/pull/458)
* @dependabot\[bot] made their first contribution in [#476](https://github.com/AprilNEA/OpenLogi/pull/476)
* @Phecda made their first contribution in [#456](https://github.com/AprilNEA/OpenLogi/pull/456)
* @tagawa0525 made their first contribution in [#415](https://github.com/AprilNEA/OpenLogi/pull/415)

**Full Changelog**: [v0.6.22...v0.6.23](https://github.com/AprilNEA/OpenLogi/compare/v0.6.22...v0.6.23)

## v0.6.22 [#v0622]

*Released July 21, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.22)

#### What's Changed [#whats-changed-5]

* fix(gui): keep the post-sync asset repaint until the inventory is Ready by @AprilNEA in [#427](https://github.com/AprilNEA/OpenLogi/pull/427)
* feat(hidpp): add level-gated wire tracing for HID++ requests by @AprilNEA in [#439](https://github.com/AprilNEA/OpenLogi/pull/439)
* refactor(gui): introduce a design-token layer and unify chrome by @AprilNEA in [#440](https://github.com/AprilNEA/OpenLogi/pull/440)
* fix(hid): probe Bolt slots concurrently so slow devices finish enumerating by @AprilNEA in [#438](https://github.com/AprilNEA/OpenLogi/pull/438)
* refactor(gui): introduce a HIG-based typography scale by @AprilNEA in [#441](https://github.com/AprilNEA/OpenLogi/pull/441)
* chore: release v0.6.22 by @aprilnea\[bot] in [#425](https://github.com/AprilNEA/OpenLogi/pull/425)

**Full Changelog**: [v0.6.21...v0.6.22](https://github.com/AprilNEA/OpenLogi/compare/v0.6.21...v0.6.22)

## v0.6.21 [#v0621]

*Released July 19, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.21)

#### What's Changed [#whats-changed-6]

* feat(packaging): add Arch Linux release packages by @cserby in [#265](https://github.com/AprilNEA/OpenLogi/pull/265)
* fix: hotplug-driven inventory and phantom device-card suppression by @davidbudnick in [#346](https://github.com/AprilNEA/OpenLogi/pull/346)
* I18n(gui): Update pl localize by @mariuszkrzaczkowski in [#372](https://github.com/AprilNEA/OpenLogi/pull/372)
* fix(hid): bound the Bolt per-slot probe so one hung device can't drop the whole receiver (#218) by @buliwyf42 in [#251](https://github.com/AprilNEA/OpenLogi/pull/251)
* fix(hid): persist wheel resolution across reconnects by @yuki-yano in [#416](https://github.com/AprilNEA/OpenLogi/pull/416)
* fix: make one-shot enumerate() retry transport-agnostic so Unifying partial drains recover by @mvanhorn in [#287](https://github.com/AprilNEA/OpenLogi/pull/287)
* test(core): add hires\_wheel to the inventory equality test helper by @AprilNEA in [#417](https://github.com/AprilNEA/OpenLogi/pull/417)
* fix(unifying): enable wireless notifications so paired devices enumerate by @laofun in [#309](https://github.com/AprilNEA/OpenLogi/pull/309)
* fix(gui): draw a client-side titlebar on Linux for window controls by @stefan-siebert in [#338](https://github.com/AprilNEA/OpenLogi/pull/338)
* docs: sync READMEs with current platform support by @AprilNEA in [#418](https://github.com/AprilNEA/OpenLogi/pull/418)
* ci(release): build installers on PRs via a reusable build.yml by @AprilNEA in [#423](https://github.com/AprilNEA/OpenLogi/pull/423)
* Windows follow-ups: app icon, working update checks, settings fixes, docs by @davidbudnick in [#358](https://github.com/AprilNEA/OpenLogi/pull/358)
* feat(assets): race Cloudflare and npm mirrors by @AprilNEA in [#424](https://github.com/AprilNEA/OpenLogi/pull/424)
* chore: release v0.6.21 by @aprilnea\[bot] in [#414](https://github.com/AprilNEA/OpenLogi/pull/414)

#### New Contributors [#new-contributors-4]

* @mariuszkrzaczkowski made their first contribution in [#372](https://github.com/AprilNEA/OpenLogi/pull/372)
* @yuki-yano made their first contribution in [#416](https://github.com/AprilNEA/OpenLogi/pull/416)

**Full Changelog**: [v0.6.20...v0.6.21](https://github.com/AprilNEA/OpenLogi/compare/v0.6.20...v0.6.21)

## v0.6.20 [#v0620]

*Released July 18, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.20)

#### What's Changed [#whats-changed-7]

* fix(smartshift): stop runaway free-spin scroll and SmartShift control snap-back by @AprilNEA in [#333](https://github.com/AprilNEA/OpenLogi/pull/333)
* docs: add agent guides (AGENTS.md, CLAUDE.md, path-scoped rules) by @AprilNEA in [#379](https://github.com/AprilNEA/OpenLogi/pull/379)
* fix(gui): avoid RefCell panic when applying theme on OS appearance change by @stefan-siebert in [#337](https://github.com/AprilNEA/OpenLogi/pull/337)
* docs: fold completed FIXDRY audit into a decision log by @AprilNEA in [#352](https://github.com/AprilNEA/OpenLogi/pull/352)
* ci: add cargo-deny (advisories, licenses, bans, sources) by @AprilNEA in [#353](https://github.com/AprilNEA/OpenLogi/pull/353)
* fix(gui): bump gpui/gpui-component pins past the Wayland startup activation rework by @kesleyfort in [#400](https://github.com/AprilNEA/OpenLogi/pull/400)
* fix: embed and sign the openlogi CLI in the macOS app bundle by @mvanhorn in [#362](https://github.com/AprilNEA/OpenLogi/pull/362)
* chore: raise MSRV to 1.96, drop fs4, consolidate workspace deps by @AprilNEA in [#407](https://github.com/AprilNEA/OpenLogi/pull/407)
* test: characterize the hidpp v20 matcher and cover the CLI by @AprilNEA in [#406](https://github.com/AprilNEA/OpenLogi/pull/406)
* refactor(core): split config.rs into settings and device submodules by @AprilNEA in [#408](https://github.com/AprilNEA/OpenLogi/pull/408)
* refactor(gui): split app.rs and drop the stale dead\_code allow by @AprilNEA in [#409](https://github.com/AprilNEA/OpenLogi/pull/409)
* refactor(inject): split inject.rs into per-platform modules by @AprilNEA in [#410](https://github.com/AprilNEA/OpenLogi/pull/410)
* refactor: adopt newly stabilized std APIs across the workspace by @AprilNEA in [#411](https://github.com/AprilNEA/OpenLogi/pull/411)
* fix(hook): break the macOS run loop on a stop flag, not CFRunLoopStop alone by @AprilNEA in [#263](https://github.com/AprilNEA/OpenLogi/pull/263)
* refactor: land the standards-audit findings in batches by @AprilNEA in [#381](https://github.com/AprilNEA/OpenLogi/pull/381)
* feat: CGEvent tap diagnostics — conflict detection + live event monitor by @AprilNEA in [#276](https://github.com/AprilNEA/OpenLogi/pull/276)
* chore: release v0.6.20 by @aprilnea\[bot] in [#378](https://github.com/AprilNEA/OpenLogi/pull/378)

#### New Contributors [#new-contributors-5]

* @stefan-siebert made their first contribution in [#337](https://github.com/AprilNEA/OpenLogi/pull/337)
* @kesleyfort made their first contribution in [#400](https://github.com/AprilNEA/OpenLogi/pull/400)

**Full Changelog**: [v0.6.19...v0.6.20](https://github.com/AprilNEA/OpenLogi/compare/v0.6.19...v0.6.20)

## v0.6.19 [#v0619]

*Released July 6, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.19)

#### What's Changed [#whats-changed-8]

* feat(windows): bundle the background agent and add a tray icon (#347) by @davidbudnick in [#350](https://github.com/AprilNEA/OpenLogi/pull/350)
* chore: release v0.6.19 by @aprilnea\[bot] in [#332](https://github.com/AprilNEA/OpenLogi/pull/332)

**Full Changelog**: [v0.6.18...v0.6.19](https://github.com/AprilNEA/OpenLogi/compare/v0.6.18...v0.6.19)

## v0.6.18 [#v0618]

*Released June 29, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.18)

#### What's Changed [#whats-changed-9]

* fix: set app\_id on GUI windows so GNOME Wayland matches the .desktop icon and decorations by @mvanhorn in [#320](https://github.com/AprilNEA/OpenLogi/pull/320)
* Clarify MX Master 4 gesture control semantics by @AprilNEA in [#325](https://github.com/AprilNEA/OpenLogi/pull/325)
* Complete typed HID++ feature wrappers by @AprilNEA in [#326](https://github.com/AprilNEA/OpenLogi/pull/326)
* fix(hidpp): correct UnifiedBattery (0x1004) charging status codes by @PeronGH in [#330](https://github.com/AprilNEA/OpenLogi/pull/330)
* feat(gui): consolidate Settings and add theme switching by @AprilNEA in [#331](https://github.com/AprilNEA/OpenLogi/pull/331)
* chore: release v0.6.18 by @aprilnea\[bot] in [#324](https://github.com/AprilNEA/OpenLogi/pull/324)

#### New Contributors [#new-contributors-6]

* @PeronGH made their first contribution in [#330](https://github.com/AprilNEA/OpenLogi/pull/330)

**Full Changelog**: [v0.6.17...v0.6.18](https://github.com/AprilNEA/OpenLogi/compare/v0.6.17...v0.6.18)

## v0.6.17 [#v0617]

*Released June 27, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.17)

#### What's Changed [#whats-changed-10]

* feat: add Capture Region to Clipboard button action by @mvanhorn in [#296](https://github.com/AprilNEA/OpenLogi/pull/296)
* feat(gui): show connection-type icon on device cards by @laofun in [#310](https://github.com/AprilNEA/OpenLogi/pull/310)
* chore: release v0.6.17 by @aprilnea\[bot] in [#311](https://github.com/AprilNEA/OpenLogi/pull/311)

**Full Changelog**: [v0.6.16...v0.6.17](https://github.com/AprilNEA/OpenLogi/compare/v0.6.16...v0.6.17)

## v0.6.16 [#v0616]

*Released June 22, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.16)

#### What's Changed [#whats-changed-11]

* fix(hidpp): prune abandoned event receivers by @AprilNEA in [#297](https://github.com/AprilNEA/OpenLogi/pull/297)
* fix(pairing): keep session pause balanced by @AprilNEA in [#298](https://github.com/AprilNEA/OpenLogi/pull/298)
* fix(hidpp): guard message listener lifetimes by @AprilNEA in [#299](https://github.com/AprilNEA/OpenLogi/pull/299)
* feat(ipc): poll agent snapshots by @AprilNEA in [#300](https://github.com/AprilNEA/OpenLogi/pull/300)
* refactor(agent): arbitrate receiver access with leases by @AprilNEA in [#302](https://github.com/AprilNEA/OpenLogi/pull/302)
* refactor(ipc): acknowledge pairing commands by @AprilNEA in [#304](https://github.com/AprilNEA/OpenLogi/pull/304)
* chore: release v0.6.16 by @aprilnea\[bot] in [#301](https://github.com/AprilNEA/OpenLogi/pull/301)

**Full Changelog**: [v0.6.15...v0.6.16](https://github.com/AprilNEA/OpenLogi/compare/v0.6.15...v0.6.16)

## v0.6.15 [#v0615]

*Released June 21, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.15)

#### What's Changed [#whats-changed-12]

* fix(release): re-register PSGallery before Windows Artifact Signing by @AprilNEA in [#264](https://github.com/AprilNEA/OpenLogi/pull/264)
* i18n: localize the About window update section + Crowdin sync by @AprilNEA in [#275](https://github.com/AprilNEA/OpenLogi/pull/275)
* fix(gui): fit keyboard image and render glow live by @davidbudnick in [#283](https://github.com/AprilNEA/OpenLogi/pull/283)
* feat(scroll): per-device inverted scrolling by @AprilNEA in [#294](https://github.com/AprilNEA/OpenLogi/pull/294)
* chore: release v0.6.15 by @aprilnea\[bot] in [#273](https://github.com/AprilNEA/OpenLogi/pull/273)

**Full Changelog**: [v0.6.14...v0.6.15](https://github.com/AprilNEA/OpenLogi/compare/v0.6.14...v0.6.15)

## v0.6.14 [#v0614]

*Released June 15, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.14)

#### What's Changed [#whats-changed-13]

* refactor(core): extract the OS input-injection layer into openlogi-inject by @AprilNEA in [#240](https://github.com/AprilNEA/OpenLogi/pull/240)
* fix(hid): solid keyboard colour via 0x8070 effect by @davidbudnick in [#205](https://github.com/AprilNEA/OpenLogi/pull/205)
* chore(nix): remove the self-hosted flake package by @AprilNEA in [#262](https://github.com/AprilNEA/OpenLogi/pull/262)
* chore: release v0.6.14 by @aprilnea\[bot] in [#261](https://github.com/AprilNEA/OpenLogi/pull/261)

**Full Changelog**: [v0.6.13...v0.6.14](https://github.com/AprilNEA/OpenLogi/compare/v0.6.13...v0.6.14)

## v0.6.13 [#v0613]

*Released June 15, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.13)

#### What's Changed [#whats-changed-14]

* fix(agent): respawn the capture session after a setup failure by @tfohlmeister in [#225](https://github.com/AprilNEA/OpenLogi/pull/225)
* chore: release v0.6.12 by @aprilnea\[bot] in [#250](https://github.com/AprilNEA/OpenLogi/pull/250)
* fix(hid): retry the one-shot enumerate() through a transient probe miss (#218) by @buliwyf42 in [#237](https://github.com/AprilNEA/OpenLogi/pull/237)
* fix(assets): verify a downloaded asset before it reaches the cache by @AprilNEA in [#243](https://github.com/AprilNEA/OpenLogi/pull/243)
* refactor(hook,hid): document the Windows FFI unsafe blocks by @AprilNEA in [#242](https://github.com/AprilNEA/OpenLogi/pull/242)
* fix(gui): localize the DPI panel's status strings by @AprilNEA in [#241](https://github.com/AprilNEA/OpenLogi/pull/241)
* refactor(hidpp): dedup the feature layer (registry data-macro + FeatureEndpoint) by @AprilNEA in [#238](https://github.com/AprilNEA/OpenLogi/pull/238)
* fix(gui): launch the agent helper via LaunchServices for a stable TCC identity by @davidbudnick in [#207](https://github.com/AprilNEA/OpenLogi/pull/207)
* refactor(gui): fold duplicated DPI/SmartShift code into shared abstractions by @AprilNEA in [#239](https://github.com/AprilNEA/OpenLogi/pull/239)
* fix(gui): give the bundled agent the OpenLogi app icon by @AprilNEA in [#260](https://github.com/AprilNEA/OpenLogi/pull/260)
* chore: release v0.6.13 by @aprilnea\[bot] in [#259](https://github.com/AprilNEA/OpenLogi/pull/259)

#### New Contributors [#new-contributors-7]

* @tfohlmeister made their first contribution in [#225](https://github.com/AprilNEA/OpenLogi/pull/225)
* @buliwyf42 made their first contribution in [#237](https://github.com/AprilNEA/OpenLogi/pull/237)

**Full Changelog**: [v0.6.12...v0.6.13](https://github.com/AprilNEA/OpenLogi/compare/v0.6.12...v0.6.13)

## v0.6.12 [#v0612]

*Released June 13, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.12)

#### What's Changed [#whats-changed-15]

* fix(agent): persist DPI/SmartShift per device and reapply volatile settings on reconnect by @AprilNEA in [#223](https://github.com/AprilNEA/OpenLogi/pull/223)
* fix(gui): keep asleep devices and their panels in the device list by @AprilNEA in [#224](https://github.com/AprilNEA/OpenLogi/pull/224)
* fix(updater): bump gpui-updater to v0.0.5 (relaunch as GUI, not in Terminal) by @AprilNEA in [#249](https://github.com/AprilNEA/OpenLogi/pull/249)
* chore: release v0.6.12 by @aprilnea\[bot] in [#246](https://github.com/AprilNEA/OpenLogi/pull/246)

**Full Changelog**: [v0.6.11...v0.6.12](https://github.com/AprilNEA/OpenLogi/compare/v0.6.11...v0.6.12)

## v0.6.11 [#v0611]

*Released June 13, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.11)

#### What's Changed [#whats-changed-16]

* fix(hid): replay a node's last inventory through transient probe failures by @AprilNEA in [#222](https://github.com/AprilNEA/OpenLogi/pull/222)
* fix(release): install the GUI's x11/wayland deps so Linux packaging links by @AprilNEA in [#244](https://github.com/AprilNEA/OpenLogi/pull/244)
* chore: release v0.6.11 by @aprilnea\[bot] in [#236](https://github.com/AprilNEA/OpenLogi/pull/236)

**Full Changelog**: [v0.6.10...v0.6.11](https://github.com/AprilNEA/OpenLogi/compare/v0.6.10...v0.6.11)

## v0.6.7 [#v067]

*Released June 12, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.7)

#### What's Changed [#whats-changed-17]

* feat(release): Windows arm64 build + static CRT by @AprilNEA in [#208](https://github.com/AprilNEA/OpenLogi/pull/208)
* fix: stop flashing the permission gate and empty state at startup by @AprilNEA in [#212](https://github.com/AprilNEA/OpenLogi/pull/212)
* feat(release): per-arch MSI installers + drop the GUI qualifier from Windows jobs by @AprilNEA in [#211](https://github.com/AprilNEA/OpenLogi/pull/211)
* fix: don't flash the empty-state screen while the agent is still scanning by @AprilNEA in [#213](https://github.com/AprilNEA/OpenLogi/pull/213)
* fix(ipc): harden the agent-GUI link end to end (review follow-ups for #212/#213) by @AprilNEA in [#215](https://github.com/AprilNEA/OpenLogi/pull/215)
* Fix macOS volume and media key posting by @jonstuebe in [#184](https://github.com/AprilNEA/OpenLogi/pull/184)
* fix(release): derive macOS bundle versions from the workspace by @AprilNEA in [#217](https://github.com/AprilNEA/OpenLogi/pull/217)
* chore: release v0.6.7 by @aprilnea\[bot] in [#210](https://github.com/AprilNEA/OpenLogi/pull/210)

#### New Contributors [#new-contributors-8]

* @jonstuebe made their first contribution in [#184](https://github.com/AprilNEA/OpenLogi/pull/184)

**Full Changelog**: [v0.6.6...v0.6.7](https://github.com/AprilNEA/OpenLogi/compare/v0.6.6...v0.6.7)

## v0.6.6 [#v066]

*Released June 11, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.6)

#### What's Changed [#whats-changed-18]

* ci: fix SignTool timestamp URL and add a signing dry-run workflow by @AprilNEA in [#202](https://github.com/AprilNEA/OpenLogi/pull/202)
* fix(hidpp): bound device-controlled name lengths in Bolt parsing by @AprilNEA in [#200](https://github.com/AprilNEA/OpenLogi/pull/200)
* fix(assets): keep sync writes inside the cache root and verified by @AprilNEA in [#201](https://github.com/AprilNEA/OpenLogi/pull/201)
* feat(windows): ship the signed GUI as the Windows release artifact by @AprilNEA in [#204](https://github.com/AprilNEA/OpenLogi/pull/204)
* chore: release v0.6.6 by @aprilnea\[bot] in [#203](https://github.com/AprilNEA/OpenLogi/pull/203)

**Full Changelog**: [v0.6.5...v0.6.6](https://github.com/AprilNEA/OpenLogi/compare/v0.6.5...v0.6.6)

## v0.6.5 [#v065]

*Released June 11, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.5)

#### What's Changed [#whats-changed-19]

* ci(release): harden the Windows signing and publish pipeline by @AprilNEA in [#198](https://github.com/AprilNEA/OpenLogi/pull/198)
* style: collapse nested ifs flagged by current stable clippy by @AprilNEA in [#197](https://github.com/AprilNEA/OpenLogi/pull/197)
* chore: release v0.6.5 by @aprilnea\[bot] in [#199](https://github.com/AprilNEA/OpenLogi/pull/199)

**Full Changelog**: [v0.6.4...v0.6.5](https://github.com/AprilNEA/OpenLogi/compare/v0.6.4...v0.6.5)

## v0.6.4 [#v064]

*Released June 11, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.4)

#### What's Changed [#whats-changed-20]

* fix(gui): stop sensitivity Default label clipping by @davidbudnick in [#186](https://github.com/AprilNEA/OpenLogi/pull/186)
* Minor: glow keyboard card in lighting colour by @davidbudnick in [#185](https://github.com/AprilNEA/OpenLogi/pull/185)
* feat(openlogi-gui): expand UI to 19 fully-translated locales by @davidbudnick in [#24](https://github.com/AprilNEA/OpenLogi/pull/24)
* refactor: unify device-kind precedence docs + drop the redundant caps carry-forward by @AprilNEA in [#194](https://github.com/AprilNEA/OpenLogi/pull/194)
* fix(hid): refresh the volatile battery every tick without the feature-table walk by @AprilNEA in [#193](https://github.com/AprilNEA/OpenLogi/pull/193)
* feat(windows): port the headless agent to Windows by @AprilNEA in [#167](https://github.com/AprilNEA/OpenLogi/pull/167)
* \[codex] Update time to fix RustSec advisory by @Nicolas0315 in [#128](https://github.com/AprilNEA/OpenLogi/pull/128)
* ci(release): sign Windows CLI build with Azure Artifact Signing by @AprilNEA in [#196](https://github.com/AprilNEA/OpenLogi/pull/196)
* chore: release v0.6.4 by @aprilnea\[bot] in [#190](https://github.com/AprilNEA/OpenLogi/pull/190)

#### New Contributors [#new-contributors-9]

* @Nicolas0315 made their first contribution in [#128](https://github.com/AprilNEA/OpenLogi/pull/128)

**Full Changelog**: [v0.6.3...v0.6.4](https://github.com/AprilNEA/OpenLogi/compare/v0.6.3...v0.6.4)

## v0.6.3 [#v063]

*Released June 10, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.3)

#### What's Changed [#whats-changed-21]

* feat: unified Binding model + gesture-button menu (gesture uplift Phase A) by @AprilNEA in [#187](https://github.com/AprilNEA/OpenLogi/pull/187)
* feat: gesture buttons on Middle/Back/Forward via the OS hook (Phase B) by @AprilNEA in [#188](https://github.com/AprilNEA/OpenLogi/pull/188)
* chore: release v0.6.3 by @aprilnea\[bot] in [#183](https://github.com/AprilNEA/OpenLogi/pull/183)

**Full Changelog**: [v0.6.2...v0.6.3](https://github.com/AprilNEA/OpenLogi/compare/v0.6.2...v0.6.3)

## v0.6.2 [#v062]

*Released June 8, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.2)

#### What's Changed [#whats-changed-22]

* chore: release v0.6.1 by @aprilnea\[bot] in [#175](https://github.com/AprilNEA/OpenLogi/pull/175)
* Integrate Crowdin localization workflow by @AprilNEA in [#174](https://github.com/AprilNEA/OpenLogi/pull/174)
* ci: switch release notes generation to Codex by @AprilNEA in [#177](https://github.com/AprilNEA/OpenLogi/pull/177)
* feat(tray): route menu actions to GUI via openlogi:// URL scheme by @AprilNEA in [#176](https://github.com/AprilNEA/OpenLogi/pull/176)
* chore: release v0.6.2 by @aprilnea\[bot] in [#178](https://github.com/AprilNEA/OpenLogi/pull/178)

**Full Changelog**: [v0.6.1...v0.6.2](https://github.com/AprilNEA/OpenLogi/compare/v0.6.1...v0.6.2)

## v0.6.1 [#v061]

*Released June 8, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.1)

#### What's Changed [#whats-changed-23]

* docs: Linux device permissions (udev rule for Bolt + Bluetooth-direct) by @recchia in [#149](https://github.com/AprilNEA/OpenLogi/pull/149)
* Fix Cmd+W window close handling by @AprilNEA in [#168](https://github.com/AprilNEA/OpenLogi/pull/168)
* fix(cli): diag selects a device that exposes the feature under test by @recchia in [#150](https://github.com/AprilNEA/OpenLogi/pull/150)
* Fix auxiliary window actions by @AprilNEA in [#170](https://github.com/AprilNEA/OpenLogi/pull/170)
* ci: generate release notes with Amp by @AprilNEA in [#171](https://github.com/AprilNEA/OpenLogi/pull/171)
* chore: release v0.6.1 by @aprilnea\[bot] in [#169](https://github.com/AprilNEA/OpenLogi/pull/169)

#### New Contributors [#new-contributors-10]

* @recchia made their first contribution in [#149](https://github.com/AprilNEA/OpenLogi/pull/149)

**Full Changelog**: [v0.6.0...v0.6.1](https://github.com/AprilNEA/OpenLogi/compare/v0.6.0...v0.6.1)

## v0.6.0 [#v060]

*Released June 8, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.6.0)

#### Highlights [#highlights]

OpenLogi 0.6.0 splits the app into a headless, always-on agent daemon and an on-demand GUI configuration editor. This makes remapping and device I/O keep running after the GUI window is closed, while the GUI becomes a normal app that opens only when you need to change settings.

#### What's new [#whats-new]

* Added `openlogi-agent`, a headless daemon that owns HID++ device I/O, the CGEventTap hook, foreground-app tracking, pairing, and the menu-bar status item.
* Reworked `openlogi-gui` into an on-demand configuration app that talks to the agent over IPC instead of running device I/O itself.
* Added a tarpc-based agent IPC protocol for inventory/status polling, DPI, lighting, SmartShift, config reloads, Accessibility prompts, and pairing.
* Moved device pairing into the agent so receiver pairing no longer competes with the live gesture/capture session from another process.
* The GUI can auto-launch the embedded agent helper when the IPC socket is unavailable.

#### macOS packaging and startup [#macos-packaging-and-startup]

* The packaged app now embeds `OpenLogiAgent.app` as a nested login-item helper under `OpenLogi.app/Contents/Library/LoginItems/`.
* Launch-at-login is now handled by the agent instead of the GUI.
* The menu-bar item is hosted by the agent, so remapping and tray access can stay alive independently of the GUI window.
* Release signing now signs the nested agent helper inside-out before signing the outer app, helping the agent keep a stable Accessibility/TCC identity across updates.
* Homebrew tap update dispatch for `openlogi@latest` has been re-enabled.

#### Fixes and hardening [#fixes-and-hardening]

* Fixed an agent single-instance race that could start two agents and install duplicate hooks.
* Fixed Accessibility prompting so macOS authorizes the agent binary, not the GUI.
* Fixed transient inventory errors wiping live runtime state or resetting DPI-cycle state.
* Fixed agent recovery when the GUI sees a dead or quit agent.
* Fixed LaunchAgent plist escaping and made tray **Quit** stay quit after a clean exit.
* Preserved typed device errors across IPC so unsupported DPI/SmartShift features latch correctly instead of retrying forever.
* Kept the agent and GUI device ordering consistent so the default selected device matches in both processes.
* Confirmed SmartShift writes with a re-read so rejected writes self-correct in the UI.
* Honored the Show-in-menu-bar preference at agent startup.
* Added IPC protocol handshake checks and pairing-session race fixes from review.

#### Upgrade notes [#upgrade-notes]

* On first launch after upgrading, macOS may ask you to grant Accessibility permission to `OpenLogiAgent`. This is expected: the agent is now the process that owns the input hook.
* If remapping does not work after upgrading, check System Settings → Privacy & Security → Accessibility and ensure the agent/helper is allowed, then restart OpenLogi.

#### Included PRs [#included-prs]

* \#165 — Split into a headless agent daemon + on-demand GUI
* \#161 — Dispatch `openlogi@latest` Homebrew tap updates
* \#164 — Release v0.6.0

**Full changelog**: [v0.5.3...v0.6.0](https://github.com/AprilNEA/OpenLogi/compare/v0.5.3...v0.6.0)

## v0.5.3 [#v053]

*Released June 6, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.5.3)

#### What's Changed [#whats-changed-24]

* fix: gate device config panels on HID++ capabilities (closes #127) by @AprilNEA in [#147](https://github.com/AprilNEA/OpenLogi/pull/147)
* chore: release v0.5.3 by @aprilnea\[bot] in [#157](https://github.com/AprilNEA/OpenLogi/pull/157)

**Full Changelog**: [v0.5.2...v0.5.3](https://github.com/AprilNEA/OpenLogi/compare/v0.5.2...v0.5.3)

## v0.5.2 [#v052]

*Released June 5, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.5.2)

#### What's Changed [#whats-changed-25]

* fix(gui): retry asset sync on failure with backoff instead of latching once by @AprilNEA in [#140](https://github.com/AprilNEA/OpenLogi/pull/140)
* refactor: replace hand-rolled code with std/num\_enum equivalents by @AprilNEA in [#141](https://github.com/AprilNEA/OpenLogi/pull/141)
* feat: enable Thumb Wheel Up/Down mapping, "Do Nothing" action, and native scroll sensitivity  by @Miguel-sdj in [#125](https://github.com/AprilNEA/OpenLogi/pull/125)
* feat(hook): implement Linux evdev+uinput mouse hook by @cserby in [#119](https://github.com/AprilNEA/OpenLogi/pull/119)
* feat(core): implement Action::execute on Linux via uinput by @cserby in [#120](https://github.com/AprilNEA/OpenLogi/pull/120)
* feat: SmartShift 0x2110 support (MX Master 2S) + diag smartshift --sensitivity by @laofun in [#77](https://github.com/AprilNEA/OpenLogi/pull/77)
* feat(hook): implement frontmost\_bundle\_id on Linux via X11 by @cserby in [#122](https://github.com/AprilNEA/OpenLogi/pull/122)
* feat(i18n): add italian language by @elax46 in [#63](https://github.com/AprilNEA/OpenLogi/pull/63)
* feat(core): LockScreen and media actions via D-Bus on Linux by @cserby in [#124](https://github.com/AprilNEA/OpenLogi/pull/124)
* ci: run clippy on Windows instead of bare cargo check by @AprilNEA in [#146](https://github.com/AprilNEA/OpenLogi/pull/146)
* chore: release v0.5.2 by @aprilnea\[bot] in [#139](https://github.com/AprilNEA/OpenLogi/pull/139)

#### New Contributors [#new-contributors-11]

* @Miguel-sdj made their first contribution in [#125](https://github.com/AprilNEA/OpenLogi/pull/125)
* @cserby made their first contribution in [#119](https://github.com/AprilNEA/OpenLogi/pull/119)
* @laofun made their first contribution in [#77](https://github.com/AprilNEA/OpenLogi/pull/77)
* @elax46 made their first contribution in [#63](https://github.com/AprilNEA/OpenLogi/pull/63)

**Full Changelog**: [v0.5.1...v0.5.2](https://github.com/AprilNEA/OpenLogi/compare/v0.5.1...v0.5.2)

## v0.5.1 [#v051]

*Released June 5, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.5.1)

#### What's Changed [#whats-changed-26]

* ci(release-plz): drop the unneeded stale-branch prune step by @AprilNEA in [#135](https://github.com/AprilNEA/OpenLogi/pull/135)
* fix(assets): match devices against every model id a depot lists by @AprilNEA in [#137](https://github.com/AprilNEA/OpenLogi/pull/137)
* feat(gui): SmartShift settings panel (wheel mode, sensitivity, permanent ratchet) by @AprilNEA in [#138](https://github.com/AprilNEA/OpenLogi/pull/138)
* chore: release v0.5.1 by @aprilnea\[bot] in [#136](https://github.com/AprilNEA/OpenLogi/pull/136)

**Full Changelog**: [v0.5.0...v0.5.1](https://github.com/AprilNEA/OpenLogi/compare/v0.5.0...v0.5.1)

## v0.5.0 [#v050]

*Released June 5, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.5.0)

#### What's Changed [#whats-changed-27]

* feat: add wired G-series keyboard RGB control by @davidbudnick in [#29](https://github.com/AprilNEA/OpenLogi/pull/29)
* fix(release-plz): restore unified-versioning config by @AprilNEA in [#110](https://github.com/AprilNEA/OpenLogi/pull/110)
* fix(gui): stop idle CPU drain from the connected status dot by @davidbudnick in [#97](https://github.com/AprilNEA/OpenLogi/pull/97)
* chore(release-plz): disable cargo-semver-checks by @AprilNEA in [#113](https://github.com/AprilNEA/OpenLogi/pull/113)
* docs: add ja/de/fr/ko READMEs and fix the relocated zh-CN one by @AprilNEA in [#112](https://github.com/AprilNEA/OpenLogi/pull/112)
* feat(gui): swap in the new app icon by @AprilNEA in [#129](https://github.com/AprilNEA/OpenLogi/pull/129)
* docs: use the new app icon as the README logo by @AprilNEA in [#132](https://github.com/AprilNEA/OpenLogi/pull/132)
* Migrate macOS ObjC FFI to objc2, fixing the menu-bar leak (#99) by @AprilNEA in [#131](https://github.com/AprilNEA/OpenLogi/pull/131)
* feat(updater): signed auto-update with fail-closed verification by @AprilNEA in [#130](https://github.com/AprilNEA/OpenLogi/pull/130)
* chore: release v0.5.0 by @aprilnea\[bot] in [#111](https://github.com/AprilNEA/OpenLogi/pull/111)

**Full Changelog**: [v0.4.1...v0.5.0](https://github.com/AprilNEA/OpenLogi/compare/v0.4.1...v0.5.0)

## v0.4.1 [#v041]

*Released June 3, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.4.1)

#### What's Changed [#whats-changed-28]

* chore: add GitHub issue form templates by @AprilNEA in [#102](https://github.com/AprilNEA/OpenLogi/pull/102)
* ci(release): disable homebrew-tap dispatch (openlogi moved to homebrew-cask) by @AprilNEA in [#105](https://github.com/AprilNEA/OpenLogi/pull/105)
* feat(nix): add nixpkgs package + flake; commit the prebuilt app icon by @AprilNEA in [#106](https://github.com/AprilNEA/OpenLogi/pull/106)
* feat(gui): add device gallery navigation by @AprilNEA in [#107](https://github.com/AprilNEA/OpenLogi/pull/107)
* chore: release v0.4.1 by @aprilnea\[bot] in [#91](https://github.com/AprilNEA/OpenLogi/pull/91)

**Full Changelog**: [v0.4.0...v0.4.1](https://github.com/AprilNEA/OpenLogi/compare/v0.4.0...v0.4.1)

## v0.4.0 [#v040]

*Released June 3, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.4.0)

#### What's Changed [#whats-changed-29]

* feat(i18n): add zh-TW (Traditional Chinese, Taiwan) locale by @PeterDaveHello in [#57](https://github.com/AprilNEA/OpenLogi/pull/57)
* feat(assets): accept both depot filename schemas in the GUI resolver by @AprilNEA in [#76](https://github.com/AprilNEA/OpenLogi/pull/76)
* fix(gui): keep Settings Language description on one line by @davidbudnick in [#72](https://github.com/AprilNEA/OpenLogi/pull/72)
* Polish empty state UI and improve user hints for UX/usability by @markosnarinian in [#88](https://github.com/AprilNEA/OpenLogi/pull/88)
* feat(core): add macos desktop switch actions by @LouisDISPA in [#67](https://github.com/AprilNEA/OpenLogi/pull/67)
* chore: release v0.4.0 by @aprilnea\[bot] in [#75](https://github.com/AprilNEA/OpenLogi/pull/75)

#### New Contributors [#new-contributors-12]

* @PeterDaveHello made their first contribution in [#57](https://github.com/AprilNEA/OpenLogi/pull/57)
* @markosnarinian made their first contribution in [#88](https://github.com/AprilNEA/OpenLogi/pull/88)
* @LouisDISPA made their first contribution in [#67](https://github.com/AprilNEA/OpenLogi/pull/67)

**Full Changelog**: [v0.3.4...v0.4.0](https://github.com/AprilNEA/OpenLogi/compare/v0.3.4...v0.4.0)

## v0.3.4 [#v034]

*Released June 2, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.3.4)

#### What's Changed [#whats-changed-30]

* chore(release): stop publishing the redundant generic -macos.dmg by @AprilNEA in [#58](https://github.com/AprilNEA/OpenLogi/pull/58)
* refactor(xtask): move macOS packaging tasks by @AprilNEA in [#47](https://github.com/AprilNEA/OpenLogi/pull/47)
* Vendor hidpp fork; BLE-direct + startup robustness; Settings permissions by @AprilNEA in [#45](https://github.com/AprilNEA/OpenLogi/pull/45)
* chore: release v0.3.4 by @aprilnea\[bot] in [#59](https://github.com/AprilNEA/OpenLogi/pull/59)

**Full Changelog**: [v0.3.3...v0.3.4](https://github.com/AprilNEA/OpenLogi/compare/v0.3.3...v0.3.4)

## v0.3.3 [#v033]

*Released June 2, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.3.3)

#### What's Changed [#whats-changed-31]

* chore: release v0.3.3 by @aprilnea\[bot] in [#56](https://github.com/AprilNEA/OpenLogi/pull/56)

**Full Changelog**: [v0.3.2...v0.3.3](https://github.com/AprilNEA/OpenLogi/compare/v0.3.2...v0.3.3)

## v0.3.2 [#v032]

*Released June 1, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.3.2)

#### What's Changed [#whats-changed-32]

* fix(release): let softprops own the draft-first GitHub Release by @AprilNEA in [#55](https://github.com/AprilNEA/OpenLogi/pull/55)
* chore: release v0.3.2 by @aprilnea\[bot] in [#49](https://github.com/AprilNEA/OpenLogi/pull/49)

**Full Changelog**: [v0.3.1...v0.3.2](https://github.com/AprilNEA/OpenLogi/compare/v0.3.1...v0.3.2)

## v0.3.1 [#v031]

*Released June 1, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.3.1)

##### Added [#added]

* *(updater)* use static R2 manifest ([#43](https://github.com/AprilNEA/OpenLogi/pull/43))

## v0.3.0 [#v030]

*Released June 1, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.3.0)

##### Added [#added-1]

* *(openlogi-gui)* add Russian localization and language select ([#38](https://github.com/AprilNEA/OpenLogi/pull/38))

##### Fixed [#fixed]

* *(gui)* stabilize device tab ordering ([#37](https://github.com/AprilNEA/OpenLogi/pull/37))

##### Known issue [#known-issue]

The macOS DMG assets for this release are unavailable. The release workflow published `v0.3.0` before uploading the generated assets, and GitHub marked the release immutable, so the DMG and checksum files cannot be attached afterward. This is a release packaging issue, not an application code issue; the workflow has been changed to keep future releases as drafts until all assets are uploaded.

## v0.2.0 [#v020]

*Released June 1, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.2.0)

OpenLogi **0.2.0** rolls up everything since `0.1.0`: directly-attached (wired & Bluetooth) mice are now first-class, plus a macOS menu-bar app, in-app updates, and DMG / Intel distribution. macOS only for now — Linux and Windows are on the roadmap.

#### ✨ Highlights [#-highlights]

##### Wired & Bluetooth-direct devices are now first-class [#wired--bluetooth-direct-devices-are-now-first-class]

Every control path used to assume a mouse hanging off a **Logi Bolt receiver**. Devices connected by **USB cable** or **Bluetooth-direct** (no receiver) are now fully supported — DPI, SmartShift, and button / gesture capture all route to them over HID++. Bluetooth devices are no longer read-only, and wired mice with no battery (e.g. a corded **G502**) are no longer dropped. ([#5](https://github.com/AprilNEA/OpenLogi/pull/5))

##### macOS menu-bar app [#macos-menu-bar-app]

* OpenLogi now runs as a **menu-bar (tray) accessory** — click the 🖱️ icon for live device status and quick **Open** / **Quit**. ([#4](https://github.com/AprilNEA/OpenLogi/pull/4))
* **Dynamic activation policy**: opening a window brings back the Dock icon and app menu; closing the last window drops back to tray-only. ([#7](https://github.com/AprilNEA/OpenLogi/pull/7))
* **Start-minimized autostart** — a login-launched instance comes up in the tray with no window. ([#7](https://github.com/AprilNEA/OpenLogi/pull/7))
* A &#x2A;*"Show in menu bar"*&#x2A; setting, and **⌘W** now closes the focused window (Settings / About too, not just the main one).

##### Gesture button [#gesture-button]

* The gesture button is now a **mappable hotspot** in the mouse diagram, with a hold-time gate so a quick tap registers as a click and a deliberate hold as a swipe. ([#4](https://github.com/AprilNEA/OpenLogi/pull/4))

##### In-app updates (opt-in, off by default) [#in-app-updates-opt-in-off-by-default]

* **Check for Updates** with a first-run prompt, an on-launch check, a clickable version, and live **download progress** — powered by `gpui-updater`.

##### Packaging & distribution [#packaging--distribution]

* Ships as a **DMG installer** with a hosted background image.
* CI now also builds **Intel (x86\_64) macOS** binaries alongside Apple Silicon.

##### UI polish [#ui-polish]

* Demo device cards replaced with a proper **empty state**, the app logo embedded as a runtime asset, and Settings language-picker layout fixes.

#### 🔧 Under the hood [#-under-the-hood]

* HID++ device addressing unified behind a single `DeviceRoute` — Bolt `\{receiver, slot\}` vs. `Direct \{vid, pid\}` resolved in one place; `DpiTarget` / `GestureTarget` collapse into it, and the CLI `diag` commands work on direct devices too. ([#5](https://github.com/AprilNEA/OpenLogi/pull/5))
* macOS status-item wrapper extracted and the menu bar gated to macOS. ([#6](https://github.com/AprilNEA/OpenLogi/pull/6))
* Release / CI hardening: a single root changelog, 1Password-sourced tokens, DMG build inside the devenv, and a real-Xcode Metal env for GUI builds.

**Full changelog**: [v0.1.0...v0.2.0](https://github.com/AprilNEA/OpenLogi/compare/v0.1.0...v0.2.0)

## v0.1.4 [#v014]

*Released May 31, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.1.4)

##### Other [#other]

* update workflow actions for Node 24
* *(release-plz)* fail loudly when a release silently stalls

## v0.1.3 [#v013]

*Released May 31, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.1.3)

*No release notes.*

## v0.1.2 [#v012]

*Released May 31, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.1.2)

##### Added [#added-2]

* Check for Updates in the About window, backed by the gpui-updater crate
* One opt-in update check on launch, with a first-run prompt to enable it
* Live download progress, and a clickable version that links to its GitHub release

## v0.1.1 [#v011]

*Released May 31, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.1.1)

##### Other [#other-1]

* *(release-plz)* write a single root changelog, not one per crate
* *(release-plz)* load CARGO\_REGISTRY\_TOKEN from 1Password

## v0.1.0 [#v010]

*Released May 30, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.1.0)

> ⚠️ &#x2A;*Only the MX Master 4 has been tested on real hardware so far.** Other Logitech devices expose different HID++ controls and are unverified — please report what works and what doesn't.

**OpenLogi 0.1.0** — A native, local-first alternative to Logitech Options+, written in Rust.

##### Remapping [#remapping]

* Remap the **Back / Forward / Middle** buttons (via a CGEventTap hook) to any action.
* Capture the MX **gesture button** over HID++ (`0x1b04` raw‑XY): directional swipes (up / down / left / right) fire **mid‑swipe**, plus a no‑swipe click — bound independently per direction.
* Capture the **DPI / ModeShift button** and the **thumb wheel** (`0x2150`) over HID++.

##### Actions [#actions]

* Keyboard shortcuts, mouse clicks, scrolling, media keys, tab navigation.
* **DPI** cycle / presets and **SmartShift** toggle, written to the device over HID++ (reusing the open capture channel for low latency).
* Window & space actions — **Mission Control, App Exposé, Show Desktop, Launchpad** — triggered directly through the Dock.

##### Devices (HID++ over a Bolt receiver) [#devices-hid-over-a-bolt-receiver]

* Receiver + paired‑device enumeration, live inventory, and on‑demand device renders.
* DPI (`0x2201`) and SmartShift (`0x2111`) read / write.
* Device pairing (add / remove) — **work in progress**.

##### Interface [#interface]

* Interactive mouse model with clickable hotspots and leader‑line labels.
* Flat‑menu binding picker with a cascading gesture submenu.
* Localized: English · 简体中文 · 繁體中文 · 日本語.
* Settings and About windows; follows the OS light / dark appearance.
* Guided macOS Accessibility‑permission onboarding.

##### Packaging [#packaging]

* Signed + notarized `.app` / DMG, a Homebrew tap, and an app icon.

##### Known limitations [#known-limitations]

* Verified only on the **MX Master 4**.
* **Launchpad** was removed in macOS 26, so that action is a no‑op there.
* Thumb‑wheel scroll re‑synthesis may need per‑device tuning.
* Device pairing is still being built out.

## v0.0.1 [#v001]

*Released May 30, 2026* · [View on GitHub ↗](https://github.com/AprilNEA/OpenLogi/releases/tag/v0.0.1)

*No release notes.*


# Connect a device (/docs/connect-device)





OpenLogi reaches Logitech HID++ devices over three transports:

* **Receiver** — a Logi Bolt, Unifying, or Lightspeed USB receiver; OpenLogi
  discovers the receiver and lists every paired device. See
  [Receivers](/hidpp/receivers) for the recognised dongles.
* **Bluetooth-direct** — a device paired straight to the computer over
  Bluetooth, no receiver.
* **Wired** — a device connected over USB.

Not everything speaks HID++: Litra lights are driven over raw HID and Logitech
webcams over USB Video Class, and both show up in the same carousel; see
[Litra lights](/docs/features/lights) and [Webcams](/docs/features/webcams).

## Verifying the connection [#verifying-the-connection]

Open the GUI. Connected devices appear in the device carousel, with battery
percentage and charge state for online devices. From the CLI, run
`openlogi list` to print the same inventory headlessly.

<Figure caption="The device carousel: each paired device with its slot, transport, and live battery state.">
    <img alt="OpenLogi device carousel listing two paired Logitech mice with battery and connection status" src="__img0" />
</Figure>

If a device doesn't show up, confirm **Logi Options+ is fully quit** (including
its background agent) so it isn't holding the receiver. On Linux, quit **Solaar**
and check the GUI's **Settings → Permissions** page: the udev rules must be
installed for `/dev/hidraw*` access (see
[Installation](/docs/installation#linux)).

## Pairing a new device [#pairing-a-new-device]

A Bolt receiver can pair new devices from the GUI's **Add device** flow:
discovery lists nearby pairable devices, and confirming one starts the passkey
exchange Bolt requires: for a mouse, a short sequence of left/right clicks.
Unifying and Lightspeed receivers are enumerated and driven, but pairing new
devices to them is not implemented; use Logitech's own pairing tool for those.


# Introduction (/docs)





**A local-first Logitech Options+ alternative written in Rust.**

Remap buttons, adjust DPI and SmartShift, and switch profiles per app without a
Logitech account, telemetry, or the official Options+ install.

<Callout type="warning">
  OpenLogi is under active development and not yet stable. Features and
  configuration may still change.
</Callout>

## Introduction [#introduction]

OpenLogi talks to Logitech HID++ peripherals over Logi Bolt, Unifying, and
Lightspeed receivers, Bluetooth-direct connections, or USB cables, without
running Logi Options+. Everything stays on your machine: bindings live in a plain
TOML file, button presses are remapped through the OS input hook, and DPI,
SmartShift, scrolling, and lighting changes are written straight to the device
over HID++.

## What it controls [#what-it-controls]

* **Mice** — buttons and gestures, DPI presets, SmartShift, scroll inversion and
  wheel resolution, the thumb wheel, and the MX Master 4's Haptic Sense Panel.
* **Keyboards** — F-row remapping over HID++, Fn-lock, host switching, RGB
  colour, and the MX Keys backlight.
* **Litra lights** — power, brightness, colour temperature, and auto-on with the
  camera.
* **Webcams** — device-level UVC image controls and a live preview.

## Beyond Options+ [#beyond-options]

Things OpenLogi does that Options+ won't:

* **Run on Linux.** Options+ ships for macOS and Windows only. OpenLogi treats
  Linux as a first-class platform: evdev/uinput hook, udev rules, a systemd
  user unit, and `.deb` / `.rpm` / `.pkg.tar.zst` packages.
* **Move the Gesture Button.** Pick which physical buttons own a gesture role —
  thumb pad, haptic panel, middle, back, or forward — with per-direction swipe
  bindings, and as many at once as you like. Options+ pins the gesture role to
  the dedicated thumb pad.
* **Keep config in plain text.** Everything is one TOML file you can read,
  diff, version-control, and copy between machines.
* **Script it.** A real CLI: device inventory, asset prefetch, light and camera
  control, and on-device HID++ diagnostics.
* **Stay light.** Native Rust binaries: no Electron suite, no resident
  updaters, no account, no telemetry.

## Quick Start [#quick-start]

<Steps>
  <Step>
    ### Install [#install]

    [Install OpenLogi](/docs/installation) on macOS, Linux, or Windows.
  </Step>

  <Step>
    ### Connect [#connect]

    <OptionsPlusNotice />

    [Connect a device](/docs/connect-device) over a Bolt, Unifying, or Lightspeed receiver, Bluetooth, or USB.
  </Step>
</Steps>

## Next steps [#next-steps]

<Cards>
  <Card icon="<BadgeCheckIcon />" href="/docs/supported-devices" title="Supported devices">
    See what OpenLogi controls and how to check your own device.
  </Card>

  <Card icon="<BadgeCheckIcon />" href="/docs/features" title="Features">
    Configure buttons, pointers, keyboards, lights, and webcams.
  </Card>

  <Card icon="<BadgeCheckIcon />" href="/hidpp" title="HID++ reference">
    Explore the protocol OpenLogi speaks.
  </Card>
</Cards>

***

*Not affiliated with Logitech. "Logitech", "MX Master", and "Options+" are
trademarks of Logitech International S.A.*


# Installation (/docs/installation)





macOS, Linux, and Windows are supported.

<OptionsPlusNotice />

## macOS [#macos]

Requires macOS 13 or later.

<Tabs items="['Homebrew', 'DMG installer']">
  <Tab value="Homebrew">
    ```sh
    brew install --cask openlogi
    ```

    The official cask is the default path. To track the latest GitHub release
    instead:

    ```sh
    brew tap aprilnea/tap
    brew install --cask aprilnea/tap/openlogi@latest
    ```

    <Callout type="info">
      Install either `openlogi` or `openlogi@latest`, not both.
    </Callout>
  </Tab>

  <Tab value="DMG installer">
    ### Download the release [#download-the-release]

    Download the signed, notarized DMG installer for
    [Apple silicon](/download/mac-arm64) or [Intel](/download/mac-x64).

    ### Install the app [#install-the-app]

    Open the DMG installer and drag **OpenLogi.app** to `/Applications`.

    ### Launch and grant permissions [#launch-and-grant-permissions]

    On first launch, macOS may ask you to confirm the app and to grant
    **Accessibility** / **Input Monitoring** permission, required to remap
    buttons through the OS event tap. Webcam preview additionally needs
    **Camera** permission.
  </Tab>
</Tabs>

## Linux [#linux]

Download the package for your distribution. Every format is published for both
`x86_64`/`amd64` and `arm64`/`aarch64`:

```sh
# Debian / Ubuntu
sudo dpkg -i openlogi_*.deb

# Fedora / RHEL
sudo rpm -i openlogi-*.rpm

# Arch Linux
sudo pacman -U openlogi-*.pkg.tar.zst
```

Direct links: [`.deb`](/download/linux) · [`.rpm`](/download/linux-rpm) ·
[`.pkg.tar.zst`](/download/linux-arch).

The package installs udev rules that grant your user access to `/dev/hidraw*`
(HID++ commands), `/dev/uinput` (the virtual remapping device), and your
Logitech mouse's `/dev/input/event*` node; no `sudo` needed. Then enable the
background agent for your user:

```sh
systemctl --user enable --now openlogi-agent.service
```

<Callout type="info">
  If a device was already plugged in when the udev rules were installed, unplug
  and replug the receiver (or power-cycle the device) so the new rules apply.
  The GUI's **Settings → Permissions** page shows a live access indicator.
</Callout>

For manual / source installs and distros without systemd, see
[INSTALL-linux.md](https://github.com/AprilNEA/OpenLogi/blob/master/docs/INSTALL-linux.md).

## Windows [#windows]

Download the signed `.msi` installer for
[x86\_64](/download/windows-x64) or [arm64](/download/windows-arm64). Portable
`.zip` builds are attached to each release too.

Both ship the GUI (`OpenLogi.exe`) alongside the background agent
(`openlogi-agent.exe`), which owns all device I/O; keep the two files side by
side when using the portable zip, or the GUI has nothing to connect to. The agent
shows a notification-area icon (Show Main Window / Quit) so the app stays
reachable after the main window is closed; to hide it, set
`show_in_menu_bar = false` in the TOML `[app_settings]` block and restart the
agent (the GUI toggle is macOS-only today).

## Build from source [#build-from-source]

See
[DEVELOPMENT.md](https://github.com/AprilNEA/OpenLogi/blob/master/docs/DEVELOPMENT.md)
in the repository.


# Supported devices (/docs/supported-devices)





<DeviceCatalog />

The feature table OpenLogi reads from your device is authoritative. Run
`openlogi diag features` to inspect it.

***

| Mark | Meaning              |
| ---- | -------------------- |
| ✓    | Fully supported.     |
| ○    | Depends on firmware. |
| —    | Not supported yet.   |

## Mice and trackballs [#mice-and-trackballs]

| Family                                  | Buttons | Gestures | DPI | SmartShift | Thumb wheel | Haptics |
| --------------------------------------- | :-----: | :------: | :-: | :--------: | :---------: | :-----: |
| MX Master 4                             |    ✓    |     ✓    |  ✓  |      ✓     |      ✓      |    ✓    |
| MX Master 3 / 3S                        |    ✓    |     ✓    |  ✓  |      ✓     |      ✓      |    —    |
| MX Master 2S                            |    ✓    |     ✓    |  ✓  | ✓ `0x2110` |  ✓ `0x6501` |    —    |
| MX Anywhere 2S / 3 / 3S                 |    ✓    |     ✓    |  ✓  |      ○     |      —      |    —    |
| MX Vertical, MX Ergo, Ergo M575         |    ✓    |     ○    |  ✓  |      ○     |      —      |    —    |
| Lift, Signature, Pebble, M-series       |    ✓    |     ○    |  ○  |      —     |      —      |    —    |
| G-series wireless (G305, G502 X, G903…) |    ○    |     —    |  ○  |      —     |      —      |    —    |

SmartShift is the enhanced `0x2111` variant on MX Master 3 / 3S / 4 and current
MX-line mice; the MX Master 2S answers the older `0x2110`, and its horizontal
wheel is a `0x6501` gesture descriptor rather than the `0x2150` thumb wheel
feature. Both paths are driven.

## Keyboards [#keyboards]

F-row remapping goes through the OS input hook, so it works on **any** keyboard,
including ones absent from the table below. What differs is the on-device
settings a board exposes:

| Family                                | Fn-lock | Host switching | Backlight | RGB |
| ------------------------------------- | :-----: | :------------: | :-------: | :-: |
| MX Keys, Keys S, Keys Mini            |    ✓    |        ✓       |     ✓     |  —  |
| MX Mechanical, Mechanical Mini        |    ✓    |        ✓       |     ✓     |  —  |
| Craft, Ergo K860, K380 / K480, K580   |    ○    |        ✓       |     ○     |  —  |
| G915, G815, G513 and other RGB boards |    ○    |        —       |     —     |  ✓  |

RGB is written as one static colour through `0x8070` rather than per-key
effects; the MX Keys white backlight is `0x1982` and is driven from the CLI. See
[Keyboard lighting](/docs/features/keyboard/lighting).

## Lights and webcams [#lights-and-webcams]

| Device              | USB ids       | Controls                                     |
| ------------------- | ------------- | -------------------------------------------- |
| Litra Glow          | `046d:c900`   | Power, 20–250 lm, 2700–6500 K in 100 K steps |
| Litra Beam          | `046d:c901`   | Power, 20–250 lm, 2700–6500 K in 100 K steps |
| Any Logitech webcam | vendor `046d` | UVC image controls, live preview, profiles   |

A Litra is matched on its complete raw-HID route — vendor id, product id, usage
page `0xff43`, usage `0x0202` — not the product id alone, so a vendor report
never lands on an unrelated HID collection. Other Litra models are not driven
yet. Cameras are the opposite: they are standard UVC devices, so detection keys
off the vendor id and no model table is involved. See
[Litra lights](/docs/features/lights) and [Webcams](/docs/features/webcams).

## Receivers [#receivers]

| Receiver   | USB ids                     | Status                                                        |
| ---------- | --------------------------- | ------------------------------------------------------------- |
| Logi Bolt  | `046D:C548`                 | Discovery, enumeration, and passkey pairing from the GUI      |
| Unifying   | `046D:C52B`, `C532`, `C537` | Enumerated and driven; pairing new devices is not implemented |
| Lightspeed | `046D:C539`, `C53F`, `C547` | Same code path as Unifying, surfaced under its own name       |

`C537` is the Nano receiver bundled with the G602; `C539` ships with the G502
LIGHTSPEED and the G Pro Wireless, `C53F` is the G305's nano receiver, and
`C547` ships with newer G-series hardware such as the G915 and the G502 X
LIGHTSPEED. Lightspeed dongles answer the same HID++ 1.0 registers as Unifying,
so they are enumerated, routed, and paired through the Unifying path; only the
displayed name differs. Details in the
[receiver reference](/hidpp/receivers).

A dongle outside those ids is not recognised as a receiver, so the devices
paired to it are invisible. The device itself is still supported over Bluetooth
or a cable — Bluetooth-direct and wired devices are addressed on their own HID++
channel at device index `0xFF`, with no pairing-slot indirection. On macOS, a
Bluetooth-direct mouse such as the Lift or a Signature needs **Input
Monitoring** before it appears; see
[Connect a device](/docs/connect-device).

## Not supported yet [#not-supported-yet]

* **Audio devices.** A headset paired to a receiver is listed in the inventory,
  but there are no panels for it — no sidetone, no equalizer.
* **Onboard-memory profiles.** OpenLogi never writes a G-series onboard profile
  ([`0x8100`](/hidpp/features/x8100-onboard-profiles)). Its own
  [per-app profiles](/docs/features/profiles) are host-side and follow the
  frontmost application.
* **Non-Logitech hardware.** Both HID++ enumeration and camera enumeration
  filter on the Logitech vendor id (`0x046d`).

## Check your own device [#check-your-own-device]

```sh
openlogi list            # what is connected, per receiver, with battery
openlogi diag features   # every HID++ feature the active device reports
openlogi diag controls   # reprogrammable controls and their capability flags
```

If `openlogi list` finds nothing, the usual cause is &#x2A;*Logi Options+** still
holding the receiver, or missing permissions — see
[Connect a device](/docs/connect-device).

## Feature panels [#feature-panels]

OpenLogi reads a device's feature table (`0x0001 FeatureSet`) once and gates
panels on the feature ids present, never on the device's marketing type — a
mouse that misreports itself keeps every panel its firmware backs.

| Panel              | Driving feature                                                                       | Notes                                                                            |
| ------------------ | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Buttons & gestures | `0x1b00`–`0x1b04` ReprogControls                                                      | Divertable controls are remapped through the OS input hook                       |
| DPI presets        | `0x2201` AdjustableDpi or `0x2202` ExtendedAdjustableDpi                              | Either id turns the panel on; both are drivable                                  |
| SmartShift         | `0x2111` SmartShiftEnhanced, falling back to `0x2110`                                 | The MX Master 2S is the classic `0x2110`-only case                               |
| Scroll inversion   | `0x2121` HiResWheel reporting `has_invert`                                            | Native inversion, written to the device                                          |
| Wheel resolution   | `0x2121` HiResWheel                                                                   | Read and changed independently of inversion                                      |
| Thumb wheel        | `0x2150` Thumbwheel, or `0x6501` Gestures2 gesture 46                                 | The legacy descriptor covers MX Master 2S-era mice                               |
| Haptic feedback    | `0x19b0` HapticFeedback                                                               |                                                                                  |
| Haptic Sense Panel | A divertable `0x01a0` control in the `0x1b04` table                                   | The MX Master 4's panel; bindable like any other control                         |
| RGB colour         | `0x8070` ColorLedEffects, then `0x8081` PerKeyLighting2, then `0x8080` PerKeyLighting | `0x8070` is preferred because a fixed effect overrides a running onboard profile |
| Backlight          | `0x1982` Backlight                                                                    | The MX Keys white backlight, not RGB                                             |
| Fn-lock            | `0x40a3` FnInversionForMultiHost, falling back to `0x40a2`                            | Multi-host boards store it per Easy-Switch slot                                  |
| Host switching     | `0x1814` ChangeHost with `0x1815` HostsInfo                                           | Switching still works when `0x1815` is absent                                    |
| Battery            | `0x1004` unified, `0x1000` legacy, or `0x1001` voltage                                | G-series wireless devices report voltage only                                    |

Look any id up in the [HID++ reference](/hidpp/features).


# Index (/hidpp/features)



Each feature is identified by a 16-bit ID. The attribution and copyright notice
is on the [HID++ overview](/hidpp).

## Support status [#support-status]

OpenLogi ships typed wrappers for 44 of 112 catalogued HID++ 2.0 features;
22 more notable features are documented here for reference, and the remaining
46 are listed below.

**Legend:** ✅ **Implemented** — typed wrapper in `openlogi-hidpp` · 📄 **Documented** — reference page, not implemented · ▫ **Listed** — known feature, no page yet

## Core [#core]

| ID                                                     | Feature                | Status | Summary                               |
| ------------------------------------------------------ | ---------------------- | ------ | ------------------------------------- |
| [`0x0000`](/hidpp/features/x0000-root)                 | root                   | ✅      | Resolve a feature ID to its index     |
| [`0x0001`](/hidpp/features/x0001-featureset)           | featureSet             | ✅      | Enumerate the device's features       |
| `0x0002`                                               | featureInfo            | ▫      | Query metadata about a feature        |
| [`0x0003`](/hidpp/features/x0003-device-information)   | deviceInformation      | ✅      | Firmware, serial, entity list         |
| `0x0004`                                               | unitId                 | ▫      | Persistent unit identity across hosts |
| [`0x0005`](/hidpp/features/x0005-device-type-and-name) | deviceTypeAndName      | ✅      | Device type and marketing name        |
| `0x0006`                                               | deviceGroups           | ▫      | Logical device group membership       |
| [`0x0007`](/hidpp/features/x0007-device-friendly-name) | deviceFriendlyName     | ✅      | User-assigned device name             |
| `0x0008`                                               | keepAlive              | ▫      | Periodic heartbeat to prevent timeout |
| `0x0020`                                               | configChange           | ▫      | Notify host of configuration changes  |
| `0x0021`                                               | uniqueRandomId         | ▫      | Random unique ID generation           |
| `0x0030`                                               | targetSoftware         | ▫      | Target software compatibility info    |
| `0x0080`                                               | wirelessSignalStrength | ▫      | RF signal strength reporting          |

## Firmware, DFU & device management [#firmware-dfu--device-management]

| ID                                                   | Feature            | Status | Summary                          |
| ---------------------------------------------------- | ------------------ | ------ | -------------------------------- |
| `0x00c0`                                             | dfuControlLegacy   | ▫      | Legacy DFU mode entry control    |
| `0x00c1`                                             | dfuControlUnsigned | ▫      | Unsigned firmware update control |
| [`0x00c2`](/hidpp/features/x00c2-dfu-control-signed) | dfuControlSigned   | 📄     | Signed firmware update entry     |
| `0x00c3`                                             | dfuControlBolt     | ▫      | Bolt-receiver DFU control        |
| [`0x00d0`](/hidpp/features/x00d0-dfu)                | dfu                | 📄     | Device firmware upgrade protocol |
| `0x00d1`                                             | dfuResumable       | ▫      | Resumable DFU session support    |
| [`0x1802`](/hidpp/features/x1802-device-reset)       | deviceReset        | 📄     | Trigger a device software reset  |
| [`0x1805`](/hidpp/features/x1805-oob-state)          | oobState           | 📄     | Out-of-box state management      |
| `0x1806`                                             | configDeviceProps  | ▫      | Configurable device properties   |
| `0x1f1f`                                             | firmwareProperties | ▫      | Firmware build properties        |
| `0x1f20`                                             | adcMeasurement     | ▫      | On-device ADC sensor readings    |
| `0x1800`                                             | genericTest        | ▫      | Generic manufacturing test hooks |
| `0x1df0`                                             | remainingPairings  | ▫      | Remaining pairing slots count    |

## Power & battery [#power--battery]

| ID                                                         | Feature                | Status | Summary                            |
| ---------------------------------------------------------- | ---------------------- | ------ | ---------------------------------- |
| [`0x1000`](/hidpp/features/x1000-battery-status)           | batteryStatus          | ✅      | Battery charge percentage & status |
| [`0x1001`](/hidpp/features/x1001-battery-voltage)          | batteryVoltage         | ✅      | Raw battery voltage level          |
| [`0x1004`](/hidpp/features/x1004-unified-battery)          | unifiedBattery         | ✅      | Battery percentage and state       |
| [`0x1010`](/hidpp/features/x1010-charging-control)         | chargingControl        | 📄     | Charge speed and threshold control |
| [`0x4301`](/hidpp/features/x4301-solar-keyboard-dashboard) | solarKeyboardDashboard | ✅      | Solar keyboard light & battery     |

## Connection, hosts & platform [#connection-hosts--platform]

| ID                                                       | Feature              | Status | Summary                            |
| -------------------------------------------------------- | -------------------- | ------ | ---------------------------------- |
| [`0x1814`](/hidpp/features/x1814-change-host)            | changeHost           | ✅      | Switch the active host channel     |
| [`0x1815`](/hidpp/features/x1815-hosts-info)             | hostsInfo            | ✅      | Multi-host slots & names           |
| [`0x1d4b`](/hidpp/features/x1d4b-wireless-device-status) | wirelessDeviceStatus | ✅      | Link / reconnect events            |
| `0x1bc0`                                                 | reportHidUsages      | ▫      | Report HID usage pages to host     |
| `0x1300`                                                 | ledControl           | ▫      | Generic LED on/off control         |
| [`0x4530`](/hidpp/features/x4530-dual-platform)          | dualPlatform         | ✅      | Two-platform (iOS/Android…) select |
| [`0x4531`](/hidpp/features/x4531-multi-platform)         | multiPlatform        | ✅      | Per-host OS platform select        |

## Pointer, DPI & wheel [#pointer-dpi--wheel]

| ID                                                          | Feature                 | Status | Summary                            |
| ----------------------------------------------------------- | ----------------------- | ------ | ---------------------------------- |
| [`0x2100`](/hidpp/features/x2100-vertical-scrolling)        | verticalScrolling       | ✅      | Roller info & ratchet/free toggle  |
| [`0x2110`](/hidpp/features/x2110-smartshift)                | smartShiftWheel         | ✅      | Original SmartShift variant        |
| [`0x2111`](/hidpp/features/x2111-smartshift-enhanced)       | smartShiftWheelEnhanced | ✅      | Ratchet/free-spin + threshold      |
| [`0x2120`](/hidpp/features/x2120-high-resolution-scrolling) | highResolutionScrolling | 📄     | High-resolution scroll wheel       |
| [`0x2121`](/hidpp/features/x2121-hires-wheel)               | hiResWheel              | ✅      | High-resolution / free-spin wheel  |
| `0x2130`                                                    | ratchetWheel            | ▫      | Ratchet mode control               |
| [`0x2150`](/hidpp/features/x2150-thumbwheel)                | thumbwheel              | ✅      | Side thumbwheel                    |
| [`0x2200`](/hidpp/features/x2200-mouse-pointer)             | mousePointer            | ✅      | Sensor resolution & tuning hints   |
| [`0x2201`](/hidpp/features/x2201-adjustable-dpi)            | adjustableDpi           | ✅      | DPI sensors and presets            |
| [`0x2202`](/hidpp/features/x2202-extended-adjustable-dpi)   | extendedAdjustableDpi   | ✅      | X/Y DPI, LOD, calibration          |
| `0x2205`                                                    | pointerMotionScaling    | ▫      | Pointer acceleration scaling       |
| `0x2230`                                                    | sensorAngleSnapping     | ▫      | Sensor angle-snapping control      |
| `0x2240`                                                    | surfaceTuning           | ▫      | Surface-specific sensor tuning     |
| `0x2250`                                                    | xyStats                 | ▫      | X/Y motion statistics              |
| `0x2251`                                                    | wheelStats              | ▫      | Scroll-wheel motion statistics     |
| `0x2400`                                                    | hybridTrackingEngine    | ▫      | Hybrid optical/mechanical tracking |

## Report rate [#report-rate]

| ID                                                     | Feature                      | Status | Summary                    |
| ------------------------------------------------------ | ---------------------------- | ------ | -------------------------- |
| [`0x8060`](/hidpp/features/x8060-report-rate)          | adjustableReportRate         | ✅      | Report-rate list & select  |
| [`0x8061`](/hidpp/features/x8061-extended-report-rate) | extendedAdjustableReportRate | ✅      | Per-connection report rate |

## Keyboard [#keyboard]

| ID                                                        | Feature                        | Status | Summary                            |
| --------------------------------------------------------- | ------------------------------ | ------ | ---------------------------------- |
| [`0x40a0`](/hidpp/features/x40a0-fn-inversion)            | fnInversion                    | 📄     | Legacy global Fn-inversion toggle  |
| [`0x40a2`](/hidpp/features/x40a2-fn-inversion)            | fnInversionWithDefaultState    | ✅      | Global Fn-inversion state          |
| [`0x40a3`](/hidpp/features/x40a3-fn-inversion-multi-host) | fnInversionForMultiHostDevices | ✅      | Per-host Fn-inversion state        |
| `0x4100`                                                  | encryption                     | ▫      | Keyboard encryption key management |
| `0x4220`                                                  | lockKeyState                   | ▫      | Caps/Num/Scroll lock key state     |
| [`0x4520`](/hidpp/features/x4520-keyboard-layout)         | keyboardLayout                 | 📄     | Physical keyboard layout reporting |
| [`0x4521`](/hidpp/features/x4521-disable-keys)            | disableKeys                    | ✅      | Disable lock / system keys         |
| [`0x4522`](/hidpp/features/x4522-disable-keys-by-usage)   | disableKeysByUsage             | ✅      | Disable any keys by HID usage      |
| `0x4540`                                                  | keyboardInternationalLayouts   | ▫      | International layout variants      |

## Buttons & remapping [#buttons--remapping]

| ID                                                             | Feature                    | Status | Summary                           |
| -------------------------------------------------------------- | -------------------------- | ------ | --------------------------------- |
| [`0x1b00`](/hidpp/features/x1b00-reprog-controls)              | reprogControls             | 📄     | Legacy reprogrammable controls    |
| `0x1b01`                                                       | reprogControls2            | ▫      | Reprogrammable controls v2        |
| `0x1b02`                                                       | reprogControls3            | ▫      | Reprogrammable controls v3        |
| `0x1b03`                                                       | reprogControls4            | ▫      | Reprogrammable controls v4        |
| [`0x1b04`](/hidpp/features/x1b04-special-keys-mse-buttons)     | reprogControls5            | ✅      | Reprogrammable controls / buttons |
| [`0x1c00`](/hidpp/features/x1c00-persistent-remappable-action) | persistentRemappableAction | ✅      | Persistent on-device remaps       |
| [`0x2001`](/hidpp/features/x2001-swap-left-right-button)       | swapLeftRightButton        | 📄     | Swap primary mouse buttons        |
| `0x2005`                                                       | buttonSwapCancel           | ▫      | Cancel button-swap state          |
| `0x2006`                                                       | pointerAxesOrientation     | ▫      | Flip X/Y pointer axes             |

## Lighting [#lighting]

| ID                                                   | Feature            | Status | Summary                         |
| ---------------------------------------------------- | ------------------ | ------ | ------------------------------- |
| [`0x1981`](/hidpp/features/x1981-backlight1)         | backlight1         | 📄     | Keyboard backlight (v1)         |
| [`0x1982`](/hidpp/features/x1982-backlight)          | backlight2         | ✅      | Keyboard backlight & effects    |
| `0x1983`                                             | backlight3         | ▫      | Keyboard backlight v3           |
| [`0x1990`](/hidpp/features/x1990-illumination)       | illumination       | ✅      | Brightness & color temperature  |
| [`0x19b0`](/hidpp/features/x19b0-haptic-feedback)    | hapticFeedback     | ✅      | Haptic actuator control         |
| `0x19c0`                                             | forceSensingButton | ▫      | Force-sensing button thresholds |
| [`0x8040`](/hidpp/features/x8040-brightness-control) | brightnessControl  | ✅      | Generic brightness control      |
| [`0x8070`](/hidpp/features/x8070-color-led-effects)  | colorLedEffects    | ✅      | Per-zone RGB effect engine      |
| [`0x8071`](/hidpp/features/x8071-rgb-effects)        | rgbEffects         | ✅      | Modern per-cluster RGB engine   |
| [`0x8080`](/hidpp/features/x8080-per-key-lighting)   | perKeyLighting     | 📄     | Per-key RGB (legacy protocol)   |
| [`0x8081`](/hidpp/features/x8081-per-key-lighting)   | perKeyLighting2    | ✅      | Per-key RGB zones               |
| [`0x8090`](/hidpp/features/x8090-mode-status)        | modeStatus         | ✅      | Host vs onboard mode            |

## Audio [#audio]

| ID                                          | Feature    | Status | Summary                |
| ------------------------------------------- | ---------- | ------ | ---------------------- |
| [`0x8300`](/hidpp/features/x8300-sidetone)  | sidetone   | ✅      | Headset sidetone level |
| [`0x8310`](/hidpp/features/x8310-equalizer) | equalizer  | ✅      | Audio EQ band gains    |
| `0x8320`                                    | headsetOut | ▫      | Headset output routing |

## Touch & crown [#touch--crown]

| ID                                                | Feature                   | Status | Summary                         |
| ------------------------------------------------- | ------------------------- | ------ | ------------------------------- |
| [`0x4600`](/hidpp/features/x4600-crown)           | crown                     | ✅      | MX crown mode & events          |
| `0x6010`                                          | touchpadFwItems           | ▫      | Touchpad firmware items         |
| `0x6011`                                          | touchpadSwItems           | ▫      | Touchpad software items         |
| `0x6012`                                          | touchpadWin8FwItems       | ▫      | Win8-specific touchpad firmware |
| [`0x6020`](/hidpp/features/x6020-tap-enable)      | tapEnable                 | 📄     | Touchpad tap-to-click toggle    |
| `0x6021`                                          | tapEnableExtended         | ▫      | Extended tap gesture settings   |
| `0x6030`                                          | cursorBallistic           | ▫      | Touchpad cursor ballistics      |
| `0x6040`                                          | touchpadResolutionDivider | ▫      | Touchpad resolution scaling     |
| [`0x6100`](/hidpp/features/x6100-touchpad-raw-xy) | touchpadRawXY             | ✅      | Raw touchpad multi-touch data   |
| [`0x6110`](/hidpp/features/x6110-touch-mouse-raw) | touchMouseRawTouchPoints  | ✅      | Raw touch-mouse points          |
| `0x6120`                                          | btTouchMouseSettings      | ▫      | Bluetooth touch-mouse settings  |
| [`0x6500`](/hidpp/features/x6500-gestures1)       | gestures1                 | 📄     | Gesture configuration (v1)      |
| [`0x6501`](/hidpp/features/x6501-gestures2)       | gestures2                 | ✅      | Gesture configuration (v2)      |

## Gaming [#gaming]

| ID                                                    | Feature           | Status | Summary                          |
| ----------------------------------------------------- | ----------------- | ------ | -------------------------------- |
| [`0x8010`](/hidpp/features/x8010-gaming-g-keys)       | gamingGKeys       | 📄     | Programmable G-key management    |
| [`0x8020`](/hidpp/features/x8020-gaming-m-keys)       | gamingMKeys       | 📄     | Mode M-key management            |
| [`0x8030`](/hidpp/features/x8030-macro-record)        | macroRecord       | 📄     | On-device macro recording        |
| [`0x8100`](/hidpp/features/x8100-onboard-profiles)    | onboardProfiles   | 📄     | Onboard profile storage & switch |
| [`0x8110`](/hidpp/features/x8110-mouse-button-filter) | mouseButtonFilter | 📄     | Mouse button event filtering     |
| [`0x8111`](/hidpp/features/x8111-latency-monitoring)  | latencyMonitoring | 📄     | End-to-end click latency stats   |
| `0x8120`                                              | gamingAttachments | ▫      | Peripheral attachment detection  |
| [`0x8123`](/hidpp/features/x8123-force-feedback)      | forceFeedback     | 📄     | Force-feedback actuator control  |

## Presenter & special input [#presenter--special-input]

| ID                                                  | Feature          | Status | Summary                           |
| --------------------------------------------------- | ---------------- | ------ | --------------------------------- |
| [`0x1a00`](/hidpp/features/x1a00-presenter-control) | presenterControl | 📄     | Presenter remote control actions  |
| `0x1a01`                                            | sensor3D         | ▫      | 3D gyroscope/accelerometer sensor |


# 0x0000 · root (/hidpp/features/x0000-root)



The entry point of every HID++ 2.0 device. `root` maps a 16-bit **feature ID**
to the **feature index** the device uses for that feature in the current session,
and reports the HID++ protocol version.

A client calls `getFeature(featureId)` first, then addresses every other feature
by the index it returns. `root` itself is always at index `0`.

> **Spec:** Logitech HID++ 2.0 — *x0000 root*. &#x2A;*Used by:** all device access.

## Function reference [#function-reference]

The `RootFeature` wrapper (`0x0000`, always bound to feature index `0`) exposes:

### Methods [#methods]

| Function      | HID++ fn | Signature    | Returns                      |
| ------------- | -------- | ------------ | ---------------------------- |
| `get_feature` | 0        | `(id: u16)`  | `Option<FeatureInformation>` |
| `ping`        | 1        | `(data: u8)` | `u8`                         |

All methods are `async` and return `Result<…, Hidpp20Error>`. `get_feature` resolves to `None` when the device does not support the requested feature ID; `ping` echoes `data` back when the link is alive.

### Types [#types]

#### `FeatureInformation` [#featureinformation]

Information about a feature, returned by `get_feature`.

| Field     | Type          | Description                                                                       |
| --------- | ------------- | --------------------------------------------------------------------------------- |
| `index`   | `u8`          | The feature's index in the device's feature table, used to address its functions. |
| `typ`     | `FeatureType` | The feature's type flags.                                                         |
| `version` | `u8`          | Latest supported version of the feature (`0` on root v0 devices).                 |

#### `FeatureType` [#featuretype]

Feature classification flags decoded from the type byte.

| Field                         | Type   | Description                                                                          |
| ----------------------------- | ------ | ------------------------------------------------------------------------------------ |
| `obsolete`                    | `bool` | Feature replaced by a newer one, kept so older software still finds it (bit 7).      |
| `hidden`                      | `bool` | SW-hidden feature; configuration software should ignore it (bit 6).                  |
| `engineering`                 | `bool` | Hidden feature disabled for user software; internal testing / manufacturing (bit 5). |
| `manufacturing_deactivatable` | `bool` | Manufacturing feature that can be permanently deactivated (bit 4; added in v2).      |
| `compliance_deactivatable`    | `bool` | Compliance feature that can be permanently deactivated (bit 3; added in v2).         |

## Wire format [#wire-format]

Both functions use a **3-byte short-report request** payload. Responses are read from the **extended payload** (up to 16 bytes) returned by the device.

### `get_feature` (fn 0) [#get_feature-fn-0]

Request: `[ id_hi, id_lo, 0x00 ]`

* Byte 0: high byte of the 16-bit feature ID (`(id >> 8) as u8`)
* Byte 1: low byte of the 16-bit feature ID (`id as u8`)
* Byte 2: reserved, always `0x00`

Response (byte → field):

| Byte | Field                     | Notes                                                                                                                                   |
| ---- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | `index`                   | Feature index in the device table. `0` means the feature is unsupported (returns `None`).                                               |
| 1    | type byte → `FeatureType` | Bit 7 = `obsolete`, bit 6 = `hidden`, bit 5 = `engineering`, bit 4 = `manufacturing_deactivatable`, bit 3 = `compliance_deactivatable`. |
| 2    | `version`                 | Latest supported feature version; `0` on root v0 devices.                                                                               |

### `ping` (fn 1) [#ping-fn-1]

Request: `[ 0x00, 0x00, data ]`

* Bytes 0–1: reserved, always `0x00`
* Byte 2: arbitrary ping byte chosen by the caller

Response (byte → field):

| Byte | Field         | Notes                                                                                                                                    |
| ---- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 2    | echoed `data` | The device echoes the ping byte at the same offset. Bytes 0–1 also carry the HID++ protocol version but are not exposed by this wrapper. |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::root::RootFeature};

// mut device: Device, obtained via Device::new(channel, index).await?
// RootFeature is registered automatically; no enumerate_features() needed.
let feat = device.root(); // returns Arc<RootFeature>

// Look up a feature by its 16-bit ID (e.g. 0x1000 = battery unified).
if let Some(info) = feat.get_feature(0x1000).await? {
    println!("feature 0x1000 → index {}, version {}", info.index, info.version);
}

// Verify the link is alive; the device echoes the byte back.
let echo = feat.ping(0x42).await?;
assert_eq!(echo, 0x42);
```


# 0x0001 · featureSet (/hidpp/features/x0001-featureset)



Enumerates the features a device implements. `getCount()` returns how many
features there are; `getFeatureId(index)` returns the ID (and type flags:
hidden, obsolete, etc.) at each index.

Together with [`0x0000` root](/hidpp/features/x0000-root) this lets a client
discover a device's full capability set without prior knowledge of the model.

> **Spec:** Logitech HID++ 2.0 — *x0001 featureSet*. &#x2A;*Used by:** capability
> discovery.

## Function reference [#function-reference]

The `FeatureSetFeature` wrapper (`0x0001`) exposes:

### Methods [#methods]

| Function      | HID++ fn | Signature     | Returns              |
| ------------- | -------- | ------------- | -------------------- |
| `count`       | 0        | `()`          | `u8`                 |
| `get_feature` | 1        | `(index: u8)` | `FeatureInformation` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `FeatureInformation` [#featureinformation]

Information about a specific feature as returned by `get_feature`.

| Field     | Type          | Description                                                                                                                                                                                        |
| --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`      | `u16`         | The protocol ID of the feature.                                                                                                                                                                    |
| `typ`     | `FeatureType` | The type flags of the feature.                                                                                                                                                                     |
| `version` | `u8`          | The latest supported version of the feature. Added in feature version 1; `0` for older versions. Multi-version features are always backwards compatible as long as the feature ID does not change. |

#### `FeatureType` [#featuretype]

A bitfield describing some properties of a feature. Each field corresponds to one flag bit in the type byte returned by the device.

| Field                         | Bit | Description                                                                                                                                            |
| ----------------------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `obsolete`                    | 7   | Feature replaced by a newer one but still advertised so that older software can support it.                                                            |
| `hidden`                      | 6   | Should not be known, managed, or used by end-user configuration software; the host should ignore it.                                                   |
| `engineering`                 | 5   | A hidden feature disabled for user software, used for internal testing and manufacturing.                                                              |
| `manufacturing_deactivatable` | 4   | Manufacturing feature that can be permanently deactivated; usually also hidden and engineering. Added in feature version 2; `false` on older versions. |
| `compliance_deactivatable`    | 3   | Compliance feature that can be permanently deactivated; usually also hidden and engineering. Added in feature version 2; `false` on older versions.    |

## Wire format [#wire-format]

Both functions use a **3-byte short-report request payload**. Responses are read from the 16-byte extended payload (`extend_payload()`, bytes 0–15; unused trailing bytes are zero-padded).

### `count` (fn 0) [#count-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field       | Notes                                                                             |
| ---- | ----------- | --------------------------------------------------------------------------------- |
| 0    | `count: u8` | Number of features supported by the device, excluding the root feature (index 0). |

### `get_feature` (fn 1) [#get_feature-fn-1]

Request: `[index, 0x00, 0x00]` — `index` is the 1-based feature table index (must not be 0).

Response (byte → field):

| Byte | Field          | Notes                                                                                                                                                                        |
| ---- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | `id` high byte | Combined with byte 1 as `(payload[0] as u16) << 8 \| payload[1] as u16`.                                                                                                     |
| 1    | `id` low byte  | See above.                                                                                                                                                                   |
| 2    | `typ` byte     | Decoded as a `FeatureType` bitfield: bit 7 = `obsolete`, bit 6 = `hidden`, bit 5 = `engineering`, bit 4 = `manufacturing_deactivatable`, bit 3 = `compliance_deactivatable`. |
| 3    | `version: u8`  | Latest supported version of the feature; `0` for feature version 0 devices.                                                                                                  |

## Usage (Rust) [#usage-rust]

```rust
use std::sync::Arc;
use hidpp::{device::Device, feature::feature_set::FeatureSetFeature};

// chan: Arc<HidppChannel>, device_index: u8 — obtained from a receiver walk.
let mut device = Device::new(Arc::clone(&chan), device_index).await?;

// High-level: enumerate all features and register implementations.
if let Some(features) = device.enumerate_features().await? {
    for info in &features {
        println!("feature 0x{:04x}  version={}  hidden={}", info.id, info.version, info.typ.hidden);
    }
}

// Low-level: use FeatureSetFeature directly after enumeration has registered it.
if let Some(feat) = device.get_feature::<FeatureSetFeature>() {
    let count = feat.count().await?;
    for i in 1..=count {
        let info = feat.get_feature(i).await?;
        println!("  [{i}] id=0x{:04x}  obsolete={}", info.id, info.typ.obsolete);
    }
}
```


# 0x0003 · deviceInformation (/hidpp/features/x0003-device-information)



Static device metadata: the number of firmware **entities** and, per entity, the
type (main firmware, bootloader, hardware) and version. Newer versions also
expose the unit ID, model ID, and serial number.

OpenLogi reads this to identify a connected device and to look up its render and
hotspot metadata on [assets.openlogi.org](https://assets.openlogi.org). The
`model_id` and `extended_model_id` a device reports here are what the
[model id index](/hidpp/model-ids) is keyed by — that page has the id's shape
and every id the registry lists.

> **Spec:** Logitech HID++ 2.0 — *x0003 deviceInformation* (v4).

## Function reference [#function-reference]

The `DeviceInformationFeature` wrapper (`0x0003`) exposes:

### Methods [#methods]

| Function            | HID++ fn | Signature            | Returns                    |
| ------------------- | -------- | -------------------- | -------------------------- |
| `get_device_info`   | 0        | `()`                 | `DeviceInformation`        |
| `get_fw_info`       | 1        | `(entity_index: u8)` | `DeviceEntityFirmwareInfo` |
| `get_serial_number` | 2        | `()`                 | `String`                   |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `DeviceInformation` [#deviceinformation]

General information about the device and its capabilities, as returned by `get_device_info`.

| Field               | Type                            | Description                                                                                                                                                  |
| ------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `entity_count`      | `u8`                            | The number of firmware entities whose version information can be retrieved via `get_fw_info`.                                                                |
| `unit_id`           | `[u8; 4]`                       | A 4-byte random value serving as a unique identifier (among all devices with the same `model_id`) for the unit. Always `0` on feature version 0.             |
| `transport`         | `DeviceTransport`               | A bitfield stating which transport protocols the device supports. Always `0` on feature version 0.                                                           |
| `model_id`          | `[u16; 3]`                      | A 6-byte array serving as the identifier for the device model, containing the application PIDs of the supported transports. Always `0` on feature version 0. |
| `extended_model_id` | `u8`                            | An additional configurable production-line attribute (e.g. device colour). Always `0` on feature version \< 2.                                               |
| `capabilities`      | `DeviceInformationCapabilities` | Additional capability flags of this feature. All capabilities flagged as unsupported on feature version \< 4.                                                |

#### `DeviceTransport` [#devicetransport]

Bitfield stating which transport protocols the device supports (added in feature version 1).

| Field       | Type   | Description                                                                    |
| ----------- | ------ | ------------------------------------------------------------------------------ |
| `usb`       | `bool` | Whether the device supports USB.                                               |
| `e_quad`    | `bool` | Whether the device supports eQuad, the protocol used by the Unifying Receiver. |
| `btle`      | `bool` | Whether the device supports Bluetooth Low Energy as used by the Bolt Receiver. |
| `bluetooth` | `bool` | Whether the device supports Bluetooth.                                         |

#### `DeviceInformationCapabilities` [#deviceinformationcapabilities]

Bitfield stating which additional capabilities of the `0x0003` feature are supported (added in feature version 4).

| Field           | Type   | Description                                                                                                   |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------- |
| `serial_number` | `bool` | Whether serial number retrieval via `get_serial_number` is supported. Always `false` on feature version \< 4. |

#### `DeviceEntityFirmwareInfo` [#deviceentityfirmwareinfo]

Firmware and version information for a single device entity, as returned by `get_fw_info`.

| Field             | Type               | Description                                                                                                 |
| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------- |
| `entity_type`     | `DeviceEntityType` | The type of the described entity.                                                                           |
| `firmware_prefix` | `String`           | A 3-letter prefix for the firmware name.                                                                    |
| `firmware_number` | `u8`               | The firmware number (decoded from packed BCD).                                                              |
| `revision`        | `u8`               | The firmware revision (decoded from packed BCD).                                                            |
| `build`           | `u16`              | The firmware build number (decoded from packed BCD).                                                        |
| `active`          | `bool`             | Whether this entity is the responding and active one. Exactly one entity is active at any given time.       |
| `transport_pid`   | `u16`              | The transport protocol PID. Set to the actual PID for the active entity; may be zero for inactive entities. |
| `extra_version`   | `[u8; 5]`          | Optional extra versioning information.                                                                      |

#### `DeviceEntityType` [#deviceentitytype]

The type of a firmware entity within the device.

| Variant              | Value | Description                          |
| -------------------- | ----- | ------------------------------------ |
| `MainApplication`    | 0     | Main application firmware entity.    |
| `Bootloader`         | 1     | Bootloader firmware entity.          |
| `Hardware`           | 2     | Hardware entity.                     |
| `Touchpad`           | 3     | Touchpad firmware/entity.            |
| `OpticalSensor`      | 4     | Optical sensor entity.               |
| `Softdevice`         | 5     | Bluetooth SoftDevice entity.         |
| `RfCompanionMcu`     | 6     | RF companion MCU entity.             |
| `FactoryApplication` | 7     | Factory application firmware entity. |
| `RgbCustomEffect`    | 8     | RGB custom effect entity.            |
| `MotorDrive`         | 9     | Motor drive entity.                  |

## Wire format [#wire-format]

All three functions use a 3-byte request payload and return a 16-byte long payload (read via `extend_payload()`). There are no events for this feature.

### `get_device_info` (fn 0) [#get_device_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte  | Field               | Notes                                                                 |
| ----- | ------------------- | --------------------------------------------------------------------- |
| 0     | `entity_count`      | Number of firmware entities                                           |
| 1–4   | `unit_id`           | 4-byte unique unit identifier; `[0,0,0,0]` on feature version 0       |
| 5     | *(reserved)*        | Not read by the implementation                                        |
| 6     | `transport`         | Bitfield: bit 3 = USB, bit 2 = eQuad, bit 1 = BTLE, bit 0 = Bluetooth |
| 7–8   | `model_id[0]`       | Big-endian u16; first transport PID                                   |
| 9–10  | `model_id[1]`       | Big-endian u16; second transport PID                                  |
| 11–12 | `model_id[2]`       | Big-endian u16; third transport PID                                   |
| 13    | `extended_model_id` | Production-line attribute (e.g. colour); `0` on feature version \< 2  |
| 14    | `capabilities`      | Bitfield: bit 0 = `serial_number`; `0` on feature version \< 4        |

### `get_fw_info` (fn 1) [#get_fw_info-fn-1]

Request: `[entity_index, 0x00, 0x00]`

Response (byte → field):

| Byte  | Field             | Notes                                                                      |
| ----- | ----------------- | -------------------------------------------------------------------------- |
| 0     | `entity_type`     | `DeviceEntityType` enum value (u8)                                         |
| 1–3   | `firmware_prefix` | 3-byte ASCII prefix of the firmware name                                   |
| 4     | `firmware_number` | Packed BCD u8; decoded automatically                                       |
| 5     | `revision`        | Packed BCD u8; decoded automatically                                       |
| 6–7   | `build`           | Big-endian u16 in packed BCD; decoded automatically                        |
| 8     | `active`          | Bit 0: `true` if this entity is currently active                           |
| 9–10  | `transport_pid`   | Big-endian u16; actual PID for the active entity, may be zero for inactive |
| 11–15 | `extra_version`   | 5-byte optional extra versioning information                               |

### `get_serial_number` (fn 2) [#get_serial_number-fn-2]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field         | Notes                |
| ---- | ------------- | -------------------- |
| 0–11 | serial number | 12-byte UTF-8 string |

Check `DeviceInformationCapabilities::serial_number` before calling; the function is only present in feature version 4 and later.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::device_information::DeviceInformationFeature};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<DeviceInformationFeature>() {
    let info = feat.get_device_info().await?;
    println!("entity_count={}, unit_id={:02x?}", info.entity_count, info.unit_id);

    for i in 0..info.entity_count {
        let fw = feat.get_fw_info(i).await?;
        println!("[{}] {:?} {}{:02}.{:02} build={}", i, fw.entity_type,
            fw.firmware_prefix, fw.firmware_number, fw.revision, fw.build);
    }

    if info.capabilities.serial_number {
        let serial = feat.get_serial_number().await?;
        println!("serial={}", serial);
    }
}
```


# 0x0005 · deviceTypeAndName (/hidpp/features/x0005-device-type-and-name)



Reports the device's **type** (mouse, keyboard, trackball, …) and its
marketing **name**. The name is read in chunks: `getDeviceNameCount()` gives the
length, then `getDeviceName(offset)` returns successive byte ranges.

OpenLogi uses the type to pick the right diagram and the name for the device
carousel label.

> **Spec:** Logitech HID++ 2.0 — *x0005 deviceTypeAndName* (v2).

## Function reference [#function-reference]

The `DeviceTypeAndNameFeature` wrapper (`0x0005`) exposes:

### Methods [#methods]

| Function                | HID++ fn    | Signature     | Returns      |
| ----------------------- | ----------- | ------------- | ------------ |
| `get_device_name_count` | 0           | `()`          | `u8`         |
| `get_device_name`       | 1           | `(index: u8)` | `Vec<u8>`    |
| `get_whole_device_name` | convenience | `()`          | `String`     |
| `get_device_type`       | 2           | `()`          | `DeviceType` |

`get_whole_device_name` is a convenience wrapper that calls `get_device_name_count` (fn 0) and then `get_device_name` (fn 1) in a loop until the full name is assembled.

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `DeviceType` [#devicetype]

Represents the marketing category of a HID++ 2.0 device, as returned by `get_device_type`.

| Variant                  | Value | Description                     |
| ------------------------ | ----- | ------------------------------- |
| `Keyboard`               | 0     | Keyboard device.                |
| `RemoteControl`          | 1     | Remote-control device.          |
| `Numpad`                 | 2     | Numeric keypad device.          |
| `Mouse`                  | 3     | Mouse device.                   |
| `Trackpad`               | 4     | Trackpad device.                |
| `Trackball`              | 5     | Trackball device.               |
| `Presenter`              | 6     | Presenter device.               |
| `Receiver`               | 7     | Receiver device.                |
| `Headset`                | 8     | Headset device.                 |
| `Webcam`                 | 9     | Webcam device.                  |
| `SteeringWheel`          | 10    | Steering wheel device.          |
| `Joystick`               | 11    | Joystick device.                |
| `Gamepad`                | 12    | Gamepad device.                 |
| `Dock`                   | 13    | Dock device.                    |
| `Speaker`                | 14    | Speaker device.                 |
| `Microphone`             | 15    | Microphone device.              |
| `IlluminationLight`      | 16    | Illumination light device.      |
| `ProgrammableController` | 17    | Programmable controller device. |
| `CarSimPedals`           | 18    | Car-simulator pedals device.    |
| `Adapter`                | 19    | Adapter device.                 |

## Wire format [#wire-format]

All three functions use a 3-byte short-report request payload. Responses from `get_device_name_count` and `get_device_type` arrive as a short report and are read via `extend_payload()[0]`; `get_device_name` returns the raw payload bytes (3 bytes from a short report, 16 bytes from a long report, depending on device and channel capability).

### `get_device_name_count` (fn 0) [#get_device_name_count-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field   | Notes                                                  |
| ---- | ------- | ------------------------------------------------------ |
| 0    | `count` | Total number of UTF-8 characters in the marketing name |

### `get_device_name` (fn 1) [#get_device_name-fn-1]

Request: `[index, 0x00, 0x00]`

| Byte | Field   | Notes                                         |
| ---- | ------- | --------------------------------------------- |
| 0    | `index` | Byte offset to start reading from (inclusive) |

Response: the raw payload bytes returned by the device. Up to 3 bytes on a short-report channel or up to 16 bytes on a long-report channel. The caller must accumulate chunks until `count` bytes have been collected, then strip trailing NUL bytes.

### `get_device_type` (fn 2) [#get_device_type-fn-2]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field         | Notes                                            |
| ---- | ------------- | ------------------------------------------------ |
| 0    | `device_type` | `DeviceType` enum discriminant (see table above) |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::feature::device_type_and_name::DeviceTypeAndNameFeature;

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<DeviceTypeAndNameFeature>() {
    // Read the device type (mouse, keyboard, trackball, …)
    let device_type = feat.get_device_type().await?;
    println!("Device type: {:?}", device_type);

    // Read the full marketing name in one convenience call
    let name = feat.get_whole_device_name().await?;
    println!("Device name: {}", name);

    // Or read it manually: get the length, then fetch chunks
    let count = feat.get_device_name_count().await?;
    let chunk = feat.get_device_name(0).await?;
    println!("Name length: {}, first chunk: {:?}", count, chunk);
}
```


# 0x0007 · deviceFriendlyName (/hidpp/features/x0007-device-friendly-name)



The **user-assigned** name for a device: the custom label you can give a mouse
or keyboard, distinct from the fixed marketing name in
[`0x0005` deviceTypeAndName](/hidpp/features/x0005-device-type-and-name).
`getFriendlyNameLength` reports the current, maximum, and default name lengths;
`getFriendlyName` / `getDefaultFriendlyName` read the name in 15-byte chunks (or
the whole-name convenience wrappers); `setFriendlyName` writes it and
`resetFriendlyName` restores the factory default.

OpenLogi reads the friendly name during device discovery and surfaces it when it
differs from the default.

> **Spec:** Logitech HID++ 2.0 — *x0007 deviceFriendlyName*. &#x2A;*Used by:** device
> identification.

## Function reference [#function-reference]

The `DeviceFriendlyNameFeature` wrapper (`0x0007`) exposes:

### Methods [#methods]

| Function                          | HID++ fn | Signature                      | Returns                    |
| --------------------------------- | -------- | ------------------------------ | -------------------------- |
| `get_friendly_name_length`        | 0        | `()`                           | `DeviceFriendlyNameLength` |
| `get_friendly_name`               | 1        | `(index: u8)`                  | `[u8; 15]`                 |
| `get_whole_friendly_name`         | —        | `()`                           | `String`                   |
| `get_default_friendly_name`       | 2        | `(index: u8)`                  | `[u8; 15]`                 |
| `get_whole_default_friendly_name` | —        | `()`                           | `String`                   |
| `set_friendly_name`               | 3        | `(index: u8, chunk: [u8; 15])` | `u8`                       |
| `set_whole_device_name`           | —        | `(name: String)`               | `u8`                       |
| `reset_friendly_name`             | 4        | `()`                           | `u8`                       |

All methods are `async` and return `Result<…, Hidpp20Error>`.

The three rows marked `—` are convenience wrappers that compose multiple HID++ calls internally; they have no single function index.

### Types [#types]

#### `DeviceFriendlyNameLength` [#devicefriendlynamelength]

Represents the length data returned by `get_friendly_name_length`.

| Field                 | Type | Description                                     |
| --------------------- | ---- | ----------------------------------------------- |
| `name_length`         | `u8` | The current length of the friendly device name. |
| `name_max_length`     | `u8` | The maximum length of the friendly device name. |
| `default_name_length` | `u8` | The length of the default friendly device name. |

## Wire format [#wire-format]

Short getters carry a 3-byte request payload and return a 16-byte long payload; `set_friendly_name` uses a 16-byte long request. Byte offsets below are into the feature-level payload (after the HID++ header, feature index, and function/software ID byte).

### `get_friendly_name_length` (fn 0) [#get_friendly_name_length-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                 | Notes                                          |
| ---- | --------------------- | ---------------------------------------------- |
| 0    | `name_length`         | Current UTF-8 byte length of the friendly name |
| 1    | `name_max_length`     | Maximum writable length the device accepts     |
| 2    | `default_name_length` | Byte length of the factory-default name        |

### `get_friendly_name` (fn 1) [#get_friendly_name-fn-1]

Request: `[index, 0x00, 0x00]` — `index` is the byte offset into the name to start reading from.

Response (byte → field):

| Byte | Field            | Notes                                                  |
| ---- | ---------------- | ------------------------------------------------------ |
| 0    | *(echoed index)* | Skipped by the wrapper                                 |
| 1–15 | name chunk       | Up to 15 UTF-8 bytes; unused trailing bytes are `0x00` |

### `get_default_friendly_name` (fn 2) [#get_default_friendly_name-fn-2]

Request: `[index, 0x00, 0x00]` — same offset semantics as fn 1.

Response (byte → field):

| Byte | Field              | Notes                                                  |
| ---- | ------------------ | ------------------------------------------------------ |
| 0    | *(echoed index)*   | Skipped by the wrapper                                 |
| 1–15 | default name chunk | Up to 15 UTF-8 bytes; unused trailing bytes are `0x00` |

### `set_friendly_name` (fn 3) [#set_friendly_name-fn-3]

Uses a **long request** (16-byte payload):

Request: `[index, chunk[0], chunk[1], …, chunk[14]]`

| Byte | Field   | Notes                                                         |
| ---- | ------- | ------------------------------------------------------------- |
| 0    | `index` | Byte offset at which to begin writing                         |
| 1–15 | `chunk` | Exactly 15 bytes; pad with `0x00` for the final partial chunk |

Response (byte → field):

| Byte | Field                 | Notes                                             |
| ---- | --------------------- | ------------------------------------------------- |
| 0    | new total name length | Byte count of the name as stored after this write |

### `reset_friendly_name` (fn 4) [#reset_friendly_name-fn-4]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                   | Notes                                  |
| ---- | ----------------------- | -------------------------------------- |
| 0    | name length after reset | Equals `default_name_length` from fn 0 |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::device_friendly_name::DeviceFriendlyNameFeature};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<DeviceFriendlyNameFeature>() {
    // Read current name lengths
    let lengths = feat.get_friendly_name_length().await?;
    println!("name length: {}, max: {}", lengths.name_length, lengths.name_max_length);

    // Read the full current friendly name in one call
    let name = feat.get_whole_friendly_name().await?;
    println!("friendly name: {name}");

    // Read the factory-default name
    let default_name = feat.get_whole_default_friendly_name().await?;
    println!("default name: {default_name}");

    // Write a new friendly name (truncated automatically to name_max_length)
    let new_len = feat.set_whole_device_name("My MX Master".to_string()).await?;
    println!("name set, new length: {new_len}");

    // Reset back to the factory default
    let reset_len = feat.reset_friendly_name().await?;
    println!("reset, length now: {reset_len}");
}
```


# 0x00c2 · dfuControlSigned (/hidpp/features/x00c2-dfu-control-signed)



A HID++ 2.0 Device Firmware Upgrade control feature that governs how a host
initiates a signed firmware update on mice, keyboards, headsets, and other
Logitech wireless peripherals. It is one of the dfuControl feature variants
(`0x00c0` / `0x00c1` / `0x00c2` / `0x00c3`); this signed variant requires the
device to validate a cryptographic signature before accepting a DFU request,
which gives stronger authenticity guarantees than the unsigned predecessors.
`0x00d0` dfu handles the actual firmware transfer once the device is in DFU
mode.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

It is the step a host must clear before any firmware image can be transferred:

* Requests entry into DFU (Device Firmware Upgrade) mode; the device then
  reboots into its firmware-update bootloader.
* Reports whether DFU mode is available and whether an update is already in
  progress.
* Enforces the signature check: the device accepts only a DFU request carrying
  a valid signature, so arbitrary or tampered firmware cannot be flashed.
* Hands over to `0x00d0` dfu, which streams the firmware payload once the
  device is in DFU mode.

The signature requirement is what separates this variant from `0x00c0` and
`0x00c1`; `0x00c3` dfuControlV3 extends the family further. Entering DFU mode
takes the device offline, so host software has to re-enumerate it after the
bootloader reboots back to normal firmware.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x00c2` · &#x2A;*See also:** 0x00d0 dfu


# 0x00d0 · dfu (/hidpp/features/x00d0-dfu)



A HID++ 2.0 feature that carries the Device Firmware Upgrade (DFU) protocol
itself: the data-transfer phase that streams new firmware images to a device
after it has already entered DFU mode. It applies to the full range of
Logitech HID++ peripherals (mice, keyboards, headsets, receivers, and
touchpads) that support in-field firmware updates. It works with `0x00c2`
dfuControlSigned, which governs the mode transition that precedes the
transfer.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x00c2 dfuControlSigned where applicable.
</Callout>

## What it does [#what-it-does]

* Starts a transfer, naming the target firmware entity (main application,
  bootloader, hardware module) and the expected image size.
* Streams the payload in fixed-size blocks over HID++ reports.
* Reports status and progress: how much of the image the device has accepted,
  and any transfer error.
* Finalises the transfer, upon which the device verifies, commits, and
  restarts on the new firmware.

The feature operates only while the device is in DFU mode (entered via
`0x00c2` dfuControlSigned); it is not available during normal operation.
Images must be cryptographically signed by Logitech; the device rejects
unsigned or tampered payloads before committing them.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x00d0` · &#x2A;*See also:** 0x00c2 dfuControlSigned


# 0x1000 · batteryStatus (/hidpp/features/x1000-battery-status)



The legacy battery feature: it reports a device's charge as a **discharge level**
plus a **charging status**. It predates the consolidated `0x1004`
unifiedBattery, and older mice such as the MX Master 2S expose `0x1000` and never
`0x1004`.

OpenLogi's inventory probe falls back to this feature when the unified one is
absent — the same enhanced-then-legacy pattern SmartShift uses for `0x2111` /
`0x2110` — so a legacy mouse still shows battery in the carousel and in
`openlogi list`.

> **Spec:** Logitech HID++ 2.0 — *x1000 batteryStatus*. &#x2A;*Used by:** battery
> percentage / charge state on legacy devices.

## Function reference [#function-reference]

The `BatteryStatusFeature` wrapper (`0x1000`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature | Returns             |
| -------------------------- | -------- | --------- | ------------------- |
| `get_battery_level_status` | 0        | `()`      | `LegacyBatteryInfo` |

The method is `async` and returns `Result<…, Hidpp20Error>`. The optional
`getBatteryCapability` (function 1) and the broadcast event are not implemented;
neither is needed to display a charge reading.

### Types [#types]

#### `LegacyBatteryInfo` [#legacybatteryinfo]

| Field             | Type                  | Description                                                                                                 |
| ----------------- | --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `discharge_level` | `u8`                  | Current charge as a percentage (`0`–`100`). Firmware reports it in coarse steps, not as a continuous value. |
| `next_level`      | `u8`                  | The next lower discharge step the firmware will report — a granularity hint, unused for display.            |
| `status`          | `LegacyBatteryStatus` | The current charging status.                                                                                |

#### `LegacyBatteryStatus` [#legacybatterystatus]

| Variant          | Value | Description                                                                                                                                            |
| ---------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Discharging`    | 0     | Battery is discharging.                                                                                                                                |
| `Recharging`     | 1     | Battery is recharging.                                                                                                                                 |
| `AlmostFull`     | 2     | Charging and nearly full.                                                                                                                              |
| `Full`           | 3     | Charge complete.                                                                                                                                       |
| `SlowRecharge`   | 4     | Recharging below optimal speed.                                                                                                                        |
| `InvalidBattery` | 5     | The battery type is invalid.                                                                                                                           |
| `ThermalError`   | 6     | The battery subsystem reported a thermal error.                                                                                                        |
| `Other`          | 7     | Other charging error. Kept explicit so a device reporting it surfaces as unknown instead of failing the parse and making the battery indicator vanish. |

## Wire format [#wire-format]

### `get_battery_level_status` (fn 0) [#get_battery_level_status-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field             | Notes                                                          |
| ---- | ----------------- | -------------------------------------------------------------- |
| 0    | `discharge_level` | percentage, in coarse firmware steps                           |
| 1    | `next_level`      | next lower step the firmware will report                       |
| 2    | `status`          | `LegacyBatteryStatus` enum value (0 = Discharging … 7 = Other) |

An unrecognised status byte fails as `Hidpp20Error::UnsupportedResponse` rather
than being guessed at.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::battery_status::BatteryStatusFeature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<BatteryStatusFeature>() {
    let info = feat.get_battery_level_status().await?;
    println!("{}% — {:?}", info.discharge_level, info.status);
}
```

**See also:** [0x1004 unifiedBattery](/hidpp/features/x1004-unified-battery),
[0x1001 batteryVoltage](/hidpp/features/x1001-battery-voltage).


# 0x1001 · batteryVoltage (/hidpp/features/x1001-battery-voltage)



Reports a device's battery as a **measured voltage** plus a charging-flags byte.
G-series wireless gaming devices (G915, G903 LIGHTSPEED, G502 LIGHTSPEED) expose
`0x1001` and neither `0x1000` nor `0x1004`, so without it the inventory probe
finds no battery source for them at all.

Unlike its siblings the feature reports **no percentage**; OpenLogi estimates
one from the voltage when it renders the battery indicator.

<Callout type="info">
  The wire layout is not in a public Logitech spec. The big-endian millivolt
  `u16` followed by one flags byte was reverse-engineered; the decoding follows
  Solaar's `decipher_battery_voltage` and libratbag's consensus on the flag bits.
</Callout>

> **Spec:** reverse-engineered — *x1001 batteryVoltage*. &#x2A;*Used by:** battery
> state on G-series wireless devices.

## Function reference [#function-reference]

The `BatteryVoltageFeature` wrapper (`0x1001`) exposes:

### Methods [#methods]

| Function           | HID++ fn | Signature | Returns              |
| ------------------ | -------- | --------- | -------------------- |
| `get_battery_info` | 0        | `()`      | `VoltageBatteryInfo` |

The method is `async` and returns `Result<…, Hidpp20Error>`. The broadcast event
is not implemented.

### Types [#types]

#### `VoltageBatteryInfo` [#voltagebatteryinfo]

| Field        | Type                    | Description                                                                                                                               |
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `voltage_mv` | `u16`                   | Measured battery voltage in millivolts — roughly `3500` (empty) to `4200` (full) for the single-cell Li-Po batteries these devices carry. |
| `status`     | `VoltageChargingStatus` | Charging state decoded from the flags byte.                                                                                               |
| `critical`   | `bool`                  | The firmware's "charge level critical" marker (flags bit `5`).                                                                            |

#### `VoltageChargingStatus` [#voltagechargingstatus]

| Variant        | Description                                                               |
| -------------- | ------------------------------------------------------------------------- |
| `Discharging`  | Running on battery (bit `7` clear).                                       |
| `Charging`     | Charging at the standard rate.                                            |
| `ChargingFast` | Charging at a raised current (bit `3`).                                   |
| `ChargingSlow` | Charging at reduced current (bit `4`).                                    |
| `Full`         | On external power with charge complete (status bits `0b01`).              |
| `NotCharging`  | On external power but not charging — a charge fault (status bits `0b10`). |

Decoding is total on purpose: a contradictory or future flag combination falls
into the nearest charging bucket rather than failing, so a battery reading never
vanishes over an unknown bit.

## Wire format [#wire-format]

### `get_battery_info` (fn 0) [#get_battery_info-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field        | Notes                         |
| ---- | ------------ | ----------------------------- |
| 0–1  | `voltage_mv` | big-endian `u16`, millivolts  |
| 2    | flags        | charging state, decoded below |

Flags byte:

| Bit(s) | Meaning                                                                                                  |
| ------ | -------------------------------------------------------------------------------------------------------- |
| 7      | External power present. Clear → `Discharging`, and every other bit is meaningless.                       |
| 0–1    | Charge status: `0b01` (or `0b11`) → `Full`, `0b10` → `NotCharging`. Takes precedence over the rate bits. |
| 3      | Fast charging → `ChargingFast`.                                                                          |
| 4      | Slow charging → `ChargingSlow`.                                                                          |
| 5      | `critical` — surfaced independently of the status.                                                       |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::battery_voltage::BatteryVoltageFeature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<BatteryVoltageFeature>() {
    let info = feat.get_battery_info().await?;
    println!("{} mV — {:?} (critical: {})", info.voltage_mv, info.status, info.critical);
}
```

**See also:** [0x1004 unifiedBattery](/hidpp/features/x1004-unified-battery),
[0x1000 batteryStatus](/hidpp/features/x1000-battery-status).


# 0x1004 · unifiedBattery (/hidpp/features/x1004-unified-battery)



The modern, unified battery feature: one call for percentage, a coarse level,
and charge status, replacing the older `0x1000` batteryStatus / `0x1001`
batteryVoltage features. `getBatteryCapabilities` reports what the device can
measure; `getBatteryInfo` returns the charging percentage, a `BatteryLevel`
(full / good / low / critical), and a `BatteryStatus` (charging, discharging,
…). The feature also emits live updates as the device pushes them.

OpenLogi reads it for the battery percentage and charge state shown in the device
carousel and the `list` inventory.

> **Spec:** Logitech HID++ 2.0 — *x1004 unifiedBattery*. &#x2A;*Used by:** battery
> percentage / charge state.

## Function reference [#function-reference]

The `UnifiedBatteryFeature` wrapper (`0x1004`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature | Returns                  |
| -------------------------- | -------- | --------- | ------------------------ |
| `get_battery_capabilities` | 0        | `()`      | `BatteryCapabilities`    |
| `get_battery_info`         | 1        | `()`      | `BatteryInfo`            |
| `listen`                   | event    | `()`      | `Receiver<BatteryEvent>` |

`get_battery_capabilities` and `get_battery_info` are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver directly.

### Types [#types]

#### `BatteryCapabilities` [#batterycapabilities]

Represents the capabilities of this feature and the battery itself.

| Field             | Type                    | Description                                                                                                |
| ----------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `reported_levels` | `HashSet<BatteryLevel>` | All `BatteryLevel` variants the feature supports and reports.                                              |
| `rechargeable`    | `bool`                  | Whether the battery is rechargeable.                                                                       |
| `percentage`      | `bool`                  | Whether the device supports reporting the current charge percentage in `BatteryInfo::charging_percentage`. |

#### `BatteryInfo` [#batteryinfo]

Represents information about the current battery charge.

| Field                 | Type            | Description                                                                                                |
| --------------------- | --------------- | ---------------------------------------------------------------------------------------------------------- |
| `charging_percentage` | `u8`            | The current charge of the battery in percent. Always zero if `BatteryCapabilities::percentage` is `false`. |
| `level`               | `BatteryLevel`  | The current (approximate) level of the battery.                                                            |
| `status`              | `BatteryStatus` | The current charging status of the battery.                                                                |

#### `BatteryLevel` [#batterylevel]

Represents an approximate level of the battery charge.

| Variant    | Value | Description             |
| ---------- | ----- | ----------------------- |
| `Critical` | 1     | Critical battery level. |
| `Low`      | 2     | Low battery level.      |
| `Good`     | 4     | Good battery level.     |
| `Full`     | 8     | Full battery level.     |

#### `BatteryStatus` [#batterystatus]

Represents the charging status of the battery.

| Variant        | Value | Description                          |
| -------------- | ----- | ------------------------------------ |
| `Discharging`  | 0     | Battery is discharging.              |
| `Charging`     | 1     | Battery is charging.                 |
| `ChargingSlow` | 2     | Battery is charging slowly.          |
| `Full`         | 3     | Battery is full.                     |
| `Error`        | 4     | Battery subsystem reported an error. |

### Events [#events]

`UnifiedBatteryFeature` implements `EmittingFeature<BatteryEvent>`. Call `listen()` to obtain a channel receiver that yields `BatteryEvent` values pushed by the device.

#### `BatteryEvent` [#batteryevent]

| Variant      | Payload       | Description                                                       |
| ------------ | ------------- | ----------------------------------------------------------------- |
| `InfoUpdate` | `BatteryInfo` | Emitted whenever the battery information changes. Always enabled. |

## Wire format [#wire-format]

Both request payloads are 3 zero bytes; the device replies with a 16-byte long-format payload and the crate reads the first few bytes via `extend_payload()`.

### `get_battery_capabilities` (fn 0) [#get_battery_capabilities-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte     | Field                                 | Notes                           |
| -------- | ------------------------------------- | ------------------------------- |
| 0, bit 0 | `reported_levels` contains `Critical` | set if `payload[0] & 0x01 != 0` |
| 0, bit 1 | `reported_levels` contains `Low`      | set if `payload[0] & 0x02 != 0` |
| 0, bit 2 | `reported_levels` contains `Good`     | set if `payload[0] & 0x04 != 0` |
| 0, bit 3 | `reported_levels` contains `Full`     | set if `payload[0] & 0x08 != 0` |
| 1, bit 0 | `rechargeable`                        | set if `payload[1] & 0x01 != 0` |
| 1, bit 1 | `percentage`                          | set if `payload[1] & 0x02 != 0` |

### `get_battery_info` (fn 1) [#get_battery_info-fn-1]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                               | Notes                                                                 |
| ---- | ----------------------------------- | --------------------------------------------------------------------- |
| 0    | `charging_percentage`               | raw `u8`; zero when `BatteryCapabilities::percentage` is `false`      |
| 1    | `level`                             | `BatteryLevel` enum value (1 = Critical, 2 = Low, 4 = Good, 8 = Full) |
| 2    | `status`                            | `BatteryStatus` enum value (0 = Discharging … 4 = Error)              |
| 3    | *(external power source indicator)* | not decoded by the crate; see Linux driver notes in source            |

### `InfoUpdate` event (fn 0, event sub-id 0) [#infoupdate-event-fn-0-event-sub-id-0]

The device pushes this event unsolicited whenever battery state changes. The crate's message listener parses it identically to `get_battery_info`:

| Byte | Field                 | Notes                      |
| ---- | --------------------- | -------------------------- |
| 0    | `charging_percentage` | raw `u8`                   |
| 1    | `level`               | `BatteryLevel` enum value  |
| 2    | `status`              | `BatteryStatus` enum value |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::{EmittingFeature, unified_battery::UnifiedBatteryFeature},
};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<UnifiedBatteryFeature>() {
    // Query static capabilities once
    let caps = feat.get_battery_capabilities().await?;
    println!("rechargeable={}, percentage={}", caps.rechargeable, caps.percentage);

    // Poll current state
    let info = feat.get_battery_info().await?;
    println!("{}% — {:?} / {:?}", info.charging_percentage, info.level, info.status);

    // Subscribe to live updates pushed by the device
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("battery event: {:?}", event);
    }
}
```


# 0x1010 · chargingControl (/hidpp/features/x1010-charging-control)



A HID++ 2.0 feature for reading and changing how a rechargeable wireless device
charges. It appears on Logitech mice, keyboards, and headsets with programmable
charging parameters. Where `0x1000` batteryStatus and `0x1004` unifiedBattery
only report state, this feature adds write-side control over the charging
process.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* Queries the supported charging modes and whatever charging parameters are in
  effect.
* Sets charging options, such as a charge-limit mode (sold as "battery care" or
  "smart charging") that caps the maximum charge level to protect battery
  health.
* On some devices, overrides the charge ceiling, for instance allowing a full
  100% charge before travel while normally stopping lower.
* The firmware enforces the stored policy on its own and keeps it across power
  cycles.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1010`


# 0x1802 · deviceReset (/hidpp/features/x1802-device-reset)



A device-management feature that lets host software reboot a HID++ 2.0
peripheral without a physical power cycle. It appears across Logitech's
wireless and wired peripherals: mice, keyboards, headsets, and receivers. The
usual callers are firmware-update workflows and recovery from an inconsistent
configuration state, alongside the DFU family (`0x00C2`, `0x00D0`).

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* Commands a soft reset: the firmware restarts as if the device had been
  power-cycled.
* On some devices, resets into a specific boot target, such as normal firmware
  or a DFU/bootloader mode.
* Some devices also expose a query for reset parameters, so a caller can
  confirm the feature exists before issuing a destructive command.

A reset ends the current HID++ session and drops any configuration that only
lives in volatile memory, so save settings first.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1802`


# 0x1805 · oobState (/hidpp/features/x1805-oob-state)



A HID++ 2.0 device-management feature that tracks and controls whether a device
is in its out-of-box (factory-default) state. It applies broadly across wireless
peripherals — mice, keyboards, and similar devices — and sits in the same
lifecycle-management range as `0x1802` deviceReset and `0x1806`
configDeviceProps. A device leaves the out-of-box state once it has been paired
and configured by a host; this feature lets software query that status and
optionally restore it.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

The out-of-box marker lives in the device's non-volatile memory:

* Queries whether the device is in the out-of-box state, unconfigured as
  shipped.
* Sets or clears the flag, so software or factory tooling can restore
  factory-default condition or mark a device configured.
* Some devices may also reset the per-host or per-pairing state tied to that
  lifecycle.

The audience is mostly Logitech factory and provisioning tooling, but host
software can query it to detect first-run or freshly-reset devices and trigger
an initial-setup flow.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1805`


# 0x1814 · changeHost (/hidpp/features/x1814-change-host)



Multi-host devices expose this feature to query how many host slots / RF channels
they support and to switch between them. `get_host_info` returns a `ChangeHostInfo`
struct containing the **host count**, the **current host index**, and a
`ChangeHostCapabilities` flags field; `set_current_host` sends a fire-and-forget
switch to the requested slot; the device typically resets its connection on a
host switch, so no response is awaited.

Two additional functions manage the per-host **cookie** byte, a small value
stored in the device's non-volatile memory. `get_cookies` returns one byte per
host slot; `set_cookie` writes an individual entry. The firmware clears a slot's
cookie when a new host pairs to it. Enhanced host switching
(`ENHANCED_HOST_SWITCH` capability flag) uses non-zero cookies to select a
fallback host on a failed connection before returning to the original slot.

* **`ChangeHostInfo`** — `host_count`, `current_host`, `capabilities`.
* **`ChangeHostCapabilities::ENHANCED_HOST_SWITCH`** — on a failed connection,
  fall back to a host with a non-zero cookie before returning to the original.

> **Spec:** Logitech HID++ 2.0 — *changeHost*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `ChangeHostFeature` wrapper (`0x1814`) exposes methods for querying and changing the active host slot on multi-host devices, plus per-slot cookie management.

### Methods [#methods]

| Function           | HID++ fn | Signature                | Returns          |
| ------------------ | -------- | ------------------------ | ---------------- |
| `get_host_info`    | 0        | `()`                     | `ChangeHostInfo` |
| `set_current_host` | 1        | `(host: u8)`             | `()`             |
| `get_cookies`      | 2        | `(host_count: u8)`       | `Vec<u8>`        |
| `set_cookie`       | 3        | `(host: u8, cookie: u8)` | `()`             |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `ChangeHostInfo` [#changehostinfo]

Host configuration returned by `get_host_info`.

| Field          | Type                     | Description                             |
| -------------- | ------------------------ | --------------------------------------- |
| `host_count`   | `u8`                     | Number of hosts / RF channels.          |
| `current_host` | `u8`                     | Current host index, in `0..host_count`. |
| `capabilities` | `ChangeHostCapabilities` | Host-switching capabilities.            |

#### `ChangeHostCapabilities` [#changehostcapabilities]

Host-switching capabilities bitflags reported by `get_host_info`.

| Flag                   | Bit/Value | Description                                                                                                                                                    |
| ---------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENHANCED_HOST_SWITCH` | `1 << 0`  | Enhanced host switching is enabled: on a failed connection the device falls back to another host with a non-zero cookie before returning to the original host. |

## Wire format [#wire-format]

Short 3-byte request payloads are used for all functions; responses are read from the 16-byte long payload via `extend_payload()`.

### `get_host_info` (fn 0) [#get_host_info-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field          | Notes                                                             |
| ---- | -------------- | ----------------------------------------------------------------- |
| 0    | `host_count`   | Total number of host slots / RF channels                          |
| 1    | `current_host` | Active host index, zero-based                                     |
| 2    | `capabilities` | `ChangeHostCapabilities` bitflags; bit 0 = `ENHANCED_HOST_SWITCH` |

### `set_current_host` (fn 1) [#set_current_host-fn-1]

Sent as a **notify** (fire-and-forget); no response is read.

Request: `[host, 0x00, 0x00]`

| Byte | Field  | Notes                         |
| ---- | ------ | ----------------------------- |
| 0    | `host` | Target host index to activate |
| 1–2  | —      | Padding, always `0x00`        |

### `get_cookies` (fn 2) [#get_cookies-fn-2]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response: bytes `payload[0..host_count]` — one cookie byte per host slot in order. The caller supplies `host_count` (from `get_host_info`) to slice the response; bytes beyond `host_count` are ignored.

| Byte | Field               | Notes                    |
| ---- | ------------------- | ------------------------ |
| 0    | cookie for host 0   |                          |
| 1    | cookie for host 1   |                          |
| …    | cookie for host N-1 | Up to `host_count` bytes |

### `set_cookie` (fn 3) [#set_cookie-fn-3]

Request: `[host, cookie, 0x00]`

| Byte | Field    | Notes                                         |
| ---- | -------- | --------------------------------------------- |
| 0    | `host`   | Host slot index whose cookie is being written |
| 1    | `cookie` | Arbitrary byte value to store in NVM          |
| 2    | —        | Padding, always `0x00`                        |

The device ACK response is awaited but its payload is discarded.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::feature::change_host::ChangeHostFeature;

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ChangeHostFeature>() {
    // Query host count, current host, and capabilities
    let info = feat.get_host_info().await?;
    println!("hosts: {}, current: {}", info.host_count, info.current_host);

    // Read the per-host cookies (one byte per slot)
    let cookies = feat.get_cookies(info.host_count).await?;
    println!("cookies: {:?}", cookies);

    // Switch to host 1 (fire-and-forget; device will reset its connection)
    feat.set_current_host(1).await?;
}
```


# 0x1815 · hostsInfo (/hidpp/features/x1815-hosts-info)



Multi-host management for Bolt / BLE / eQuad devices that can pair to several
host computers simultaneously. `getFeatureInfo` (function 0) returns the total
host-slot count, the currently active slot, and two capability bitmasks.
`getHostInfo` (function 1) queries the pairing status, bus type, and
friendly-name metadata for any individual slot. `getHostDescriptor` (function 2)
reads a single raw descriptor page for the slot's transport connection.

* **`HostIndex`** — `Current` (device-selected, wire value `0xFF`) or `Slot(n)` (zero-based).
* **`HostSlotStatus`** — `Empty` or `Paired`.
* **`HostBusType`** — `Equad`, `Usb`, `Bt`, `Ble`, `BlePro` (Logi Bolt), or `Undefined`.
* **`HostsInfoCapabilities`** — bitflags advertising `GET_NAME`, `SET_NAME`, `MOVE_HOST`, `DELETE_HOST`, `SET_OS_VERSION` support on this device.
* **`HostDescriptorCapabilities`** — bitflags for the supported descriptor families: `EQUAD`, `USB`, `BT`, `BLE`.

> **Spec:** Logitech HID++ 2.0 — *hostsInfo*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `HostsInfoFeature` wrapper (`0x1815`) exposes:

### Methods [#methods]

| Function              | HID++ fn | Signature                     | Returns                |
| --------------------- | -------- | ----------------------------- | ---------------------- |
| `get_feature_info`    | 0        | `()`                          | `HostsInfoFeatureInfo` |
| `get_host_info`       | 1        | `(host: HostIndex)`           | `HostInfo`             |
| `get_host_descriptor` | 2        | `(host: HostIndex, page: u8)` | `HostDescriptorPage`   |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `HostsInfoFeatureInfo` [#hostsinfofeatureinfo]

Static information about the `HostsInfo` feature.

| Field                     | Type                         | Description                   |
| ------------------------- | ---------------------------- | ----------------------------- |
| `capabilities`            | `HostsInfoCapabilities`      | Host-management capabilities. |
| `descriptor_capabilities` | `HostDescriptorCapabilities` | Host descriptor capabilities. |
| `host_count`              | `u8`                         | Number of host slots.         |
| `current_host`            | `HostIndex`                  | Current host slot index.      |

#### `HostInfo` [#hostinfo]

Information about one host slot.

| Field          | Type             | Description                             |
| -------------- | ---------------- | --------------------------------------- |
| `host_index`   | `HostIndex`      | Host slot index returned by the device. |
| `status`       | `HostSlotStatus` | Pairing status.                         |
| `bus_type`     | `HostBusType`    | Bus type used by this host slot.        |
| `page_count`   | `u8`             | Number of descriptor pages.             |
| `name_len`     | `u8`             | Current friendly-name length.           |
| `name_max_len` | `u8`             | Maximum friendly-name length.           |

#### `HostDescriptorPage` [#hostdescriptorpage]

Raw host descriptor page.

| Field        | Type          | Description                                                   |
| ------------ | ------------- | ------------------------------------------------------------- |
| `host_index` | `HostIndex`   | Host slot index returned by the device.                       |
| `bus_type`   | `HostBusType` | Descriptor bus type, decoded from the page header when known. |
| `page_index` | `u8`          | Descriptor page index, decoded from the page header.          |
| `body`       | `[u8; 14]`    | Raw descriptor body bytes.                                    |

#### `HostIndex` [#hostindex]

A host slot selector.

| Variant    | Value  | Description                                     |
| ---------- | ------ | ----------------------------------------------- |
| `Current`  | `0xFF` | The host slot currently selected by the device. |
| `Slot(u8)` | 0–254  | A zero-based host slot index.                   |

#### `HostSlotStatus` [#hostslotstatus]

Pairing status for a host slot.

| Variant  | Value | Description              |
| -------- | ----- | ------------------------ |
| `Empty`  | 0     | The host slot is empty.  |
| `Paired` | 1     | The host slot is paired. |

#### `HostBusType` [#hostbustype]

Bus type associated with a host slot.

| Variant     | Value | Description                    |
| ----------- | ----- | ------------------------------ |
| `Undefined` | 0     | Undefined or unknown bus type. |
| `Equad`     | 1     | eQuad wireless.                |
| `Usb`       | 2     | USB.                           |
| `Bt`        | 3     | Bluetooth classic.             |
| `Ble`       | 4     | Bluetooth Low Energy.          |
| `BlePro`    | 5     | BLE Pro / Logi Bolt.           |

#### `HostsInfoCapabilities` [#hostsinfocapabilities]

Host-management capabilities.

| Flag             | Bit/Value | Description                      |
| ---------------- | --------- | -------------------------------- |
| `GET_NAME`       | `1 << 0`  | Host names can be read.          |
| `SET_NAME`       | `1 << 1`  | Host names can be written.       |
| `MOVE_HOST`      | `1 << 2`  | Host slots can be moved.         |
| `DELETE_HOST`    | `1 << 3`  | Host slots can be deleted.       |
| `SET_OS_VERSION` | `1 << 4`  | Host OS versions can be written. |

#### `HostDescriptorCapabilities` [#hostdescriptorcapabilities]

Supported host descriptor families.

| Flag    | Bit/Value | Description                                          |
| ------- | --------- | ---------------------------------------------------- |
| `EQUAD` | `1 << 0`  | eQuad host descriptors are available.                |
| `USB`   | `1 << 1`  | USB host descriptors are available.                  |
| `BT`    | `1 << 2`  | Bluetooth classic host descriptors are available.    |
| `BLE`   | `1 << 3`  | Bluetooth Low Energy host descriptors are available. |

## Wire format [#wire-format]

All three functions use a 3-byte request payload and return a 16-byte long response payload (accessed via `extend_payload()`).

### `get_feature_info` (fn 0) [#get_feature_info-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                     | Notes                                                                                                                                 |
| ---- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | `capabilities`            | `HostsInfoCapabilities` bitflags (`GET_NAME`=bit 0, `SET_NAME`=bit 1, `MOVE_HOST`=bit 2, `DELETE_HOST`=bit 3, `SET_OS_VERSION`=bit 4) |
| 1    | `descriptor_capabilities` | `HostDescriptorCapabilities` bitflags (`EQUAD`=bit 0, `USB`=bit 1, `BT`=bit 2, `BLE`=bit 3)                                           |
| 2    | `host_count`              | Total number of host slots                                                                                                            |
| 3    | `current_host`            | `0xFF` → `HostIndex::Current`; any other value → `HostIndex::Slot(n)`                                                                 |

### `get_host_info` (fn 1) [#get_host_info-fn-1]

Request: `[host, 0x00, 0x00]` — byte 0 = `HostIndex` as u8 (`0xFF` for `Current`, slot index otherwise)

Response (byte → field):

| Byte | Field          | Notes                                                                                      |
| ---- | -------------- | ------------------------------------------------------------------------------------------ |
| 0    | `host_index`   | Echo of the addressed slot (`0xFF` → `HostIndex::Current`)                                 |
| 1    | `status`       | `HostSlotStatus` — `0` = `Empty`, `1` = `Paired`                                           |
| 2    | `bus_type`     | `HostBusType` — `0`=`Undefined`, `1`=`Equad`, `2`=`Usb`, `3`=`Bt`, `4`=`Ble`, `5`=`BlePro` |
| 3    | `page_count`   | Number of descriptor pages                                                                 |
| 4    | `name_len`     | Current friendly-name length in bytes                                                      |
| 5    | `name_max_len` | Maximum friendly-name length in bytes                                                      |

### `get_host_descriptor` (fn 2) [#get_host_descriptor-fn-2]

Request: `[host, page, 0x00]` — byte 0 = `HostIndex` as u8, byte 1 = descriptor page index

Response (byte → field):

| Byte | Field        | Notes                                                                              |
| ---- | ------------ | ---------------------------------------------------------------------------------- |
| 0    | `host_index` | Echo of the addressed slot                                                         |
| 1    | page header  | Bits 7–4 (`>> 4`) = `bus_type` (`HostBusType`); bits 3–0 (`& 0x0F`) = `page_index` |
| 2–15 | `body`       | 14 raw descriptor bytes (`payload[2..16]`)                                         |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::hosts_info::{HostsInfoFeature, HostIndex}};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<HostsInfoFeature>() {
    let info = feat.get_feature_info().await?;
    println!("slots: {}, current: {:?}", info.host_count, info.current_host);

    for slot in 0..info.host_count {
        let host = feat.get_host_info(HostIndex::Slot(slot)).await?;
        println!("slot {slot}: {:?} via {:?}", host.status, host.bus_type);

        for page in 0..host.page_count {
            let desc = feat.get_host_descriptor(HostIndex::Slot(slot), page).await?;
            println!("  page {}: bus={:?} body={:?}", desc.page_index, desc.bus_type, desc.body);
        }
    }
}
```


# 0x1981 · backlight (/hidpp/features/x1981-backlight1)



`0x1981` is the first-generation HID++ 2.0 backlight feature for keyboards. It gives the host control over the keyboard backlight's on/off state and brightness level, and is an earlier, simpler design than its successors `0x1982` backlight2 and `0x1983` backlight3. Devices that expose `0x1981` are typically older Logitech wireless keyboards; newer products report `0x1982` or `0x1983` instead.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x1982 backlight2 where applicable.
</Callout>

## What it does [#what-it-does]

* **Query backlight state** — read whether the backlight is currently enabled and at what brightness level.
* **Set backlight state** — enable or disable the backlight and select a brightness step from the device's supported range.
* **Query device capabilities** — discover the number of brightness levels the hardware supports and whether automatic (ambient-light sensor) mode is available.
* **Receive change notifications** — some devices emit an unsolicited event when the user adjusts the backlight directly via keyboard hardware keys.

Because this is a first-generation feature, it does not include the richer effect engine (breathing, waves, reaction), advanced fade-out timers, or writable option flags found in `0x1982`. It focuses solely on on/off control and discrete brightness steps.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1981` · &#x2A;*See also:** 0x1982 backlight2


# 0x1982 · backlight (/hidpp/features/x1982-backlight)



Keyboard backlight control, implemented at version 3. `get_backlight_config` (function 0) reads the full persistent configuration: enabled state, active `BacklightMode`, supported and enabled `BacklightOptions`, supported effects, manual brightness level (`0`–`7`), and three fade-out durations in 5-second units (hands-out, hands-in, externally-powered). `set_backlight_config` (function 1) writes those values back to non-volatile memory. `get_backlight_info` (function 2) returns live status plus out-of-box (factory-default) durations. `set_backlight_effect` (function 3) applies an effect temporarily, stored in RAM only, not persisted.

* **`BacklightMode`** — `None` (no mode selected, wire value 0), `Automatic` (level follows the ambient-light sensor), `TemporaryManual` (adjusted by the physical backlight keys; cannot be set by software), `PermanentManual` (set by software).
* **`BacklightEffect`** — `Static`, `None`, `Breathing`, `Contrast`, `Reaction`, `Random`, `Waves`; the `BacklightEffectList` bitfield reports which effects the device supports.
* **`BacklightOptions`** — writable flags (`WOW`, `CROWN`, `PWR_SAVE`) plus read-only capability bits that report which modes and options the device supports.
* **`BacklightStatus`** (from `get_backlight_info`) — `DisabledBySoftware`, `DisabledByCriticalBattery`, `AlsAutomatic`, `AlsSaturated`, `TemporaryManual`, `PermanentManual`.
* **`BacklightEvent::InfoChanged`** — pushed whenever the user adjusts the backlight; carries `BacklightInfoUpdate` (nb\_levels, current\_level, status, effect).

> **Spec:** Logitech HID++ 2.0 — *backlight*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `BacklightFeature` wrapper (`0x1982`) exposes:

### Methods [#methods]

| Function               | HID++ fn | Signature                      | Returns                    |
| ---------------------- | -------- | ------------------------------ | -------------------------- |
| `get_backlight_config` | 0        | `()`                           | `BacklightConfig`          |
| `set_backlight_config` | 1        | `(config: SetBacklightConfig)` | `()`                       |
| `get_backlight_info`   | 2        | `()`                           | `BacklightInfo`            |
| `set_backlight_effect` | 3        | `(effect: BacklightEffect)`    | `()`                       |
| `listen`               | event    | `()`                           | `Receiver<BacklightEvent>` |

All async methods return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver.

### Types [#types]

#### `BacklightConfig` [#backlightconfig]

Configuration returned by `get_backlight_config`.

| Field                | Type                  | Description                                                                                       |
| -------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| `enabled`            | `bool`                | Whether the backlight system is enabled.                                                          |
| `options`            | `BacklightOptions`    | Enabled options and supported capabilities.                                                       |
| `mode`               | `BacklightMode`       | Currently selected backlight mode.                                                                |
| `effect_list`        | `BacklightEffectList` | Effects the device supports.                                                                      |
| `current_level`      | `u8`                  | Current manual brightness level (`0` = off, up to `7`).                                           |
| `duration_hands_out` | `u16`                 | Fade-out duration after the last keystroke with no proximity, in 5-second units (`1`..=`0x05a0`). |
| `duration_hands_in`  | `u16`                 | Fade-out duration while hands remain in the detection zone, in 5-second units.                    |
| `duration_powered`   | `u16`                 | Fade-out duration while externally powered, in 5-second units.                                    |

#### `SetBacklightConfig` [#setbacklightconfig]

Configuration written by `set_backlight_config`.

| Field                | Type                      | Description                                                                                                   |
| -------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `enabled`            | `bool`                    | Whether to enable the backlight system.                                                                       |
| `options`            | `BacklightOptions`        | Options to enable. Only `WOW`, `CROWN`, and `PWR_SAVE` are writable; the device discards unsupported options. |
| `mode`               | `BacklightMode`           | Mode to select. `TemporaryManual` cannot be set by software.                                                  |
| `effect`             | `Option<BacklightEffect>` | Effect to apply, or `None` to leave the current effect unchanged.                                             |
| `current_level`      | `u8`                      | Manual brightness level (`0` = off, up to `7`).                                                               |
| `duration_hands_out` | `u16`                     | Fade-out duration after the last keystroke with no proximity, in 5-second units.                              |
| `duration_hands_in`  | `u16`                     | Fade-out duration while hands remain in the detection zone, in 5-second units.                                |
| `duration_powered`   | `u16`                     | Fade-out duration while externally powered, in 5-second units.                                                |

#### `BacklightInfo` [#backlightinfo]

Live status and factory defaults returned by `get_backlight_info`.

| Field                    | Type              | Description                                                               |
| ------------------------ | ----------------- | ------------------------------------------------------------------------- |
| `nb_levels`              | `u8`              | Number of user-selectable intensity levels (`0`..`nb_levels`).            |
| `current_level`          | `u8`              | Current intensity level.                                                  |
| `status`                 | `BacklightStatus` | Current backlight status.                                                 |
| `effect`                 | `BacklightEffect` | Currently applied effect.                                                 |
| `oob_duration_hands_out` | `u16`             | Out-of-box fade-out duration with hands out, in 5-second units.           |
| `oob_duration_hands_in`  | `u16`             | Out-of-box fade-out duration with hands in, in 5-second units.            |
| `oob_duration_powered`   | `u16`             | Out-of-box fade-out duration while externally powered, in 5-second units. |

#### `BacklightMode` [#backlightmode]

The backlight level-adjustment mode.

| Variant           | Value | Description                                                                              |
| ----------------- | ----- | ---------------------------------------------------------------------------------------- |
| `None`            | 0     | No mode selected.                                                                        |
| `Automatic`       | 1     | Automatic mode: level follows the ambient-light sensor.                                  |
| `TemporaryManual` | 2     | Temporary manual mode: level adjusted via the backlight keys. Cannot be set by software. |
| `PermanentManual` | 3     | Permanent manual mode: level adjusted by software.                                       |

#### `BacklightEffect` [#backlighteffect]

A predefined backlight effect.

| Variant     | Value | Description                    |
| ----------- | ----- | ------------------------------ |
| `Static`    | 0     | The "static" effect (default). |
| `None`      | 1     | The "none" effect.             |
| `Breathing` | 2     | The "breathing light" effect.  |
| `Contrast`  | 3     | The "contrast" effect.         |
| `Reaction`  | 4     | The "reaction" effect.         |
| `Random`    | 5     | The "random" effect.           |
| `Waves`     | 6     | The "waves" effect.            |

#### `BacklightStatus` [#backlightstatus]

The current backlight operational status.

| Variant                     | Value | Description                                       |
| --------------------------- | ----- | ------------------------------------------------- |
| `DisabledBySoftware`        | 0     | Disabled by software.                             |
| `DisabledByCriticalBattery` | 1     | Disabled because the battery is critically low.   |
| `AlsAutomatic`              | 2     | Automatic (ALS) mode.                             |
| `AlsSaturated`              | 3     | Automatic mode, saturated — the backlight is off. |
| `TemporaryManual`           | 4     | Temporary manual mode (set by hardware).          |
| `PermanentManual`           | 5     | Permanent manual mode (set by software).          |

#### `BacklightOptions` [#backlightoptions]

Backlight options and device capability bits from `get_backlight_config`. Low bits are active options; high bits are read-only capability flags. The 2-bit mode field is exposed separately as `BacklightMode`.

| Flag                    | Bit/Value | Description                                                    |
| ----------------------- | --------- | -------------------------------------------------------------- |
| `WOW`                   | `1 << 0`  | The "wow" power-on effect is enabled.                          |
| `CROWN`                 | `1 << 1`  | The "crown" touch effect is enabled.                           |
| `PWR_SAVE`              | `1 << 2`  | Power-save (disable backlight at critical battery) is enabled. |
| `WOW_SUPPORTED`         | `1 << 8`  | The device supports the "wow" effect.                          |
| `CROWN_SUPPORTED`       | `1 << 9`  | The device supports the "crown" effect.                        |
| `PWR_SAVE_SUPPORTED`    | `1 << 10` | The device supports power-save.                                |
| `AUTO_MODE_SUPPORTED`   | `1 << 11` | The device supports automatic (ALS) mode.                      |
| `TEMP_MANUAL_SUPPORTED` | `1 << 12` | The device supports temporary-manual mode.                     |
| `PERM_MANUAL_SUPPORTED` | `1 << 13` | The device supports permanent-manual mode.                     |

#### `BacklightEffectList` [#backlighteffectlist]

Bitmask of predefined effects the device supports, from `get_backlight_config`.

| Flag        | Bit/Value | Description                   |
| ----------- | --------- | ----------------------------- |
| `STATIC`    | `1 << 0`  | The "static" effect.          |
| `NONE`      | `1 << 1`  | The "none" effect.            |
| `BREATHING` | `1 << 2`  | The "breathing light" effect. |
| `CONTRAST`  | `1 << 3`  | The "contrast" effect.        |
| `REACTION`  | `1 << 4`  | The "reaction" effect.        |
| `RANDOM`    | `1 << 5`  | The "random" effect.          |
| `WAVES`     | `1 << 6`  | The "waves" effect.           |

### Events [#events]

The feature implements `EmittingFeature<BacklightEvent>`; call `listen()` to receive a `Receiver<BacklightEvent>`.

| Variant       | Payload               | Description                                      |
| ------------- | --------------------- | ------------------------------------------------ |
| `InfoChanged` | `BacklightInfoUpdate` | Emitted whenever the user adjusts the backlight. |

#### `BacklightInfoUpdate` [#backlightinfoupdate]

Payload carried by `BacklightEvent::InfoChanged`.

| Field           | Type              | Description                                 |
| --------------- | ----------------- | ------------------------------------------- |
| `nb_levels`     | `u8`              | Number of user-selectable intensity levels. |
| `current_level` | `u8`              | Current intensity level.                    |
| `status`        | `BacklightStatus` | Current backlight status.                   |
| `effect`        | `BacklightEffect` | Currently applied effect.                   |

## Wire format [#wire-format]

Getter requests carry a 3-byte zero payload; the setter uses a 16-byte long payload. All multi-byte fields are little-endian. All methods return `Result<_, Hidpp20Error>`.

### `get_backlight_config` (fn 0) [#get_backlight_config-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (`extend_payload()`, 16 bytes):

| Byte  | Field                | Notes                                                                                                        |
| ----- | -------------------- | ------------------------------------------------------------------------------------------------------------ |
| 0     | `enabled`            | Bit 0 set = enabled                                                                                          |
| 1–2   | `options` \| `mode`  | LE u16; bits 3–4 = `BacklightMode` (extracted as `(raw & 0x0018) >> 3`); remaining bits = `BacklightOptions` |
| 3–4   | `effect_list`        | LE u16 `BacklightEffectList`                                                                                 |
| 5     | `current_level`      | Manual brightness `0`–`7`                                                                                    |
| 6–7   | `duration_hands_out` | LE u16, 5-second units                                                                                       |
| 8–9   | `duration_hands_in`  | LE u16, 5-second units                                                                                       |
| 10–11 | `duration_powered`   | LE u16, 5-second units                                                                                       |
| 12–15 | —                    | Unused                                                                                                       |

### `set_backlight_config` (fn 1) [#set_backlight_config-fn-1]

Request (`call_long`, 16-byte payload):

| Byte  | Field                | Notes                                                                                            |
| ----- | -------------------- | ------------------------------------------------------------------------------------------------ |
| 0     | `enabled`            | `0` = disable, `1` = enable                                                                      |
| 1     | options\_byte        | `(writable_flags & 0x07) \| (mode << 3)`; writable flags = `WOW \| CROWN \| PWR_SAVE` (bits 0–2) |
| 2     | `effect`             | `BacklightEffect` value, or `0xff` when `effect` is `None` (leave unchanged)                     |
| 3     | `current_level`      | Manual brightness `0`–`7`                                                                        |
| 4–5   | `duration_hands_out` | LE u16, 5-second units                                                                           |
| 6–7   | `duration_hands_in`  | LE u16, 5-second units                                                                           |
| 8–9   | `duration_powered`   | LE u16, 5-second units                                                                           |
| 10–15 | —                    | Zeroed                                                                                           |

Response: no meaningful return bytes (acknowledgement only).

### `get_backlight_info` (fn 2) [#get_backlight_info-fn-2]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (`extend_payload()`, 16 bytes):

| Byte  | Field                    | Notes                        |
| ----- | ------------------------ | ---------------------------- |
| 0     | `nb_levels`              | Total selectable levels      |
| 1     | `current_level`          | Active level                 |
| 2     | `status`                 | `BacklightStatus` enum value |
| 3     | `effect`                 | `BacklightEffect` enum value |
| 4–5   | `oob_duration_hands_out` | LE u16, 5-second units       |
| 6–7   | `oob_duration_hands_in`  | LE u16, 5-second units       |
| 8–9   | `oob_duration_powered`   | LE u16, 5-second units       |
| 10–15 | —                        | Unused                       |

### `set_backlight_effect` (fn 3) [#set_backlight_effect-fn-3]

Request: `[effect, 0x00, 0x00]` — byte 0 = `BacklightEffect` value.

Response: no meaningful return bytes.

### Event: `backlightInfoEvent` (fn 0, sub-id 0) [#event-backlightinfoevent-fn-0-sub-id-0]

Emitted by the device whenever the user adjusts the backlight. The 16-byte event payload is parsed as `BacklightInfoUpdate`:

| Byte | Field           | Notes                         |
| ---- | --------------- | ----------------------------- |
| 0    | `nb_levels`     | Total selectable levels       |
| 1    | `current_level` | Active level after the change |
| 2    | `status`        | `BacklightStatus` enum value  |
| 3    | `effect`        | `BacklightEffect` enum value  |
| 4–15 | —               | Unused                        |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::backlight::{BacklightEffect, BacklightFeature, BacklightMode, SetBacklightConfig},
};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<BacklightFeature>() {
    // Read the current persistent configuration.
    let config = feat.get_backlight_config().await?;
    println!("enabled={}, level={}", config.enabled, config.current_level);

    // Write a new configuration: enable at level 4, permanent-manual mode.
    feat.set_backlight_config(SetBacklightConfig {
        enabled: true,
        options: config.options,
        mode: BacklightMode::PermanentManual,
        effect: Some(BacklightEffect::Breathing),
        current_level: 4,
        duration_hands_out: config.duration_hands_out,
        duration_hands_in: config.duration_hands_in,
        duration_powered: config.duration_powered,
    })
    .await?;

    // Apply an effect temporarily (RAM only, not persisted).
    feat.set_backlight_effect(BacklightEffect::Static).await?;

    // Listen for hardware-generated backlight changes.
    let rx = feat.listen();
    if let Ok(event) = rx.recv().await {
        println!("backlight event: {:?}", event);
    }
}
```


# 0x1990 · illumination (/hidpp/features/x1990-illumination)



Controls the illumination light of a device, with independent `brightness` (in Lumens) and `color_temperature` (in Kelvin) axes. Both axes share the same shape: an info query (`get_{brightness,color_temperature}_info` → `ControlInfo`), a current-value getter/setter, and a paginated level-list getter/setter. `get_illumination` / `set_illumination` toggle the light on or off as a whole.

Feature version 1 adds `get_brightness_effective_max` — a dynamic ceiling that may be lower than the static maximum — plus two events: `BrightnessEffectiveMaxChanged` and `BrightnessClamped` (carrying a `BrightnessClampedSource` that identifies whether a HID++ request or a hardware button triggered the clamp).

* **`ControlInfo`** — `capabilities` flags (`HAS_EVENTS`, `HAS_LINEAR_LEVELS`, `HAS_NON_LINEAR_LEVELS`, `HAS_DYNAMIC_MAXIMUM`), `min`, `max`, `resolution`, `max_levels`.
* **`LevelConfig`** — `Linear { min, max, step }` or `NonLinear { start_index, level_count, values }`.
* **`IlluminationState`** — `On` / `Off`.
* **Events** — `IlluminationChanged`, `BrightnessChanged`, `ColorTemperatureChanged`, `BrightnessEffectiveMaxChanged`, `BrightnessClamped`.

> **Spec:** Logitech HID++ 2.0 — *illumination*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `IlluminationFeature` wrapper (`0x1990`) exposes:

### Methods [#methods]

| Function                       | HID++ fn | Signature                    | Returns                       |
| ------------------------------ | -------- | ---------------------------- | ----------------------------- |
| `get_illumination`             | 0        | `()`                         | `IlluminationState`           |
| `set_illumination`             | 1        | `(state: IlluminationState)` | `()`                          |
| `get_brightness_info`          | 2        | `()`                         | `ControlInfo`                 |
| `get_brightness`               | 3        | `()`                         | `u16`                         |
| `set_brightness`               | 4        | `(brightness: u16)`          | `()`                          |
| `get_brightness_levels`        | 5        | `(start_index: u8)`          | `LevelConfig`                 |
| `set_brightness_levels`        | 6        | `(levels: &SetLevels)`       | `()`                          |
| `get_color_temperature_info`   | 7        | `()`                         | `ControlInfo`                 |
| `get_color_temperature`        | 8        | `()`                         | `u16`                         |
| `set_color_temperature`        | 9        | `(color_temperature: u16)`   | `()`                          |
| `get_color_temperature_levels` | 10       | `(start_index: u8)`          | `LevelConfig`                 |
| `set_color_temperature_levels` | 11       | `(levels: &SetLevels)`       | `()`                          |
| `get_brightness_effective_max` | 12       | `()`                         | `u16`                         |
| `listen`                       | event    | `()`                         | `Receiver<IlluminationEvent>` |

All async methods return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver.

### Types [#types]

#### `ControlCapabilities` [#controlcapabilities]

Capabilities of an illumination control (brightness or color temperature), from `get_brightness_info` / `get_color_temperature_info`.

| Flag                    | Bit | Description                                                    |
| ----------------------- | --- | -------------------------------------------------------------- |
| `HAS_EVENTS`            | 0   | The control emits change events.                               |
| `HAS_LINEAR_LEVELS`     | 1   | The control supports linear (min/max/step) levels.             |
| `HAS_NON_LINEAR_LEVELS` | 2   | The control supports an explicit list of non-linear levels.    |
| `HAS_DYNAMIC_MAXIMUM`   | 3   | The control has a dynamic effective maximum (brightness only). |

#### `ControlInfo` [#controlinfo]

Capabilities and range of an illumination control. Values are in Lumens for brightness and Kelvin for color temperature.

| Field          | Type                  | Description                                                                                           |
| -------------- | --------------------- | ----------------------------------------------------------------------------------------------------- |
| `capabilities` | `ControlCapabilities` | Control capabilities.                                                                                 |
| `min`          | `u16`                 | Minimum value. When `min == max` only one setting exists and the corresponding setter is unsupported. |
| `max`          | `u16`                 | Maximum value.                                                                                        |
| `resolution`   | `u16`                 | Resolution: valid values satisfy `(value - min) % resolution == 0`.                                   |
| `max_levels`   | `u8`                  | Maximum number of non-linear levels (`0` if non-linear levels are unsupported).                       |

#### `LevelConfig` [#levelconfig]

The level configuration of an illumination control, returned by `get_brightness_levels` and `get_color_temperature_levels`.

| Variant     | Fields                                               | Description                                                            |
| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------- |
| `Linear`    | `min: u16, max: u16, step: u16`                      | Evenly spaced levels from `min` to `max` inclusive in steps of `step`. |
| `NonLinear` | `start_index: u8, level_count: u8, values: Vec<u16>` | An explicit list of level values; `1..=7` values per page.             |

#### `SetLevels` [#setlevels]

A level configuration to write with `set_brightness_levels` / `set_color_temperature_levels`.

| Variant     | Fields                                               | Description                                                                                 |
| ----------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `Reset`     | —                                                    | Reset the level configuration to the factory defaults.                                      |
| `Linear`    | `min: u16, max: u16, step: u16`                      | Configure evenly spaced linear levels.                                                      |
| `NonLinear` | `start_index: u8, level_count: u8, values: Vec<u16>` | Configure an explicit list of non-linear levels (`1..=7` values, monotonically increasing). |

#### `IlluminationState` [#illuminationstate]

On/off state of the illumination.

| Variant | Value | Description          |
| ------- | ----- | -------------------- |
| `Off`   | 0     | Illumination is off. |
| `On`    | 1     | Illumination is on.  |

#### `BrightnessClampedSource` [#brightnessclampedsource]

What caused a brightness clamp event.

| Variant       | Value | Description                                          |
| ------------- | ----- | ---------------------------------------------------- |
| `Unknown`     | 0     | The source is unknown.                               |
| `HidPlusPlus` | 1     | A HID++ `setBrightness` request triggered the clamp. |
| `Button`      | 2     | A hardware button triggered the clamp.               |

### Events [#events]

`IlluminationFeature` implements `EmittingFeature<IlluminationEvent>` and broadcasts variants via `listen()`:

| Variant                         | Payload                                              | Description                                                                            |
| ------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `IlluminationChanged`           | `IlluminationState`                                  | The on/off illumination state changed.                                                 |
| `BrightnessChanged`             | `u16` (Lumens)                                       | The brightness changed.                                                                |
| `ColorTemperatureChanged`       | `u16` (Kelvin)                                       | The color temperature changed.                                                         |
| `BrightnessEffectiveMaxChanged` | `u16` (Lumens; `0` = no limit)                       | The effective maximum brightness changed. Requires feature version 1.                  |
| `BrightnessClamped`             | `source: BrightnessClampedSource`, `brightness: u16` | A brightness request was clamped to the effective maximum. Requires feature version 1. |

## Wire format [#wire-format]

Requests use a 3-byte short payload unless noted; responses are parsed from the 16-byte extended payload. All multi-byte fields are big-endian.

### `get_illumination` (fn 0) [#get_illumination-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field   | Notes                      |
| ---- | ------- | -------------------------- |
| 0    | `state` | Bit 0: `1` = On, `0` = Off |

### `set_illumination` (fn 1) [#set_illumination-fn-1]

Request: `[state, 0x00, 0x00]` — `state` is `IlluminationState as u8` (`0` = Off, `1` = On).

Response: empty (acknowledgement only).

### `get_brightness_info` (fn 2) / `get_color_temperature_info` (fn 7) [#get_brightness_info-fn-2--get_color_temperature_info-fn-7]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field          | Notes                          |
| ---- | -------------- | ------------------------------ |
| 0    | `capabilities` | `ControlCapabilities` bitfield |
| 1–2  | `min`          | BE u16                         |
| 3–4  | `max`          | BE u16                         |
| 5–6  | `resolution`   | BE u16                         |
| 7    | `max_levels`   | Low nibble only (`& 0x0f`)     |

### `get_brightness` (fn 3) / `get_color_temperature` (fn 8) / `get_brightness_effective_max` (fn 12) [#get_brightness-fn-3--get_color_temperature-fn-8--get_brightness_effective_max-fn-12]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field | Notes                                                             |
| ---- | ----- | ----------------------------------------------------------------- |
| 0–1  | value | BE u16 (Lumens or Kelvin); `0` means no effective limit for fn 12 |

### `set_brightness` (fn 4) / `set_color_temperature` (fn 9) [#set_brightness-fn-4--set_color_temperature-fn-9]

Request: `[value_hi, value_lo, 0x00]` — big-endian `u16`.

Response: empty.

### `get_brightness_levels` (fn 5) / `get_color_temperature_levels` (fn 10) [#get_brightness_levels-fn-5--get_color_temperature_levels-fn-10]

Request: `[start_index << 4, 0x00, 0x00]` — `start_index` in the high nibble of byte 0 (must be `≤ 0x0f`).

Response (byte → field):

| Byte        | Field                 | Notes                                                                      |
| ----------- | --------------------- | -------------------------------------------------------------------------- |
| 0           | flags                 | Bit 0 = linear; bits \[7:5] = valid value count (non-linear only)          |
| 1           | page info             | Bits \[7:4] = `start_index`, bits \[3:0] = `level_count` (non-linear only) |
| 2–3         | `min` / value\[0]     | Linear: `min` BE u16; NonLinear: first level value                         |
| 4–5         | `max` / value\[1]     | Linear: `max` BE u16                                                       |
| 6–7         | `step` / value\[2]    | Linear: `step` BE u16                                                      |
| 8–9 … 14–15 | value\[3] … value\[6] | Non-linear pages only; up to 7 values total                                |

### `set_brightness_levels` (fn 6) / `set_color_temperature_levels` (fn 11) [#set_brightness_levels-fn-6--set_color_temperature_levels-fn-11]

Uses `call_long` with a 16-byte payload from `SetLevels::to_payload()`:

| Variant     | Byte 0                | Byte 1                              | Bytes 2–7                                       | Remaining |
| ----------- | --------------------- | ----------------------------------- | ----------------------------------------------- | --------- |
| `Reset`     | `0x02` (bit 1)        | 0x00                                | 0x00 …                                          | ignored   |
| `Linear`    | `0x01` (bit 0)        | 0x00                                | `min` BE u16, `max` BE u16, `step` BE u16       | 0x00      |
| `NonLinear` | `(count & 0x07) << 5` | `(start_index << 4) \| level_count` | up to 7 BE u16 values packed starting at byte 2 | 0x00      |

### Events [#events-1]

Events are received as unsolicited messages; the sub-id (low nibble of the message's function byte) selects the event:

| Sub-id | Event                           | Byte → field                                                          |
| ------ | ------------------------------- | --------------------------------------------------------------------- |
| 0      | `IlluminationChanged`           | Byte 0 bit 0 = `IlluminationState`                                    |
| 1      | `BrightnessChanged`             | Bytes 0–1 BE u16 = Lumens                                             |
| 2      | `ColorTemperatureChanged`       | Bytes 0–1 BE u16 = Kelvin                                             |
| 3      | `BrightnessEffectiveMaxChanged` | Bytes 0–1 BE u16 = Lumens (`0` = no limit)                            |
| 4      | `BrightnessClamped`             | Byte 0 = `BrightnessClampedSource`; bytes 1–2 BE u16 = clamped Lumens |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::illumination::{IlluminationFeature, IlluminationState, SetLevels}};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<IlluminationFeature>() {
    // Turn illumination on
    feat.set_illumination(IlluminationState::On).await?;

    // Read brightness range, then set to halfway point
    let info = feat.get_brightness_info().await?;
    let mid = info.min + (info.max - info.min) / 2;
    feat.set_brightness(mid).await?;

    // Subscribe to events
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("{event:?}");
    }
}
```


# 0x19b0 · hapticFeedback (/hidpp/features/x19b0-haptic-feedback)



Controls a device's haptic actuator: which waveforms it can play, whether
haptics are enabled and at what intensity, and playing one waveform immediately.

OpenLogi uses it for the [Actions Ring](/docs/features/actions-ring) on the MX
Master 4, a subtle pulse when the highlighted slot changes, and a firmer one
when an action runs.

<Callout type="info">
  Logitech has not published this feature in the public HID++ spec. The function
  and payload layouts here were cross-checked against Solaar and an MX Master 4;
  additions must be verified against hardware rather than guessed.
</Callout>

> **Spec:** reverse-engineered — *x19b0 hapticFeedback*. &#x2A;*Used by:** Actions
> Ring hover and activation feedback.

## Function reference [#function-reference]

The `HapticFeedbackFeature` wrapper (`0x19b0`) exposes:

### Methods [#methods]

| Function            | HID++ fn | Signature                                     | Returns               |
| ------------------- | -------- | --------------------------------------------- | --------------------- |
| `get_capabilities`  | 0        | `()`                                          | `HapticCapabilities`  |
| `get_configuration` | 1        | `()`                                          | `HapticConfiguration` |
| `set_configuration` | 2        | `(enabled: bool, intensity: HapticIntensity)` | `()`                  |
| `play`              | 4        | `(waveform: HapticWaveform)`                  | `()`                  |

All are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `HapticCapabilities` [#hapticcapabilities]

| Field            | Type                 | Description                                    |
| ---------------- | -------------------- | ---------------------------------------------- |
| `unknown_prefix` | `[u8; 4]`            | Bytes whose meaning has not yet been verified. |
| `waveforms`      | `SupportedWaveforms` | Supported waveform mask.                       |

#### `SupportedWaveforms` [#supportedwaveforms]

A bitflags mask; unknown bits are retained so newer firmware doesn't silently
lose capability information.

| Flag                | Bit | Description                                                            |
| ------------------- | --- | ---------------------------------------------------------------------- |
| `DAMP_STATE_CHANGE` | 1   | A damp state-change pulse, used after activating a ring action.        |
| `SUBTLE_COLLISION`  | 4   | A subtle collision pulse, used when the highlighted ring slot changes. |

#### `HapticWaveform` [#hapticwaveform]

| Variant           | Value | Description                                      |
| ----------------- | ----- | ------------------------------------------------ |
| `DampStateChange` | 1     | Confirmation pulse used when an action runs.     |
| `SubtleCollision` | 4     | Light boundary pulse used for hover transitions. |

#### `HapticConfiguration` [#hapticconfiguration]

| Field         | Type              | Description                                           |
| ------------- | ----------------- | ----------------------------------------------------- |
| `enabled`     | `bool`            | Whether firmware haptic playback is enabled.          |
| `intensity`   | `HapticIntensity` | Current intensity percentage.                         |
| `level_count` | `u8`              | Number of discrete levels advertised by the firmware. |
| `level_step`  | `u8`              | Percentage step between discrete levels.              |

#### `HapticIntensity` [#hapticintensity]

A validated `0`–`100` percentage: `HapticIntensity::new(value)` returns `None`
above `HapticIntensity::MAX` (`100`), so an out-of-range intensity can never
reach the wire.

## Wire format [#wire-format]

| Function              | Request                      | Response                                                                                                      |
| --------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 0 `get_capabilities`  | `[0x00, 0x00, 0x00]`         | bytes 0–3 unverified prefix; bytes 4–7 big-endian `u32` waveform mask                                         |
| 1 `get_configuration` | `[0x00, 0x00, 0x00]`         | byte 0 enabled (`0`/`1`); byte 1 intensity percent; byte 2 high nibble = level step, low nibble = level count |
| 2 `set_configuration` | `[enabled, intensity, 0x00]` | —                                                                                                             |
| 4 `play`              | `[waveform, 0x00, 0x00]`     | —                                                                                                             |

An enabled byte other than `0`/`1`, or an intensity above `100`, fails as
`Hidpp20Error::UnsupportedResponse` rather than being coerced.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::haptic_feedback::{HapticFeedbackFeature, HapticIntensity, HapticWaveform},
};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<HapticFeedbackFeature>() {
    let caps = feat.get_capabilities().await?;
    println!("waveforms: {:?}", caps.waveforms);

    if let Some(intensity) = HapticIntensity::new(60) {
        feat.set_configuration(true, intensity).await?;
    }
    feat.play(HapticWaveform::SubtleCollision).await?;
}
```


# 0x1a00 · presenterControl (/hidpp/features/x1a00-presenter-control)



`presenterControl` is the HID++ 2.0 interface for presentation remotes, the
handheld clickers used to drive slide decks. It covers advance slide, previous
slide, start or end a presentation, and blank the screen: the primary
functions of Logitech's Spotlight and similar presenters. It is specific to
devices built for presentation control and does not apply to mice, keyboards,
headsets, touchpads, or receivers in their usual roles.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Control enumeration** — which presenter controls the device exposes and
  each one's capabilities, such as whether it can be remapped or diverted.
* **Button events** — presenter-class presses (next slide, previous slide,
  start/stop, screen blank) arrive as HID++ events, so host software can
  intercept and customise them instead of taking the default HID
  consumer-control reports.
* **Mode and configuration** — read and write presenter settings, for example
  which software profile or pointer mode is active during a session.
* **State notifications** — the device reports state changes (entering or
  leaving presentation mode) so companion software can stay in sync.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1a00`


# 0x1b00 · reprogControls (/hidpp/features/x1b00-reprog-controls)



`reprogControls` is an earlier-generation HID++ 2.0 feature for enumerating and
diverting the physical controls (buttons, function keys, hotkeys) on Logitech
mice and keyboards. It defines the foundational control-table model — each entry
carries a control ID, a default task ID, and a set of capability flags — with
functions to query that table and to switch individual controls between their
default HID report behaviour and HID++ event diversion. The feature was
later superseded by `0x1b04` (specialKeysMSEButtons / reprogControlsV4), which
adds persistent diversion, raw-XY and raw-wheel event streams, analytics key
events, group masking, and the v6 capability query.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x1b04 reprogControls4/specialKeysMSEButtons where applicable.
</Callout>

## What it does [#what-it-does]

* **Control enumeration** — one function reports how many programmable
  controls the device has; another fetches each descriptor by index, with its
  default task ID and capability flags (reprogrammable, divertable).
* **Control diversion** — read and write the diversion state per control ID; a
  diverted control stops sending its usual HID report and emits HID++ events
  instead.
* **Button events** — broadcasts the set of currently pressed diverted controls
  as HID++ events, so host software can respond to them without tying up HID
  report bandwidth.
* **Earlier protocol revision** — compared with `0x1b04`, this version has a
  narrower flag set and does not include persistent diversion, raw pointer
  deltas, raw wheel deltas, or analytics reporting. Devices that advertise only
  `0x1b00` predate those capabilities.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x1b00` · &#x2A;*See also:** 0x1b04 reprogControls4/specialKeysMSEButtons


# 0x1B04 · specialKeysMSEButtons (/hidpp/features/x1b04-special-keys-mse-buttons)



**SpecialKeysMSEButtons** (also called `ReprogControlsV4`) enumerates every
physical and virtual control on a device — buttons, function keys, and hotkeys
— and lets host software divert or remap them. `getCount` reports the number of
entries in the control table; `getCidInfo` returns one row at a time, including
the control's default `TaskId`, its `CidFlags` capability bits, and its group
membership. `getCidReporting` / `setCidReporting` read and write the active
reporting state for a given `ControlId`.

When a control is diverted the device stops sending its default HID report and
emits HID++ events instead. Four event kinds are defined:

* **`DivertedButtons`** — up to four simultaneously pressed diverted controls.
* **`DivertedRawMouseXy`** — raw `dx`/`dy` pointer delta while a diverted control is held.
* **`AnalyticsKeyEvents`** — batch of up to five `AnalyticsKeyEvent` entries (CID + device-defined event code).
* **`DivertedRawWheel`** — raw vertical wheel delta with `RawWheelResolution` (`Low`/`High`) and a period count.

`getCapabilities` (v6 devices) exposes whether `resetAllCidReportSettings` is
supported, which clears all diversion and remapping in one call.

> **Spec:** Logitech HID++ 2.0 — *specialKeysMSEButtons / ReprogControlsV4*. &#x2A;*Used by:** Gesture Button diversion, button remapping in `openlogi-hid`.

## Function reference [#function-reference]

The `ReprogControlsFeature` wrapper (`0x1b04`) exposes:

### Methods [#methods]

| Function                        | HID++ fn | Signature                                      | Returns                         |
| ------------------------------- | -------- | ---------------------------------------------- | ------------------------------- |
| `get_count`                     | 0        | `()`                                           | `u8`                            |
| `get_cid_info`                  | 1        | `(index: u8)`                                  | `CidInfo`                       |
| `get_cid_reporting`             | 2        | `(cid: ControlId)`                             | `CidReporting`                  |
| `set_cid_reporting`             | 3        | `(cid: ControlId, change: CidReportingChange)` | `CidReportingChangeEcho`        |
| `get_capabilities`              | 4        | `()`                                           | `ReprogControlsCapabilities`    |
| `reset_all_cid_report_settings` | 5        | `()`                                           | `()`                            |
| `listen`                        | event    | `()`                                           | `Receiver<ReprogControlsEvent>` |

All methods are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns an `async_channel::Receiver<ReprogControlsEvent>` directly.

### Types [#types]

#### `ControlId` [#controlid]

A HID++ control ID (newtype wrapper around `u16`).

| Field | Type  | Description           |
| ----- | ----- | --------------------- |
| `0`   | `u16` | Raw control ID value. |

#### `TaskId` [#taskid]

A HID++ task ID (newtype wrapper around `u16`); represents the behaviour a control performs.

| Field | Type  | Description        |
| ----- | ----- | ------------------ |
| `0`   | `u16` | Raw task ID value. |

#### `CidInfo` [#cidinfo]

One `getCidInfo` row.

| Field        | Type        | Description                                            |
| ------------ | ----------- | ------------------------------------------------------ |
| `cid`        | `ControlId` | Control ID.                                            |
| `task_id`    | `TaskId`    | Default task ID currently associated with the control. |
| `flags`      | `CidFlags`  | Capability and classification flags.                   |
| `position`   | `u8`        | Physical position value reported by the device.        |
| `group`      | `u8`        | Control group number.                                  |
| `group_mask` | `GroupMask` | Bit mask of groups this control belongs to.            |

#### `CidFlags` [#cidflags]

Capability and classification flags for one control ID (bitflags).

| Flag                      | Bit       | Description                                          |
| ------------------------- | --------- | ---------------------------------------------------- |
| `MOUSE`                   | `1 << 0`  | Control belongs to a mouse/pointer device.           |
| `FUNCTION_KEY`            | `1 << 1`  | Control is a keyboard function key.                  |
| `HOTKEY`                  | `1 << 2`  | Control is a hotkey.                                 |
| `FN_TOGGLE`               | `1 << 3`  | Control toggles Fn behavior.                         |
| `REPROGRAMMABLE`          | `1 << 4`  | Control can be reprogrammed.                         |
| `DIVERTABLE`              | `1 << 5`  | Control can be temporarily diverted to HID++ events. |
| `PERSISTENTLY_DIVERTABLE` | `1 << 6`  | Control can be persistently diverted.                |
| `VIRTUAL_CONTROL`         | `1 << 7`  | Control is virtual rather than a physical input.     |
| `RAW_XY`                  | `1 << 8`  | Control supports raw XY reporting.                   |
| `FORCE_RAW_XY`            | `1 << 9`  | Control supports force raw XY reporting.             |
| `ANALYTICS_KEY_EVENTS`    | `1 << 10` | Control supports analytics key events.               |
| `RAW_WHEEL`               | `1 << 11` | Control supports raw wheel events.                   |

#### `GroupMask` [#groupmask]

Group mask `g1..g8` from `getCidInfo` (newtype wrapper around `u8`).

| Field | Type | Description                                                |
| ----- | ---- | ---------------------------------------------------------- |
| `0`   | `u8` | Bitmask where each bit represents a group membership slot. |

#### `CidReporting` [#cidreporting]

Current reporting/remapping state returned by `getCidReporting`.

| Field                   | Type                | Description                                |
| ----------------------- | ------------------- | ------------------------------------------ |
| `cid`                   | `ControlId`         | Control ID whose reporting state was read. |
| `diverted`              | `bool`              | Whether temporary diversion is enabled.    |
| `persistently_diverted` | `bool`              | Whether persistent diversion is enabled.   |
| `force_raw_xy`          | `bool`              | Whether force raw XY reporting is enabled. |
| `raw_xy`                | `bool`              | Whether raw XY reporting is enabled.       |
| `remap`                 | `Option<ControlId>` | Optional remapping target control ID.      |
| `analytics_key_events`  | `bool`              | Whether analytics key events are enabled.  |
| `raw_wheel`             | `bool`              | Whether raw wheel reporting is enabled.    |

#### `CidReportingChange` [#cidreportingchange]

Changes for `setCidReporting`. For boolean fields, `None` means "leave unchanged".

| Field                   | Type                | Description                                                               |
| ----------------------- | ------------------- | ------------------------------------------------------------------------- |
| `diverted`              | `Option<bool>`      | New temporary diversion state, or `None` to leave unchanged.              |
| `persistently_diverted` | `Option<bool>`      | New persistent diversion state, or `None` to leave unchanged.             |
| `force_raw_xy`          | `Option<bool>`      | New force raw XY state, or `None` to leave unchanged.                     |
| `raw_xy`                | `Option<bool>`      | New raw XY state, or `None` to leave unchanged.                           |
| `remap`                 | `Option<ControlId>` | Remaps to another control ID; `None` sends `0` (no persistent remapping). |
| `analytics_key_events`  | `Option<bool>`      | New analytics key event state, or `None` to leave unchanged.              |
| `raw_wheel`             | `Option<bool>`      | New raw wheel state, or `None` to leave unchanged.                        |

#### `CidReportingChangeEcho` [#cidreportingchangeecho]

Echo returned by `setCidReporting`.

| Field                   | Type                | Description                                     |
| ----------------------- | ------------------- | ----------------------------------------------- |
| `cid`                   | `ControlId`         | Control ID whose reporting state was changed.   |
| `diverted`              | `Option<bool>`      | Echoed temporary diversion state when changed.  |
| `persistently_diverted` | `Option<bool>`      | Echoed persistent diversion state when changed. |
| `force_raw_xy`          | `Option<bool>`      | Echoed force raw XY state when changed.         |
| `raw_xy`                | `Option<bool>`      | Echoed raw XY state when changed.               |
| `remap`                 | `Option<ControlId>` | Echoed remapping target when present.           |
| `analytics_key_events`  | `Option<bool>`      | Echoed analytics key event state when changed.  |
| `raw_wheel`             | `Option<bool>`      | Echoed raw wheel state when changed.            |

#### `ReprogControlsCapabilities` [#reprogcontrolscapabilities]

Feature-level capabilities returned by `getCapabilities` on v6 devices.

| Field                           | Type   | Description                                       |
| ------------------------------- | ------ | ------------------------------------------------- |
| `reset_all_cid_report_settings` | `bool` | Whether `resetAllCidReportSettings` is supported. |

### Events [#events]

`ReprogControlsFeature` implements `EmittingFeature<ReprogControlsEvent>`; call `listen()` to receive a channel of `ReprogControlsEvent` values.

#### `ReprogControlsEvent` [#reprogcontrolsevent]

Event emitted by `0x1b04`.

| Variant              | Event fn | Description                                                                                        |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `DivertedButtons`    | 0        | Up to four currently pressed diverted controls (`[ControlId; 4]`).                                 |
| `DivertedRawMouseXy` | 1        | Raw pointer movement while a diverted control is held; fields `dx: i16`, `dy: i16`.                |
| `AnalyticsKeyEvents` | 2        | Batch of five analytics key event entries (`[AnalyticsKeyEvent; 5]`).                              |
| `DivertedRawWheel`   | 4        | Raw wheel movement; fields `resolution: RawWheelResolution`, `periods: U4`, `delta_vertical: i16`. |

#### `AnalyticsKeyEvent` [#analyticskeyevent]

One analytics key event entry.

| Field   | Type        | Description                                     |
| ------- | ----------- | ----------------------------------------------- |
| `cid`   | `ControlId` | Control ID associated with the analytics event. |
| `event` | `u8`        | Device-defined analytics event code.            |

#### `U4` [#u4]

An unsigned 4-bit value (nibble) encoded as a byte; used for the `periods` field in `DivertedRawWheel`.

| Method             | Description                                                  |
| ------------------ | ------------------------------------------------------------ |
| `from_lo(raw: u8)` | Constructs a nibble from the 4 low/rightmost bits of a byte. |
| `from_hi(raw: u8)` | Constructs a nibble from the 4 high/leftmost bits of a byte. |
| `to_lo() -> u8`    | Returns a byte with the nibble in the 4 low/rightmost bits.  |
| `to_hi() -> u8`    | Returns a byte with the nibble in the 4 high/leftmost bits.  |

#### `RawWheelResolution` [#rawwheelresolution]

Raw wheel movement resolution.

| Variant | Value | Description                     |
| ------- | ----- | ------------------------------- |
| `Low`   | `0`   | Low-resolution wheel movement.  |
| `High`  | `1`   | High-resolution wheel movement. |

## Wire format [#wire-format]

Getter requests carry a short 3-byte payload; `getCidInfo` and `setCidReporting` use a long 16-byte request payload and always return a long 16-byte response payload. All other responses are also read from the 16-byte long payload via `extend_payload()`.

### `get_count` (fn 0) [#get_count-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field | Notes                                   |
| ---- | ----- | --------------------------------------- |
| 0    | count | Number of rows in the control ID table. |

### `get_cid_info` (fn 1) [#get_cid_info-fn-1]

Request (16-byte long payload):

| Byte | Field | Notes                                         |
| ---- | ----- | --------------------------------------------- |
| 0    | index | Row index to retrieve; bytes 1–15 are `0x00`. |

Response (16-byte long payload → `CidInfo`):

| Byte(s) | Field                | Notes                                                                                  |
| ------- | -------------------- | -------------------------------------------------------------------------------------- |
| 0–1     | `cid`                | Big-endian `u16` control ID.                                                           |
| 2–3     | `task_id`            | Big-endian `u16` task ID.                                                              |
| 4       | `flags` (primary)    | Bits 0–7 of `CidFlags` (MOUSE through VIRTUAL\_CONTROL).                               |
| 5       | `position`           | Physical position value.                                                               |
| 6       | `group`              | Control group number.                                                                  |
| 7       | `group_mask`         | Bitmask of group memberships.                                                          |
| 8       | `flags` (additional) | Bits 8–11 of `CidFlags` (RAW\_XY through RAW\_WHEEL); shifted left by 8 when combined. |

### `get_cid_reporting` (fn 2) [#get_cid_reporting-fn-2]

Request: `[cid_hi, cid_lo, 0x00]` (big-endian `ControlId`)

Response (16-byte long payload → `CidReporting`):

| Byte(s) | Field                   | Notes                                                                |
| ------- | ----------------------- | -------------------------------------------------------------------- |
| 0–1     | `cid`                   | Big-endian `u16` control ID.                                         |
| 2 bit 0 | `diverted`              | `1` = temporary diversion enabled.                                   |
| 2 bit 2 | `persistently_diverted` | `1` = persistent diversion enabled.                                  |
| 2 bit 4 | `raw_xy`                | `1` = raw XY reporting enabled.                                      |
| 2 bit 6 | `force_raw_xy`          | `1` = force raw XY enabled.                                          |
| 3–4     | `remap`                 | Big-endian `u16` remap target CID; `0x0000` = no remapping (`None`). |
| 5 bit 0 | `analytics_key_events`  | `1` = analytics key events enabled.                                  |
| 5 bit 2 | `raw_wheel`             | `1` = raw wheel reporting enabled.                                   |

### `set_cid_reporting` (fn 3) [#set_cid_reporting-fn-3]

Request (16-byte long payload built by `CidReportingChange::to_payload`):

| Byte(s) | Field                        | Notes                                                               |
| ------- | ---------------------------- | ------------------------------------------------------------------- |
| 0–1     | `cid`                        | Big-endian `u16` control ID.                                        |
| 2 bit 1 | diverted-valid               | `1` = the diverted value bit is being changed.                      |
| 2 bit 0 | diverted value               | New temporary diversion state (only meaningful when bit 1 is set).  |
| 2 bit 3 | persistently\_diverted-valid | `1` = the persistently\_diverted value bit is being changed.        |
| 2 bit 2 | persistently\_diverted value | New persistent diversion state (only meaningful when bit 3 is set). |
| 2 bit 5 | raw\_xy-valid                | `1` = the raw\_xy value bit is being changed.                       |
| 2 bit 4 | raw\_xy value                | New raw XY state (only meaningful when bit 5 is set).               |
| 2 bit 7 | force\_raw\_xy-valid         | `1` = the force\_raw\_xy value bit is being changed.                |
| 2 bit 6 | force\_raw\_xy value         | New force raw XY state (only meaningful when bit 7 is set).         |
| 3–4     | remap                        | Big-endian `u16` remap target CID; `0x0000` when `remap` is `None`. |
| 5 bit 1 | analytics\_key\_events-valid | `1` = the analytics\_key\_events value bit is being changed.        |
| 5 bit 0 | analytics\_key\_events value | New analytics key event state (only meaningful when bit 1 is set).  |
| 5 bit 3 | raw\_wheel-valid             | `1` = the raw\_wheel value bit is being changed.                    |
| 5 bit 2 | raw\_wheel value             | New raw wheel state (only meaningful when bit 3 is set).            |

Response echoes the same layout as the request; each value field is only valid when its corresponding valid bit is set (see `CidReportingChangeEcho`).

### `get_capabilities` (fn 4) [#get_capabilities-fn-4]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte    | Field                           | Notes                                           |
| ------- | ------------------------------- | ----------------------------------------------- |
| 0 bit 0 | `reset_all_cid_report_settings` | `1` = `resetAllCidReportSettings` is supported. |

### `reset_all_cid_report_settings` (fn 5) [#reset_all_cid_report_settings-fn-5]

Request: `[0x00, 0x00, 0x00]`

Response carries no defined fields; success is indicated by the absence of a `Hidpp20Error`.

### Events [#events-1]

Events arrive as unsolicited long messages (function\_id in the sub-ID nibble, software\_id nibble = `0`).

#### `DivertedButtons` (event fn 0) [#divertedbuttons-event-fn-0]

| Byte(s) | Field      | Notes                                         |
| ------- | ---------- | --------------------------------------------- |
| 0–1     | slot 0 CID | Big-endian `u16`; `0x0000` = slot not in use. |
| 2–3     | slot 1 CID | Big-endian `u16`.                             |
| 4–5     | slot 2 CID | Big-endian `u16`.                             |
| 6–7     | slot 3 CID | Big-endian `u16`.                             |

#### `DivertedRawMouseXy` (event fn 1) [#divertedrawmousexy-event-fn-1]

| Byte(s) | Field | Notes                              |
| ------- | ----- | ---------------------------------- |
| 0–1     | `dx`  | Big-endian `i16` horizontal delta. |
| 2–3     | `dy`  | Big-endian `i16` vertical delta.   |

#### `AnalyticsKeyEvents` (event fn 2) [#analyticskeyevents-event-fn-2]

Five packed 3-byte entries (bytes 0–14); each entry:

| Offset within entry | Field   | Notes                                |
| ------------------- | ------- | ------------------------------------ |
| 0–1                 | `cid`   | Big-endian `u16` control ID.         |
| 2                   | `event` | Device-defined analytics event code. |

#### `DivertedRawWheel` (event fn 4) [#divertedrawwheel-event-fn-4]

| Byte(s)    | Field            | Notes                                                 |
| ---------- | ---------------- | ----------------------------------------------------- |
| 0 bits 7–5 | (reserved)       | Unused upper bits.                                    |
| 0 bit 4    | `resolution`     | `0` = Low, `1` = High (`RawWheelResolution`).         |
| 0 bits 3–0 | `periods`        | 4-bit period count (`U4`, decoded via `U4::from_lo`). |
| 1–2        | `delta_vertical` | Big-endian `i16` vertical wheel delta.                |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::reprog_controls::{ReprogControlsFeature, CidReportingChange}};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ReprogControlsFeature>() {
    // How many controls does this device expose?
    let count = feat.get_count().await?;
    for i in 0..count {
        let info = feat.get_cid_info(i).await?;
        println!("CID {:04x}  divertable={}", info.cid.0, info.flags.is_divertable());
    }

    // Temporarily divert the first divertable control and stream its events.
    let info = feat.get_cid_info(0).await?;
    if info.flags.is_divertable() {
        let change = CidReportingChange::temporary_diversion(true, false);
        feat.set_cid_reporting(info.cid, change).await?;

        let rx = feat.listen();
        if let Ok(event) = rx.recv().await {
            println!("event: {:?}", event);
        }

        // Un-divert before exit.
        feat.set_cid_reporting(info.cid, CidReportingChange::temporary_diversion(false, false)).await?;
    }
}
```


# 0x1C00 · persistentRemappableAction (/hidpp/features/x1c00-persistent-remappable-action)



Persistently remaps a device control to a different HID action, writing the
mapping to the device's non-volatile memory. Controls are identified by the same
`ControlId`s as `0x1B04` (reprogControls); when both features are present, a
live divert from `0x1B04` takes precedence over a persistent remap here.

`get_feature_info` reports which HID output categories the device supports;
`get_count` returns the number of remappable controls and host slots.
`get_cid_info` enumerates the control table; `get_persistent_action` /
`set_persistent_action` read and write a per-control, per-host mapping.
`reset_persistent_action` restores one mapping to its factory default;
`reset_to_factory_settings` clears all mappings for a selected set of host slots.

* **`ActionId`** — the action type a control produces: `SendKeyboard`,
  `SendMouseButton`, `SendXDisplacement`, `SendYDisplacement`,
  `SendVerticalRoller`, `SendHorizontalRoller`, `SendConsumerControl`,
  `ExecuteInternalFunction`, `SendPowerKey`.
* **`RemappableCapabilities`** — bitflags advertising which `ActionId` types
  the device can produce; returned by `get_feature_info`.
* **`ModifierMask`** — keyboard modifier flags (left/right Ctrl, Shift, Alt,
  GUI); applicable to `SendKeyboard` actions only.
* **`HostMask`** — selects one or more host slots (`HOST_1`–`HOST_3`) for
  `reset_to_factory_settings`.

> **Spec:** Logitech HID++ 2.0 — *persistentRemappableAction*. &#x2A;*Used by:**
> Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `PersistentRemappableActionFeature` wrapper (`0x1c00`) exposes:

### Methods [#methods]

| Function                    | HID++ fn | Signature                                                           | Returns                  |
| --------------------------- | -------- | ------------------------------------------------------------------- | ------------------------ |
| `get_feature_info`          | 0        | `()`                                                                | `RemappableCapabilities` |
| `get_count`                 | 1        | `()`                                                                | `RemappableInfo`         |
| `get_cid_info`              | 2        | `(index: u8, host: HostIndex)`                                      | `ControlId`              |
| `get_persistent_action`     | 3        | `(cid: ControlId, host: HostIndex)`                                 | `PersistentAction`       |
| `set_persistent_action`     | 4        | `(cid: ControlId, host: HostIndex, config: PersistentActionConfig)` | `()`                     |
| `reset_persistent_action`   | 5        | `(cid: ControlId, host: HostIndex)`                                 | `()`                     |
| `reset_to_factory_settings` | 6        | `(hosts: HostMask)`                                                 | `()`                     |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `RemappableCapabilities` [#remappablecapabilities]

Bitflags advertising which HID output categories the device's persistent remapping can produce; returned by `get_feature_info`.

| Flag                | Bit/Value | Description                                     |
| ------------------- | --------- | ----------------------------------------------- |
| `KEYBOARD_REPORT`   | `1 << 0`  | Can send keyboard/keypad keys.                  |
| `MOUSE_BUTTONS`     | `1 << 1`  | Can send mouse buttons.                         |
| `X_DISPLACEMENT`    | `1 << 2`  | Can send mouse X displacement.                  |
| `Y_DISPLACEMENT`    | `1 << 3`  | Can send mouse Y displacement.                  |
| `VERTICAL_ROLLER`   | `1 << 4`  | Can send vertical roller increments.            |
| `HORIZONTAL_ROLLER` | `1 << 5`  | Can send horizontal roller (AC pan) increments. |
| `CONSUMER_CONTROL`  | `1 << 6`  | Can send consumer-control codes.                |
| `INTERNAL_FUNCTION` | `1 << 7`  | Can execute internal functions.                 |
| `POWER_KEY`         | `1 << 8`  | Can send power keys.                            |

#### `RemappableInfo` [#remappableinfo]

Control-table sizing returned by `get_count`.

| Field        | Type | Description                          |
| ------------ | ---- | ------------------------------------ |
| `count`      | `u8` | Number of control IDs in the table.  |
| `host_count` | `u8` | Number of hosts the device supports. |

#### `PersistentAction` [#persistentaction]

The action mapped to a control, returned by `get_persistent_action`.

| Field           | Type           | Description                                                        |
| --------------- | -------------- | ------------------------------------------------------------------ |
| `cid`           | `ControlId`    | The control the action belongs to.                                 |
| `host`          | `HostIndex`    | The host slot the mapping applies to.                              |
| `action_id`     | `ActionId`     | The action performed when triggered.                               |
| `value`         | `u16`          | The HID usage code, displacement, or internal-function index sent. |
| `modifier_mask` | `ModifierMask` | Keyboard modifiers applied (keyboard actions only).                |
| `remapped`      | `bool`         | Whether the control is remapped away from its default behaviour.   |

#### `PersistentActionConfig` [#persistentactionconfig]

The action to assign via `set_persistent_action`.

| Field           | Type           | Description                                                           |
| --------------- | -------------- | --------------------------------------------------------------------- |
| `action_id`     | `ActionId`     | The action to perform when triggered.                                 |
| `value`         | `u16`          | The HID usage code, displacement, or internal-function index to send. |
| `modifier_mask` | `ModifierMask` | Keyboard modifiers to apply (keyboard actions only).                  |

#### `ActionId` [#actionid]

The action a control performs when triggered.

| Variant                   | Value  | Description                                                                |
| ------------------------- | ------ | -------------------------------------------------------------------------- |
| `SendKeyboard`            | `0x01` | Send a keyboard/keypad report (HID usage page 7).                          |
| `SendMouseButton`         | `0x02` | Send a mouse-button report (usage page 9).                                 |
| `SendXDisplacement`       | `0x03` | Send mouse X displacement (usage page 1, code 0x30).                       |
| `SendYDisplacement`       | `0x04` | Send mouse Y displacement (usage page 1, code 0x31).                       |
| `SendVerticalRoller`      | `0x05` | Send vertical roller/wheel displacement (usage page 1, code 0x38).         |
| `SendHorizontalRoller`    | `0x06` | Send horizontal roller / AC pan displacement (usage page 12, code 0x0238). |
| `SendConsumerControl`     | `0x07` | Send a consumer-control report (usage page 12).                            |
| `ExecuteInternalFunction` | `0x08` | Execute an internal function (the value is the function index).            |
| `SendPowerKey`            | `0x09` | Send a power-key report (usage page 1).                                    |

#### `ModifierMask` [#modifiermask]

Standard keyboard modifier keys for a remapped keyboard action. Modifiers only apply to keyboard reports.

| Flag          | Bit/Value | Description              |
| ------------- | --------- | ------------------------ |
| `LEFT_CTRL`   | `1 << 0`  | Left Control.            |
| `LEFT_SHIFT`  | `1 << 1`  | Left Shift.              |
| `LEFT_ALT`    | `1 << 2`  | Left Alt.                |
| `LEFT_GUI`    | `1 << 3`  | Left GUI (Win/Command).  |
| `RIGHT_CTRL`  | `1 << 4`  | Right Control.           |
| `RIGHT_SHIFT` | `1 << 5`  | Right Shift.             |
| `RIGHT_ALT`   | `1 << 6`  | Right Alt.               |
| `RIGHT_GUI`   | `1 << 7`  | Right GUI (Win/Command). |

#### `HostMask` [#hostmask]

A set of host slots for `reset_to_factory_settings`.

| Flag     | Bit/Value | Description |
| -------- | --------- | ----------- |
| `HOST_1` | `1 << 0`  | Host 1.     |
| `HOST_2` | `1 << 1`  | Host 2.     |
| `HOST_3` | `1 << 2`  | Host 3.     |

## Wire format [#wire-format]

Short getter requests carry a 3-byte zero payload; `set_persistent_action` uses a 16-byte long-payload request. All responses are read from the 16-byte long payload returned by `.extend_payload()`.

### `get_feature_info` (fn 0) [#get_feature_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field                    | Notes                                          |
| ---- | ------------------------ | ---------------------------------------------- |
| 0–1  | `RemappableCapabilities` | Big-endian u16 bitfield; see flag table above. |

### `get_count` (fn 1) [#get_count-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field        | Notes                                     |
| ---- | ------------ | ----------------------------------------- |
| 0    | `count`      | Number of control IDs in the table.       |
| 1    | `host_count` | Number of host slots the device supports. |

### `get_cid_info` (fn 2) [#get_cid_info-fn-2]

Request: `[index, host, 0x00]`

Response (byte → field):

| Byte | Field       | Notes                              |
| ---- | ----------- | ---------------------------------- |
| 0–1  | `ControlId` | Big-endian u16 control identifier. |

### `get_persistent_action` (fn 3) [#get_persistent_action-fn-3]

Request: `[cid_hi, cid_lo, host]` — `ControlId` split into two bytes big-endian, followed by the `HostIndex` byte.

Response (byte → field):

| Byte | Field           | Notes                                                              |
| ---- | --------------- | ------------------------------------------------------------------ |
| 0–1  | `cid`           | `ControlId` as big-endian u16.                                     |
| 2    | `host`          | `HostIndex` byte.                                                  |
| 3    | `action_id`     | `ActionId` discriminant (0x01–0x09).                               |
| 4–5  | `value`         | HID usage code, displacement, or function index as big-endian u16. |
| 6    | `modifier_mask` | `ModifierMask` bitfield byte.                                      |
| 7    | `remapped`      | Bit 0: `1` = remapped away from factory default.                   |

### `set_persistent_action` (fn 4) [#set_persistent_action-fn-4]

Uses `call_long`: a 16-byte request payload; bytes 7–15 are zero-padded.

| Byte | Field           | Notes                                                        |
| ---- | --------------- | ------------------------------------------------------------ |
| 0–1  | `cid`           | `ControlId` as big-endian u16.                               |
| 2    | `host`          | `HostIndex` byte.                                            |
| 3    | `action_id`     | `ActionId` discriminant.                                     |
| 4–5  | `value`         | HID usage / displacement / function index as big-endian u16. |
| 6    | `modifier_mask` | `ModifierMask` bitfield byte.                                |
| 7–15 | —               | Reserved, zero.                                              |

No meaningful response body.

### `reset_persistent_action` (fn 5) [#reset_persistent_action-fn-5]

Request: `[cid_hi, cid_lo, host]` — same layout as `get_persistent_action` request.

No meaningful response body.

### `reset_to_factory_settings` (fn 6) [#reset_to_factory_settings-fn-6]

Request: `[hosts_bits, 0x00, 0x00]` — `HostMask` bitfield in byte 0; bits 0/1/2 select HOST\_1/HOST\_2/HOST\_3.

No meaningful response body.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::{
        hosts_info::HostIndex,
        persistent_remappable_action::{
            ActionId, ModifierMask, PersistentActionConfig, PersistentRemappableActionFeature,
        },
        reprog_controls::ControlId,
    },
};

// device: &mut Device, already created via Device::new(channel, device_index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<PersistentRemappableActionFeature>() {
    // Query device capabilities and table size.
    let caps = feat.get_feature_info().await?;
    let info = feat.get_count().await?;
    println!("caps={caps:?}, controls={}, hosts={}", info.count, info.host_count);

    // Read the current mapping for control 0x00c4 on host 0.
    let host = HostIndex::from(0u8);
    let cid = ControlId::from(0x00c4u16);
    let action = feat.get_persistent_action(cid, host).await?;
    println!("current action: {:?}", action.action_id);

    // Remap the control to send Ctrl+C (usage 0x06 = HID keycode for 'c').
    let config = PersistentActionConfig {
        action_id: ActionId::SendKeyboard,
        value: 0x0006,
        modifier_mask: ModifierMask::LEFT_CTRL,
    };
    feat.set_persistent_action(cid, host, config).await?;
}
```


# 0x1D4B · wirelessDeviceStatus (/hidpp/features/x1d4b-wireless-device-status)



An event-only feature: the device emits a **status broadcast** when its wireless
link changes — typically on reconnection — and can ask the host software to
reload its configuration.

OpenLogi listens for this so the GUI can refresh a device that wakes or
re-pairs without a manual rescan.

> **Spec:** Logitech HID++ 2.0 — *x1d4b wirelessDeviceStatus* (v0).

## Function reference [#function-reference]

`WirelessDeviceStatusFeature` (`0x1D4B`) is event-only: it has no request functions; the host subscribes and reacts to broadcasts.

### Methods [#methods]

| Function | HID++ fn | Signature | Returns                               |
| -------- | -------- | --------- | ------------------------------------- |
| `listen` | event    | `()`      | `Receiver<WirelessDeviceStatusEvent>` |

`listen` is synchronous and returns an `async_channel::Receiver<WirelessDeviceStatusEvent>` directly (via the `EmittingFeature` trait).

### Events [#events]

#### `WirelessDeviceStatusEvent` [#wirelessdevicestatusevent]

| Variant           | Payload                         | Description                                                         |
| ----------------- | ------------------------------- | ------------------------------------------------------------------- |
| `StatusBroadcast` | `WirelessDeviceStatusBroadcast` | Emitted whenever a device (re)connects to the host. Always enabled. |

#### `WirelessDeviceStatusBroadcast` [#wirelessdevicestatusbroadcast]

| Field     | Type                          | Description                       |
| --------- | ----------------------------- | --------------------------------- |
| `status`  | `WirelessDeviceStatus`        | The status the device reports.    |
| `request` | `WirelessDeviceStatusRequest` | What the device asks of the host. |
| `reason`  | `WirelessDeviceStatusReason`  | Why the broadcast was sent.       |

#### `WirelessDeviceStatus` [#wirelessdevicestatus]

| Variant        | Value  | Description                     |
| -------------- | ------ | ------------------------------- |
| `Unknown`      | `0x00` | Unknown wireless device status. |
| `Reconnection` | `0x01` | Device is reconnecting.         |

#### `WirelessDeviceStatusRequest` [#wirelessdevicestatusrequest]

| Variant                         | Value  | Description                                |
| ------------------------------- | ------ | ------------------------------------------ |
| `NoRequest`                     | `0x00` | No host action requested.                  |
| `SoftwareReconfigurationNeeded` | `0x01` | Host software must reconfigure the device. |

#### `WirelessDeviceStatusReason` [#wirelessdevicestatusreason]

| Variant                | Value  | Description                                      |
| ---------------------- | ------ | ------------------------------------------------ |
| `Unknown`              | `0x00` | Unknown broadcast reason.                        |
| `PowerSwitchActivated` | `0x01` | Broadcast was caused by the device power switch. |

## Wire format [#wire-format]

This feature is event-only: the device sends unsolicited broadcasts; the host never sends a request payload. All broadcasts use sub-function index `0x00`.

### `StatusBroadcast` (event, sub-fn 0) [#statusbroadcast-event-sub-fn-0]

The device pushes a short HID++ message whose payload starts at the standard feature-event offset. The listener fires only when the sub-function nibble is `0`.

Event payload (byte → field):

| Byte | Field     | Type                          | Values                                                     |
| ---- | --------- | ----------------------------- | ---------------------------------------------------------- |
| 0    | `status`  | `WirelessDeviceStatus`        | `0x00` = Unknown, `0x01` = Reconnection                    |
| 1    | `request` | `WirelessDeviceStatusRequest` | `0x00` = NoRequest, `0x01` = SoftwareReconfigurationNeeded |
| 2    | `reason`  | `WirelessDeviceStatusReason`  | `0x00` = Unknown, `0x01` = PowerSwitchActivated            |

Bytes beyond index 2 are not read by the crate.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::{EmittingFeature, wireless_device_status::{WirelessDeviceStatusEvent, WirelessDeviceStatusFeature}},
};

// let mut device = Device::new(Arc::clone(&channel), device_index).await?;
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<WirelessDeviceStatusFeature>() {
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        if let WirelessDeviceStatusEvent::StatusBroadcast(broadcast) = event {
            println!("status={:?} request={:?} reason={:?}",
                broadcast.status, broadcast.request, broadcast.reason);
        }
    }
}
```


# 0x2001 · swapLeftRightButton (/hidpp/features/x2001-swap-left-right-button)



`swapLeftRightButton` is a HID++ 2.0 mouse feature that swaps the primary
(left) and secondary (right) buttons at the firmware level, making the device
a left-handed mouse without OS-level remapping. The setting is typically
stored on the device, so it survives power cycles and host reconnections.
`0x2005` buttonSwapCancel is registered alongside it in the Logitech feature
set.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Swap control** — enable or disable the hardware swap; the device then
  reports swapped clicks with no operating-system configuration change.
* **State query** — read the current swap state back from the device's
  persistent storage.
* **Persistence** — the setting is stored on the device and applies on
  whatever host or operating system the mouse connects to, across power cycles
  and USB re-enumeration.

It appears on mice and pointing devices only; it is not defined for keyboards,
headsets, or receivers.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x2001` · &#x2A;*See also:** 0x2005 buttonSwapCancel


# 0x2100 · verticalScrolling (/hidpp/features/x2100-vertical-scrolling)



Reports the physical characteristics of the device's vertical scroll wheel. The single
function `get_roller_info` returns a `RollerInfo` snapshot describing the roller
hardware and its default scroll behaviour; there are no write functions and no events.

* **roller\_type** — the wheel mechanism: `Standard`, `ThreeG`, `MicroRatchet`,
  `Touchpad`, or `TouchpadNaturalDefault`.
* **ratchets\_per\_turn** — number of physical detents per full wheel revolution.
* **scroll\_lines** — the device's preferred scroll quantum: `SystemDefault` (defer to
  the OS), `Lines(n)` for a fixed count per detent, or `Page` (one full page per detent).

> **Spec:** Logitech HID++ 2.0 — *verticalScrolling*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `VerticalScrollingFeature` wrapper (`0x2100`) exposes:

### Methods [#methods]

| Function          | HID++ fn | Signature | Returns      |
| ----------------- | -------- | --------- | ------------ |
| `get_roller_info` | 0        | `()`      | `RollerInfo` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `RollerInfo` [#rollerinfo]

Vertical scrolling roller information.

| Field               | Type          | Description                        |
| ------------------- | ------------- | ---------------------------------- |
| `roller_type`       | `RollerType`  | Roller type.                       |
| `ratchets_per_turn` | `u8`          | Number of ratchets per wheel turn. |
| `scroll_lines`      | `ScrollLines` | Scroll-line behavior.              |

#### `RollerType` [#rollertype]

Roller type reported by `VerticalScrolling`.

| Variant                  | Value  | Description                                         |
| ------------------------ | ------ | --------------------------------------------------- |
| `Standard`               | `0x01` | Standard one- or two-dimensional roller.            |
| `ThreeG`                 | `0x03` | 3G roller.                                          |
| `MicroRatchet`           | `0x04` | Micro-ratchet roller.                               |
| `Touchpad`               | `0x05` | Touchpad scrolling.                                 |
| `TouchpadNaturalDefault` | `0x06` | Touchpad with natural scrolling enabled by default. |

#### `ScrollLines` [#scrolllines]

Number of lines scrolled for a wheel movement.

| Variant         | Value         | Description                                |
| --------------- | ------------- | ------------------------------------------ |
| `SystemDefault` | `0x00`        | Do not change the host system setting.     |
| `Lines(u8)`     | `0x01`–`0xFE` | Scroll this many lines per movement.       |
| `Page`          | `0xFF`        | Scroll a full page or screen per movement. |

## Wire format [#wire-format]

This feature has one function. The request carries a 3-byte zero payload; the response is read from the 16-byte long payload returned by the device.

### `get_roller_info` (fn 0) [#get_roller_info-fn-0]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are padding zeros.

Response (byte → field):

| Byte | Field               | Notes                                                                                                       |
| ---- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
| 0    | `roller_type`       | `RollerType` discriminant (`0x01`, `0x03`–`0x06`); unknown values yield `Hidpp20Error::UnsupportedResponse` |
| 1    | `ratchets_per_turn` | Raw `u8` — number of physical detents per full revolution                                                   |
| 2    | `scroll_lines`      | `0x00` → `SystemDefault`; `0xFF` → `Page`; `0x01`–`0xFE` → `Lines(n)`                                       |
| 3–15 | —                   | Reserved / ignored                                                                                          |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::vertical_scrolling::VerticalScrollingFeature};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<VerticalScrollingFeature>() {
    let info = feat.get_roller_info().await?;
    println!("Roller type:       {:?}", info.roller_type);
    println!("Ratchets per turn: {}", info.ratchets_per_turn);
    println!("Scroll lines:      {:?}", info.scroll_lines);
}
```


# 0x2110 · smartShift (/hidpp/features/x2110-smartshift)



The original **smartShift** scroll-wheel feature. `getRatchetControlMode` (function 0)
reads the current wheel mode, the auto-disengage speed threshold, and its factory
default; `setRatchetControlMode` (function 1) writes them back. Fields passed as `None`
(or `0` for the byte values) are left unchanged by the device.

* **wheelMode** — `Freespin` (`1`) or `Ratchet` (`2`); reflects the software-set or
  button-set mode, not the transient auto-disengage state.
* **autoDisengage** — `0x01`–`0xFE`, quarter-turns per second at which a ratchet-mode
  wheel releases into free-spin; `0xFF` keeps the ratchet permanently engaged.
* **autoDisengageDefault** — the factory-default threshold stored alongside the active
  value.

Note: OpenLogi's SmartShift panel and `ToggleSmartShift` action drive the newer
`0x2111` smartShiftEnhanced variant instead.

> **Spec:** Logitech HID++ 2.0 — *smartShift*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `SmartShiftFeature` wrapper (`0x2110`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature                                                                                         | Returns              |
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------- | -------------------- |
| `get_ratchet_control_mode` | 0        | `()`                                                                                              | `RatchetControlMode` |
| `set_ratchet_control_mode` | 1        | `(wheel_mode: Option<WheelMode>, auto_disengage: Option<u8>, auto_disengage_default: Option<u8>)` | `()`                 |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `RatchetControlMode` [#ratchetcontrolmode]

Represents the ratchet control mode of the mouse wheel.

| Field                    | Type        | Description                                                                                                                        |
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `wheel_mode`             | `WheelMode` | The mode the wheel is currently set to. Does not reflect the transient auto-disengage state.                                       |
| `auto_disengage`         | `u8`        | Quarter-turns per second at which the wheel automatically disengages. `0xff` disables automatic disengagement (permanent ratchet). |
| `auto_disengage_default` | `u8`        | The factory-default value of `auto_disengage`.                                                                                     |

#### `WheelMode` [#wheelmode]

Represents the ratchet mode of the scroll wheel.

| Variant    | Value | Description           |
| ---------- | ----- | --------------------- |
| `Freespin` | `1`   | Free-spin wheel mode. |
| `Ratchet`  | `2`   | Ratchet wheel mode.   |

## Wire format [#wire-format]

Both functions send a 3-byte short-report request payload. The response is accessed via `extend_payload()`, which always returns a 16-byte array (zero-padded from a short response); only bytes 0–2 carry meaningful data for `get_ratchet_control_mode`.

### `get_ratchet_control_mode` (fn 0) [#get_ratchet_control_mode-fn-0]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are zero.

Response (byte → field):

| Byte | Field                    | Notes                                                 |
| ---- | ------------------------ | ----------------------------------------------------- |
| 0    | `wheel_mode`             | `WheelMode` enum: `1` = `Freespin`, `2` = `Ratchet`   |
| 1    | `auto_disengage`         | Quarter-turns/s threshold; `0xFF` = permanent ratchet |
| 2    | `auto_disengage_default` | Factory-default value of `auto_disengage`             |

### `set_ratchet_control_mode` (fn 1) [#set_ratchet_control_mode-fn-1]

Request: `[wheel_mode_byte, auto_disengage_byte, auto_disengage_default_byte]`

| Byte | Source                                | Notes                                                                |
| ---- | ------------------------------------- | -------------------------------------------------------------------- |
| 0    | `wheel_mode.map_or(0, u8::from)`      | `0` = leave unchanged; `1` = `Freespin`, `2` = `Ratchet`             |
| 1    | `auto_disengage.unwrap_or(0)`         | `0` = leave unchanged; `0x01`–`0xFE` = threshold; `0xFF` = permanent |
| 2    | `auto_disengage_default.unwrap_or(0)` | `0` = leave unchanged; same range as byte 1                          |

Response: not used (the device echoes the written values but the wrapper discards them).

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::smartshift::SmartShiftFeature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<SmartShiftFeature>() {
    // Read current mode
    let mode = feat.get_ratchet_control_mode().await?;
    println!("wheel_mode={:?}, auto_disengage={}", mode.wheel_mode, mode.auto_disengage);

    // Switch to free-spin, keep the auto_disengage threshold unchanged
    feat.set_ratchet_control_mode(
        Some(hidpp::feature::smartshift::WheelMode::Freespin),
        None,
        None,
    ).await?;
}
```


# 0x2111 · smartShiftEnhanced (/hidpp/features/x2111-smartshift-enhanced)



The **SmartShift Enhanced** wheel feature, the variant on the MX Master 3 / 3S /
4 and most current MX-line mice, and the one OpenLogi's SmartShift panel and the
`ToggleSmartShift` action actually drive. `getStatus` (function 1) reads the wheel
mode, the auto-disengage threshold, and the tunable-torque value; `setStatus`
(function 2) writes them back. The device persists all three in its own
non-volatile memory.

* **wheelMode** — `1` free-spin (frictionless), `2` ratchet (clicky).
* **autoDisengage** — `0x01`–`0xFE`, the wheel speed (in 0.25 turn/s steps) past
  which a ratchet-mode wheel releases into free-spin, the SmartShift threshold.
  `0xFF` keeps the ratchet engaged permanently.
* **tunable torque** — ratchet resistance as a percent of the device maximum, or
  `0` when unsupported; read and re-sent unchanged so adjusting the mode or
  threshold doesn't disturb it.

The original **`0x2110` smartShift** uses a different function table — the vendored
`hidpp` library implements that one — but OpenLogi drives the Enhanced `0x2111`
variant.

> **Spec:** Logitech HID++ 2.0 — *x2111 smartShift Enhanced*. &#x2A;*Used by:**
> SmartShift panel, `ToggleSmartShift`.

## Function reference [#function-reference]

The `SmartShiftEnhancedFeature` wrapper (`0x2111`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature                                  | Returns                    |
| -------------------------- | -------- | ------------------------------------------ | -------------------------- |
| `get_capabilities`         | 0        | `()`                                       | `SmartShiftEnhancedInfo`   |
| `get_ratchet_control_mode` | 1        | `()`                                       | `SmartShiftEnhancedStatus` |
| `set_ratchet_control_mode` | 2        | `(change: SmartShiftEnhancedStatusChange)` | `SmartShiftEnhancedStatus` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `SmartShiftEnhancedInfo` [#smartshiftenhancedinfo]

Capability and default values for enhanced SmartShift.

| Field                    | Type                             | Description                                               |
| ------------------------ | -------------------------------- | --------------------------------------------------------- |
| `capabilities`           | `SmartShiftEnhancedCapabilities` | Supported capabilities.                                   |
| `auto_disengage_default` | `u8`                             | Default automatic disengage threshold.                    |
| `default_tunable_torque` | `u8`                             | Default tunable torque, as a percentage of maximum force. |
| `max_force`              | `u8`                             | Maximum force in gram-force units.                        |

#### `SmartShiftEnhancedStatus` [#smartshiftenhancedstatus]

Current enhanced SmartShift status.

| Field                    | Type        | Description                                               |
| ------------------------ | ----------- | --------------------------------------------------------- |
| `wheel_mode`             | `WheelMode` | Current requested wheel mode.                             |
| `auto_disengage`         | `u8`        | Automatic disengage threshold.                            |
| `current_tunable_torque` | `u8`        | Current tunable torque, as a percentage of maximum force. |

#### `SmartShiftEnhancedStatusChange` [#smartshiftenhancedstatuschange]

Enhanced SmartShift status update. `None` fields are encoded as `0` ("do not change") on the wire.

| Field            | Type                | Description                                                                                                                             |
| ---------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `wheel_mode`     | `Option<WheelMode>` | Wheel mode to apply, or `None` to leave unchanged.                                                                                      |
| `auto_disengage` | `Option<NonZeroU8>` | Automatic disengage threshold, or `None` to leave unchanged. HID++ encodes `0` as "do not change", so writable values must be non-zero. |
| `tunable_torque` | `Option<NonZeroU8>` | Tunable torque, or `None` to leave unchanged. HID++ encodes `0` as "do not change", so writable values must be non-zero.                |

#### `SmartShiftEnhancedCapabilities` [#smartshiftenhancedcapabilities]

Capabilities reported by `SmartShiftWheelEnhanced`.

| Flag             | Bit/Value | Description                                 |
| ---------------- | --------- | ------------------------------------------- |
| `TUNABLE_TORQUE` | bit 0     | The device supports tunable ratchet torque. |

#### `WheelMode` [#wheelmode]

Represents the ratchet mode of the scroll wheel (re-exported from `0x2110`).

| Variant    | Value | Description           |
| ---------- | ----- | --------------------- |
| `Freespin` | 1     | Free-spin wheel mode. |
| `Ratchet`  | 2     | Ratchet wheel mode.   |

## Wire format [#wire-format]

All three functions use a 3-byte request payload and return a 16-byte long-message response. Fields absent from a `set` call are encoded as `0` (the documented "do not change" sentinel).

### `get_capabilities` (fn 0) [#get_capabilities-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                    | Notes                               |
| ---- | ------------------------ | ----------------------------------- |
| 0    | `capabilities`           | Bitfield — bit 0 = `TUNABLE_TORQUE` |
| 1    | `auto_disengage_default` | Default auto-disengage threshold    |
| 2    | `default_tunable_torque` | Default torque as % of max force    |
| 3    | `max_force`              | Maximum force in gram-force units   |

### `get_ratchet_control_mode` (fn 1) [#get_ratchet_control_mode-fn-1]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                    | Notes                                                                    |
| ---- | ------------------------ | ------------------------------------------------------------------------ |
| 0    | `wheel_mode`             | `1` = `Freespin`, `2` = `Ratchet`; unknown values fall back to `Ratchet` |
| 1    | `auto_disengage`         | Current threshold (`0x01`–`0xFE`; `0xFF` = always ratchet)               |
| 2    | `current_tunable_torque` | Current torque as % of max force                                         |

### `set_ratchet_control_mode` (fn 2) [#set_ratchet_control_mode-fn-2]

Request: `[wheel_mode, auto_disengage, tunable_torque]`

| Byte | Source                          | Notes                                              |
| ---- | ------------------------------- | -------------------------------------------------- |
| 0    | `change.wheel_mode` as `u8`     | `0` = do not change, `1` = Freespin, `2` = Ratchet |
| 1    | `change.auto_disengage` as `u8` | `0` = do not change (`NonZeroU8` enforces this)    |
| 2    | `change.tunable_torque` as `u8` | `0` = do not change (`NonZeroU8` enforces this)    |

Response layout is identical to `get_ratchet_control_mode` — the device echoes back the resulting state.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::smartshift_enhanced::{SmartShiftEnhancedFeature, SmartShiftEnhancedStatusChange},
    feature::smartshift::WheelMode,
};
use std::num::NonZeroU8;

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<SmartShiftEnhancedFeature>() {
    let info = feat.get_capabilities().await?;
    println!("max force: {}gf, default threshold: {}", info.max_force, info.auto_disengage_default);

    let status = feat.get_ratchet_control_mode().await?;
    println!("wheel mode: {:?}, threshold: {}", status.wheel_mode, status.auto_disengage);

    // Switch to ratchet and raise the auto-disengage threshold to 50
    let updated = feat.set_ratchet_control_mode(SmartShiftEnhancedStatusChange {
        wheel_mode: Some(WheelMode::Ratchet),
        auto_disengage: NonZeroU8::new(50),
        tunable_torque: None, // leave unchanged
    }).await?;
    println!("new threshold: {}", updated.auto_disengage);
}
```


# 0x2120 · highResolutionScrolling (/hidpp/features/x2120-high-resolution-scrolling)



An older HID++ 2.0 scrolling feature that enables high-resolution wheel reporting
on mice and similar pointing devices. It predates the more capable `0x2121` hiResWheel
feature and is found on earlier Logitech wireless and wired mice. Devices that
advertise `0x2121` will typically not advertise `0x2120`; where both are absent,
the OS receives only coarse low-resolution wheel ticks via the standard HID path.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x2121 hiResWheel where applicable.
</Callout>

## What it does [#what-it-does]

* Reads whether the device supports high-resolution scroll reporting, and
  whether it is on: each physical ratchet step is broken into several finer
  increments instead of one coarse tick.
* Enables or disables high-resolution mode, and routes the fine-grained data
  either through the standard HID wheel axis or as diverted HID++
  notifications.
* On some devices, inverts the scroll direction at the device level.

Because it is a predecessor to `0x2121`, the feature set is narrower: it does not
expose ratchet-mode switching (free-spin vs. ratchet) or the richer capability
metadata that `0x2121` provides.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x2120` · &#x2A;*See also:** 0x2121 hiResWheel


# 0x2121 · hiResWheel (/hidpp/features/x2121-hires-wheel)



High-resolution scrolling. The wheel can report fine-grained motion, switch
between ratchet and free-spin, and route its events either as standard HID or as
HID++ notifications. Functions cover reading the wheel capability, getting/setting
the current mode, and inverting direction.

> **Spec:** Logitech HID++ 2.0 — *x2121 hiResWheel* (v1).

## Function reference [#function-reference]

The `HiResWheelFeature` wrapper (`0x2121`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature                                                                 | Returns                     |
| -------------------------- | -------- | ------------------------------------------------------------------------- | --------------------------- |
| `get_wheel_capabilities`   | 0        | `()`                                                                      | `WheelCapabilities`         |
| `get_wheel_mode`           | 1        | `()`                                                                      | `WheelMode`                 |
| `set_wheel_mode`           | 2        | `(target: WheelEventTarget, resolution: WheelResolution, inverted: bool)` | `WheelMode`                 |
| `get_ratchet_switch_state` | 3        | `()`                                                                      | `WheelRatchetState`         |
| `listen`                   | event    | `()`                                                                      | `Receiver<HiResWheelEvent>` |

All request methods are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns an `async_channel::Receiver<HiResWheelEvent>` directly. The analytics functions are intentionally not wrapped; their data structure is undocumented.

### Types [#types]

#### `WheelCapabilities` [#wheelcapabilities]

Wheel and feature capabilities, returned by `get_wheel_capabilities`.

| Field                  | Type   | Description                                                                     |
| ---------------------- | ------ | ------------------------------------------------------------------------------- |
| `multiplier`           | `u8`   | Hi-res report multiplier: reports produced per ratchet distance in hi-res mode. |
| `has_invert`           | `bool` | Device can invert scroll direction in native HID mode (never in diverted mode). |
| `has_switch`           | `bool` | Device has a physical switch for the ratchet mode.                              |
| `ratches_per_rotation` | `u8`   | Ratchets generated by one full wheel rotation.                                  |
| `wheel_diameter`       | `u8`   | Nominal wheel diameter in millimeters.                                          |

#### `WheelMode` [#wheelmode]

Current wheel mode, returned by `get_wheel_mode` / `set_wheel_mode`.

| Field        | Type               | Description                                       |
| ------------ | ------------------ | ------------------------------------------------- |
| `inverted`   | `bool`             | Scroll direction inverted (native HID mode only). |
| `resolution` | `WheelResolution`  | Current scrolling resolution.                     |
| `target`     | `WheelEventTarget` | Where wheel reports are routed.                   |

#### `WheelResolution` [#wheelresolution]

| Variant | Value | Description                      |
| ------- | ----- | -------------------------------- |
| `Low`   | `0`   | Low-resolution wheel reporting.  |
| `High`  | `1`   | High-resolution wheel reporting. |

#### `WheelEventTarget` [#wheeleventtarget]

| Variant    | Value | Description                                 |
| ---------- | ----- | ------------------------------------------- |
| `Native`   | `0`   | Wheel reports go to the native HID path.    |
| `Diverted` | `1`   | Wheel reports are diverted to HID++ events. |

#### `WheelRatchetState` [#wheelratchetstate]

| Variant    | Value | Description                 |
| ---------- | ----- | --------------------------- |
| `Freespin` | `0`   | Wheel is in free-spin mode. |
| `Ratchet`  | `1`   | Wheel is in ratchet mode.   |

### Events [#events]

#### `HiResWheelEvent` [#hireswheelevent]

| Variant         | Payload             | Description                                            |
| --------------- | ------------------- | ------------------------------------------------------ |
| `WheelMovement` | `WheelMovementData` | Emitted on wheel movement in diverted HID++ mode.      |
| `RatchetSwitch` | `WheelRatchetState` | Emitted when the ratchet mode changes. Always enabled. |

#### `WheelMovementData` [#wheelmovementdata]

| Field            | Type              | Description                                                                                       |
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------------- |
| `resolution`     | `WheelResolution` | Current wheel resolution.                                                                         |
| `periods`        | `U4`              | Number of sampling periods for this event; `U4` is a 4-bit unsigned integer (nibble), range 0–15. |
| `delta_vertical` | `i16`             | Vertical movement delta; moving away from the user is positive.                                   |

## Wire format [#wire-format]

All request payloads are 3 bytes. Responses are read from the 16-byte long payload returned by the HID++ 2.0 frame (`extend_payload()`). Event payloads are also parsed from that same 16-byte area.

### `get_wheel_capabilities` (fn 0) [#get_wheel_capabilities-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte    | Field                  | Notes                              |
| ------- | ---------------------- | ---------------------------------- |
| 0       | `multiplier`           | Hi-res report multiplier (`u8`).   |
| 1 bit 3 | `has_invert`           | `payload[1] & (1 << 3) != 0`       |
| 1 bit 2 | `has_switch`           | `payload[1] & (1 << 2) != 0`       |
| 2       | `ratches_per_rotation` | Ratchets per full rotation (`u8`). |
| 3       | `wheel_diameter`       | Nominal diameter in mm (`u8`).     |

### `get_wheel_mode` (fn 1) [#get_wheel_mode-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte    | Field        | Notes                                              |
| ------- | ------------ | -------------------------------------------------- |
| 0 bit 2 | `inverted`   | `payload[0] & (1 << 2) != 0`                       |
| 0 bit 1 | `resolution` | `(payload[0] & (1 << 1)) >> 1` → `WheelResolution` |
| 0 bit 0 | `target`     | `payload[0] & 1` → `WheelEventTarget`              |

### `set_wheel_mode` (fn 2) [#set_wheel_mode-fn-2]

Request: `[mode_byte, 0x00, 0x00]`

`mode_byte` is assembled as:

| Bit | Parameter    | Notes                       |
| --- | ------------ | --------------------------- |
| 2   | `inverted`   | Set when `inverted == true` |
| 1   | `resolution` | `u8::from(resolution) << 1` |
| 0   | `target`     | `u8::from(target)`          |

Response: same byte layout as `get_wheel_mode`.

### `get_ratchet_switch_state` (fn 3) [#get_ratchet_switch_state-fn-3]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte    | Field         | Notes                                  |
| ------- | ------------- | -------------------------------------- |
| 0 bit 0 | ratchet state | `payload[0] & 1` → `WheelRatchetState` |

### Events [#events-1]

#### `WheelMovement` (event fn 0) [#wheelmovement-event-fn-0]

| Byte       | Field            | Notes                                                |
| ---------- | ---------------- | ---------------------------------------------------- |
| 0 bit 4    | `resolution`     | `(payload[0] & (1 << 4)) >> 4` → `WheelResolution`   |
| 0 bits 3–0 | `periods`        | `U4::from_lo(payload[0])` — lower nibble, range 0–15 |
| 1–2        | `delta_vertical` | `i16::from_be_bytes([payload[1], payload[2]])`       |

#### `RatchetSwitch` (event fn 1) [#ratchetswitch-event-fn-1]

| Byte    | Field         | Notes                                  |
| ------- | ------------- | -------------------------------------- |
| 0 bit 0 | ratchet state | `payload[0] & 1` → `WheelRatchetState` |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::hires_wheel::{HiResWheelFeature, WheelEventTarget, WheelResolution}};

// mut device: Device, obtained via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<HiResWheelFeature>() {
    let caps = feat.get_wheel_capabilities().await?;
    println!("multiplier={}, ratchets/rot={}", caps.multiplier, caps.ratches_per_rotation);

    // Enable hi-res diverted mode (scroll events arrive as HID++ notifications)
    feat.set_wheel_mode(WheelEventTarget::Diverted, WheelResolution::High, false).await?;

    // Listen for wheel movement and ratchet-switch events
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("{event:?}");
    }
}
```


# 0x2150 · thumbwheel (/hidpp/features/x2150-thumbwheel)



The horizontal **thumb wheel** on MX-line mice. `getThumbwheelInfo` reports its
capabilities (resolution, native vs divertable, default direction);
`getThumbwheelStatus` reads the current reporting mode; `setThumbwheelReporting`
diverts the wheel so its rotation is delivered to OpenLogi as events instead of
native scrolling. While diverted, the feature emits status updates carrying the
rotation delta plus touch / tap flags.

OpenLogi diverts the thumb wheel to drive horizontal scroll and the
`ThumbwheelScrollUp` / `ThumbwheelScrollDown` bindings, scaled by the
[thumb-wheel sensitivity](/docs/configurations) setting.

> **Spec:** Logitech HID++ 2.0 — *x2150 thumbwheel*. &#x2A;*Used by:** thumb-wheel
> bindings.

## Function reference [#function-reference]

The `ThumbwheelFeature` wrapper (`0x2150`) exposes:

### Methods [#methods]

| Function                   | HID++ fn | Signature                                                 | Returns                                    |
| -------------------------- | -------- | --------------------------------------------------------- | ------------------------------------------ |
| `get_thumbwheel_info`      | 0        | `()`                                                      | `ThumbwheelInfo`                           |
| `get_thumbwheel_status`    | 1        | `()`                                                      | `ThumbwheelStatus`                         |
| `set_thumbwheel_reporting` | 2        | `(mode: ThumbwheelReportingMode, invert_direction: bool)` | `()`                                       |
| `listen`                   | event    | `()`                                                      | `async_channel::Receiver<ThumbwheelEvent>` |

`get_thumbwheel_info`, `get_thumbwheel_status`, and `set_thumbwheel_reporting` are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a receiver directly.

### Types [#types]

#### `ThumbwheelInfo` [#thumbwheelinfo]

Information about the thumbwheel as reported by `get_thumbwheel_info`.

| Field                 | Type                     | Description                                                                                                                              |
| --------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `native_resolution`   | `u16`                    | Number of ratchets generated per revolution in native (HID) mode.                                                                        |
| `diverted_resolution` | `u16`                    | Number of rotation increments generated per revolution in diverted (HID++) mode.                                                         |
| `time_unit`           | `u16`                    | Timestamp unit in microseconds for `ThumbwheelStatusUpdate::time_elapsed`. `0` if `ThumbwheelCapabilities::time_stamp` is not supported. |
| `default_direction`   | `ThumbwheelDirection`    | The default rotation direction.                                                                                                          |
| `capabilities`        | `ThumbwheelCapabilities` | The capabilities of the thumbwheel.                                                                                                      |

#### `ThumbwheelCapabilities` [#thumbwheelcapabilities]

The capabilities the thumbwheel may support.

| Field        | Type   | Description                                                                                                              |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| `time_stamp` | `bool` | Whether the thumbwheel supports emitting the elapsed time between two events via `ThumbwheelStatusUpdate::time_elapsed`. |
| `touch`      | `bool` | Whether the thumbwheel is equipped with a touch sensor.                                                                  |
| `proxy`      | `bool` | Whether the thumbwheel is equipped with a proximity sensor.                                                              |
| `single_tap` | `bool` | Whether the thumbwheel supports detecting single taps.                                                                   |

#### `ThumbwheelDirection` [#thumbwheeldirection]

Determines which rotation direction produces a positive value in `ThumbwheelStatusUpdate::rotation`. Direction descriptors (`LeftOrBack`, `RightOrFront`) are specific to device orientation.

| Variant                    | Value | Description                                                      |
| -------------------------- | ----- | ---------------------------------------------------------------- |
| `PositiveWhenLeftOrBack`   | `0`   | Positive rotation means left/back for this device orientation.   |
| `PositiveWhenRightOrFront` | `1`   | Positive rotation means right/front for this device orientation. |

#### `ThumbwheelStatus` [#thumbwheelstatus]

Current thumbwheel status as reported by `get_thumbwheel_status`.

| Field                | Type                      | Description                                                                                                             |
| -------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `reporting_mode`     | `ThumbwheelReportingMode` | The mode how thumbwheel events are reported (native/HID or diverted/HID++).                                             |
| `direction_inverted` | `bool`                    | Whether the default direction is inverted.                                                                              |
| `touch`              | `bool`                    | Whether the user is currently touching the thumbwheel. Only meaningful if `ThumbwheelCapabilities::touch` is supported. |
| `proxy`              | `bool`                    | Whether the user is currently close to the thumbwheel. Only meaningful if `ThumbwheelCapabilities::proxy` is supported. |

#### `ThumbwheelReportingMode` [#thumbwheelreportingmode]

The mode how the thumbwheel reports its events.

| Variant    | Value | Description                                                                                                 |
| ---------- | ----- | ----------------------------------------------------------------------------------------------------------- |
| `Native`   | `0`   | Thumbwheel events are reported only to the native HID channel.                                              |
| `Diverted` | `1`   | Thumbwheel events are reported only to the diverted HID++ channel. Required for `listen` to receive events. |

### Events [#events]

`ThumbwheelFeature` implements `EmittingFeature<ThumbwheelEvent>`. Call `listen()` to obtain an `async_channel::Receiver<ThumbwheelEvent>`. Events are only delivered when the thumbwheel is in `ThumbwheelReportingMode::Diverted`.

#### `ThumbwheelEvent` [#thumbwheelevent]

| Variant                                | Description                                                                       |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| `StatusUpdate(ThumbwheelStatusUpdate)` | Emitted whenever the thumbwheel status updates. Requires diverted reporting mode. |

#### `ThumbwheelStatusUpdate` [#thumbwheelstatusupdate]

| Field             | Type                       | Description                                                                                                               |
| ----------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `rotation`        | `i16`                      | Rotation delta, relative to `ThumbwheelInfo::native_resolution` or `ThumbwheelInfo::diverted_resolution`.                 |
| `time_elapsed`    | `u16`                      | Time elapsed since the last event, in units of `ThumbwheelInfo::time_unit`. `0` if timestamp capability is not supported. |
| `rotation_status` | `ThumbwheelRotationStatus` | Status of the current rotation gesture.                                                                                   |
| `touch`           | `bool`                     | Whether the user is touching the thumbwheel. Only set if `ThumbwheelCapabilities::touch` is supported.                    |
| `proxy`           | `bool`                     | Whether the user is close to the thumbwheel. Only set if `ThumbwheelCapabilities::proxy` is supported.                    |
| `single_tap`      | `bool`                     | Whether the user single-tapped the thumbwheel. Only set if `ThumbwheelCapabilities::single_tap` is supported.             |

#### `ThumbwheelRotationStatus` [#thumbwheelrotationstatus]

| Variant    | Value | Description                          |
| ---------- | ----- | ------------------------------------ |
| `Inactive` | `0`   | The thumbwheel was not rotated.      |
| `Start`    | `1`   | The thumbwheel rotation was started. |
| `Active`   | `2`   | The thumbwheel rotation is ongoing.  |
| `Stop`     | `3`   | The thumbwheel was released.         |

## Wire format [#wire-format]

All three methods use a 3-byte request payload. Getter responses are read from the 16-byte extended payload returned by `extend_payload()`. The event payload is delivered by the message listener on function sub-id 0.

### `get_thumbwheel_info` (fn 0) [#get_thumbwheel_info-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Bytes | Field                 | Notes                                                                                |
| ----- | --------------------- | ------------------------------------------------------------------------------------ |
| 0–1   | `native_resolution`   | Big-endian `u16`                                                                     |
| 2–3   | `diverted_resolution` | Big-endian `u16`                                                                     |
| 4     | `default_direction`   | Bit 0 only (`& 1`); `0` = `PositiveWhenLeftOrBack`, `1` = `PositiveWhenRightOrFront` |
| 5     | `capabilities`        | Bit 0 = `time_stamp`, bit 1 = `touch`, bit 2 = `proxy`, bit 3 = `single_tap`         |
| 6–7   | `time_unit`           | Big-endian `u16`; `0` when `time_stamp` capability is absent                         |

### `get_thumbwheel_status` (fn 1) [#get_thumbwheel_status-fn-1]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field            | Notes                                                          |
| ---- | ---------------- | -------------------------------------------------------------- |
| 0    | `reporting_mode` | `0` = `Native`, `1` = `Diverted`                               |
| 1    | flags            | Bit 0 = `direction_inverted`, bit 1 = `touch`, bit 2 = `proxy` |

### `set_thumbwheel_reporting` (fn 2) [#set_thumbwheel_reporting-fn-2]

Request: `[mode, invert, 0x00]`

| Byte | Parameter          | Notes                                                               |
| ---- | ------------------ | ------------------------------------------------------------------- |
| 0    | `mode`             | `ThumbwheelReportingMode` as `u8`; `0` = `Native`, `1` = `Diverted` |
| 1    | `invert_direction` | `1` if inverted, `0` otherwise                                      |
| 2    | —                  | Reserved, always `0x00`                                             |

Response: ignored (no fields decoded).

### `StatusUpdate` event (fn 0, sub-id 0) [#statusupdate-event-fn-0-sub-id-0]

Emitted by the device while in `Diverted` mode. The listener filters on function sub-id 0 via `func.to_lo() != 0`.

| Bytes | Field             | Notes                                                         |
| ----- | ----------------- | ------------------------------------------------------------- |
| 0–1   | `rotation`        | Big-endian `i16` rotation delta                               |
| 2–3   | `time_elapsed`    | Big-endian `u16`; `0` when timestamp capability is absent     |
| 4     | `rotation_status` | `0` = `Inactive`, `1` = `Start`, `2` = `Active`, `3` = `Stop` |
| 5     | flags             | Bit 1 = `touch`, bit 2 = `proxy`, bit 3 = `single_tap`        |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::feature::thumbwheel::{ThumbwheelFeature, ThumbwheelReportingMode};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<ThumbwheelFeature>() {
    let info = feat.get_thumbwheel_info().await?;
    println!("native res: {}, diverted res: {}", info.native_resolution, info.diverted_resolution);

    // Divert the thumbwheel so HID++ events are delivered instead of native scrolling.
    feat.set_thumbwheel_reporting(ThumbwheelReportingMode::Diverted, false).await?;

    // Subscribe to rotation events.
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("{event:?}");
    }
}
```


# 0x2200 · mousePointer (/hidpp/features/x2200-mouse-pointer)



Reports basic optical-sensor properties and pointer-tuning hints from the device.
`getMousePointerInfo` (function 0) is the single call: it returns the sensor's
typical resolution in DPI and a set of advisory flags the host may use to pick
sensible defaults.

* **sensorResolution** — typical resolution on a standard surface, in 1-DPI
  steps; real-world values may vary by up to ±20% depending on the surface.
* **pointerAcceleration** — the ballistics curve the device suggests: `None`,
  `Low`, `Medium`, or `High`; hosts with multiple built-in curves can use this
  as the default hint.
* **suggestOsBallistics** — when `true`, the device recommends keeping the
  OS-native ballistics rather than substituting the host's own curve.
* **suggestVerticalTuning** — when `true` (typical for trackballs), the host
  can offer the user X/Y-to-cursor orientation tuning.

> **Spec:** Logitech HID++ 2.0 — *mousePointer*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `MousePointerFeature` wrapper (`0x2200`) exposes:

### Methods [#methods]

| Function                 | HID++ fn | Signature | Returns            |
| ------------------------ | -------- | --------- | ------------------ |
| `get_mouse_pointer_info` | 0        | `()`      | `MousePointerInfo` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `MousePointerInfo` [#mousepointerinfo]

Sensor resolution and pointer-tuning hints returned by `get_mouse_pointer_info`.

| Field                     | Type                  | Description                                                                                                                         |
| ------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `sensor_resolution`       | `u16`                 | Typical sensor resolution on a standard surface, in 1-DPI steps. Real-world values may vary by up to ±20% depending on the surface. |
| `pointer_acceleration`    | `PointerAcceleration` | The acceleration curve the device suggests.                                                                                         |
| `suggest_os_ballistics`   | `bool`                | When `false`, the host may override OS ballistics; when `true`, the device suggests keeping the OS-native ballistics.               |
| `suggest_vertical_tuning` | `bool`                | When `true` (e.g. trackballs), the host can offer the user X/Y-to-cursor orientation tuning.                                        |

#### `PointerAcceleration` [#pointeracceleration]

The pointer-acceleration ("ballistics") curve a device suggests, based on its physical characteristics.

| Variant  | Value | Description                  |
| -------- | ----- | ---------------------------- |
| `None`   | 0     | No acceleration suggested.   |
| `Low`    | 1     | A low acceleration curve.    |
| `Medium` | 2     | A medium acceleration curve. |
| `High`   | 3     | A high acceleration curve.   |

## Wire format [#wire-format]

`getMousePointerInfo` carries a 3-byte zero request payload and returns a 16-byte long response payload (obtained via `extend_payload()`).

### `get_mouse_pointer_info` (fn 0) [#get_mouse_pointer_info-fn-0]

Request: `[0x00, 0x00, 0x00]` — three padding bytes; no parameters.

Response (byte → field):

| Byte          | Field                         | Notes                                                |
| ------------- | ----------------------------- | ---------------------------------------------------- |
| 0             | `sensor_resolution` high byte | Combined with byte 1 as big-endian `u16`.            |
| 1             | `sensor_resolution` low byte  | `u16::from_be_bytes([payload[0], payload[1]])`       |
| 2 bits \[1:0] | `pointer_acceleration`        | Low 2 bits: `0`=None, `1`=Low, `2`=Medium, `3`=High. |
| 2 bit 2       | `suggest_os_ballistics`       | `(flags & (1 << 2)) != 0`                            |
| 2 bit 3       | `suggest_vertical_tuning`     | `(flags & (1 << 3)) != 0`                            |
| 3–15          | *(reserved)*                  | Ignored.                                             |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::mouse_pointer::MousePointerFeature};

// device: &mut Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<MousePointerFeature>() {
    let info = feat.get_mouse_pointer_info().await?;
    println!("Sensor resolution: {} DPI", info.sensor_resolution);
    println!("Acceleration hint: {:?}", info.pointer_acceleration);
    println!("Suggest OS ballistics: {}", info.suggest_os_ballistics);
    println!("Suggest vertical tuning: {}", info.suggest_vertical_tuning);
}
```


# 0x2201 · adjustableDpi (/hidpp/features/x2201-adjustable-dpi)



Per-sensor pointer resolution. `getSensorCount` reports how many sensors the
device has; `getSensorDpiList` returns the supported DPI values (a list or a
range with a step); `getSensorDpi` / `setSensorDpi` read and write the active
DPI.

OpenLogi builds its DPI presets on top of this feature, and exposes *Cycle DPI*
and *Set preset* as bindable actions.

> **Spec:** Logitech HID++ 2.0 — *x2201 adjustableDpi*. See also *x2202
> extendedAdjustableDpi*. &#x2A;*Used by:** DPI presets.

## Function reference [#function-reference]

The `AdjustableDpiFeature` wrapper (`0x2201`) exposes:

### Methods [#methods]

| Function              | HID++ fn | Signature                      | Returns    |
| --------------------- | -------- | ------------------------------ | ---------- |
| `get_sensor_count`    | 0        | `()`                           | `u8`       |
| `get_sensor_dpi_list` | 1        | `(sensor_index: u8)`           | `Vec<u16>` |
| `get_sensor_dpi`      | 2        | `(sensor_index: u8)`           | `u16`      |
| `set_sensor_dpi`      | 3        | `(sensor_index: u8, dpi: u16)` | `()`       |

All methods are `async` and return `Result<…, Hidpp20Error>`.

## Wire format [#wire-format]

All four functions use a 3-byte short request payload and receive a 16-byte long response payload (`extend_payload()`).

### `get_sensor_count` (fn 0) [#get_sensor_count-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field          | Notes                           |
| ---- | -------------- | ------------------------------- |
| 0    | `sensor_count` | Number of sensors on the device |

### `get_sensor_dpi_list` (fn 1) [#get_sensor_dpi_list-fn-1]

Request: `[sensor_index, 0x00, 0x00]`

Response (bytes → field): byte 0 is the echoed `sensor_index`. Bytes 1–15 carry up to seven big-endian `u16` values, terminated by `0x0000` (terminator may be absent when values fill the payload).

Each `u16` value is either:

* An explicit DPI value (top 3 bits are not `0b111`).
* A range marker (`value >> 13 == 0b111`): the low 13 bits are the step (`value & 0x1fff`). The preceding explicit value is the range start; the next explicit value (bytes `offset+2..=offset+3`) is the range end. The crate expands the range into individual DPI steps and always includes the end value.

| Bytes       | Field                        | Notes                                  |
| ----------- | ---------------------------- | -------------------------------------- |
| 0           | echoed `sensor_index`        | Skipped by the crate                   |
| 1–2, 3–4, … | DPI entry (big-endian `u16`) | Explicit value or range marker         |
| next 2      | `0x0000`                     | Terminator (absent if payload is full) |

### `get_sensor_dpi` (fn 2) [#get_sensor_dpi-fn-2]

Request: `[sensor_index, 0x00, 0x00]`

Response (bytes → field):

| Byte | Field                 | Notes                                                      |
| ---- | --------------------- | ---------------------------------------------------------- |
| 0    | echoed `sensor_index` | Ignored by the crate                                       |
| 1    | DPI high byte         | Combined as `u16::from_be_bytes([payload[1], payload[2]])` |
| 2    | DPI low byte          |                                                            |

### `set_sensor_dpi` (fn 3) [#set_sensor_dpi-fn-3]

Request: `[sensor_index, dpi_hi, dpi_lo]` — `dpi` split as `dpi.to_be_bytes()`.

Response: acknowledged; return value is `()` (response payload ignored).

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::adjustable_dpi::AdjustableDpiFeature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<AdjustableDpiFeature>() {
    let sensor_count = feat.get_sensor_count().await?;
    println!("sensors: {sensor_count}");

    // Query supported DPI values for sensor 0
    let dpi_list = feat.get_sensor_dpi_list(0).await?;
    println!("supported DPI: {dpi_list:?}");

    // Read and then raise the active DPI
    let current = feat.get_sensor_dpi(0).await?;
    println!("current DPI: {current}");
    feat.set_sensor_dpi(0, 1600).await?;
}
```


# 0x2202 · extendedAdjustableDpi (/hidpp/features/x2202-extended-adjustable-dpi)



The modern successor to [`0x2201` adjustableDpi](x2201-adjustable-dpi), adding independent
X/Y DPI, lift-off distance (LOD) control, DPI-status LED management, and a
firmware/software calibration flow. Rather than a flat DPI list, supported DPI is
described as a mix of `DpiRange::Fixed` values and `DpiRange::Stepped` ranges (each with
`from`, `to`, and `step`). `get_sensor_capabilities` reports which optional sub-features
a sensor supports via the `SensorCapabilities` flags.

`get_sensor_dpi_parameters` / `set_sensor_dpi_parameters` read and write the active DPI
for both axes and the LOD in a single call. `show_sensor_dpi_status` drives the device's
DPI indicator LED. The calibration sub-flow (`get_dpi_calibration_info` →
`start_dpi_calibration` → `set_dpi_calibration`) can run on firmware or host. Two
unsolicited events are emitted: `ParametersChanged` fires when the user presses a DPI
button, and `CalibrationCompleted` fires when a calibration finishes or times out.

* **`SensorCapabilities`** — `DPI_Y` (independent Y-axis DPI), `LOD` (lift-off control),
  `CALIBRATION`, `PROFILE` (per-profile DPI lists).
* **`Lod`** — `NotSupported`, `Low`, `Medium`, `High`.
* **`LedHoldType`** — `TimerBased`, `EventBased`, `SwControlOn`, `SwControlOff`.
* **`CalibrationType`** — `Hardware` (on-sensor) or `Software` (host-computed).
* **`DpiCalibrationCorrection`** — `Adjust(i16)` scales resolution by
  `(1024 + value) / 1024`; `RevertToOob` / `RevertToProfile` reset to defaults.

> **Spec:** Logitech HID++ 2.0 — *x2202 extendedAdjustableDpi*. &#x2A;*Used by:** DPI presets.

## Function reference [#function-reference]

The `ExtendedDpiFeature` wrapper (`0x2202`) exposes:

### Methods [#methods]

| Function                    | HID++ fn | Signature                                                                           | Returns                      |
| --------------------------- | -------- | ----------------------------------------------------------------------------------- | ---------------------------- |
| `get_sensor_count`          | 0        | `()`                                                                                | `u8`                         |
| `get_sensor_capabilities`   | 1        | `(sensor_index: u8)`                                                                | `SensorCapabilitiesInfo`     |
| `get_sensor_dpi_ranges`     | 2        | `(sensor_index: u8, direction: DpiDirection)`                                       | `Vec<DpiRange>`              |
| `get_sensor_dpi_list`       | 3        | `(sensor_index: u8, direction: DpiDirection)`                                       | `Vec<u16>`                   |
| `get_sensor_lod_list`       | 4        | `(sensor_index: u8, dpi_level_count: u8)`                                           | `Vec<Lod>`                   |
| `get_sensor_dpi_parameters` | 5        | `(sensor_index: u8)`                                                                | `DpiParameters`              |
| `set_sensor_dpi_parameters` | 6        | `(sensor_index: u8, params: SetDpiParameters)`                                      | `()`                         |
| `show_sensor_dpi_status`    | 7        | `(sensor_index: u8, params: ShowDpiStatus)`                                         | `()`                         |
| `get_dpi_calibration_info`  | 8        | `(sensor_index: u8)`                                                                | `DpiCalibrationInfo`         |
| `start_dpi_calibration`     | 9        | `(sensor_index: u8, params: StartDpiCalibration)`                                   | `()`                         |
| `set_dpi_calibration`       | 10       | `(sensor_index: u8, direction: DpiDirection, correction: DpiCalibrationCorrection)` | `()`                         |
| `listen`                    | event    | `()`                                                                                | `Receiver<ExtendedDpiEvent>` |

All async methods return `Result<…, Hidpp20Error>`; `listen` is synchronous and returns an `async_channel::Receiver<ExtendedDpiEvent>` directly.

### Types [#types]

#### `DpiDirection` [#dpidirection]

The axis a DPI value or calibration applies to.

| Variant | Value | Description          |
| ------- | ----- | -------------------- |
| `X`     | 0     | Horizontal (X) axis. |
| `Y`     | 1     | Vertical (Y) axis.   |

#### `Lod` [#lod]

A sensor's lift-off distance setting: the height above the surface at which the sensor stops tracking motion.

| Variant        | Value | Description                                 |
| -------------- | ----- | ------------------------------------------- |
| `NotSupported` | 0     | Lift-off distance control is not supported. |
| `Low`          | 1     | Low lift-off distance.                      |
| `Medium`       | 2     | Medium lift-off distance.                   |
| `High`         | 3     | High lift-off distance.                     |

#### `LedHoldType` [#ledholdtype]

How the device holds the DPI status LED after a `show_sensor_dpi_status` request.

| Variant        | Value | Description                                                                                 |
| -------------- | ----- | ------------------------------------------------------------------------------------------- |
| `TimerBased`   | 0     | Turn the LED off once a device-defined timeout elapses.                                     |
| `EventBased`   | 1     | Turn the LED off once a device-defined event completes (e.g. releasing a DPI-shift button). |
| `SwControlOn`  | 2     | Turn the LED on under software control.                                                     |
| `SwControlOff` | 3     | Turn the LED off under software control.                                                    |

#### `CalibrationType` [#calibrationtype]

Where a DPI calibration is computed.

| Variant    | Value | Description                                                |
| ---------- | ----- | ---------------------------------------------------------- |
| `Hardware` | 0     | Calibration is computed by the sensor firmware / hardware. |
| `Software` | 1     | Calibration is computed by host software.                  |

#### `SensorCapabilities` [#sensorcapabilities]

Per-sensor capability flags reported by `get_sensor_capabilities`.

| Flag          | Bit      | Description                                    |
| ------------- | -------- | ---------------------------------------------- |
| `DPI_Y`       | `1 << 0` | The sensor supports an independent Y-axis DPI. |
| `LOD`         | `1 << 1` | The sensor supports lift-off distance control. |
| `CALIBRATION` | `1 << 2` | The sensor supports DPI calibration.           |
| `PROFILE`     | `1 << 3` | The sensor supports DPI profiles.              |

#### `SensorCapabilitiesInfo` [#sensorcapabilitiesinfo]

A sensor's capabilities and DPI-level count, returned by `get_sensor_capabilities`.

| Field             | Type                 | Description                                                                       |
| ----------------- | -------------------- | --------------------------------------------------------------------------------- |
| `sensor_index`    | `u8`                 | Index of the sensor the capabilities belong to.                                   |
| `dpi_level_count` | `u8`                 | Number of selectable DPI levels, or `0` if the device does not manage DPI levels. |
| `capabilities`    | `SensorCapabilities` | Supported capabilities.                                                           |

#### `DpiRange` [#dpirange]

One entry of a sensor's supported-DPI description, returned by `get_sensor_dpi_ranges`. Fixed values and stepped ranges may be mixed; adjacent ranges may share an endpoint.

| Variant                                     | Description                                                                                          |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `Fixed(u16)`                                | A single selectable DPI value.                                                                       |
| `Stepped { from: u16, to: u16, step: u16 }` | A contiguous range of selectable DPI values from `from` to `to` (inclusive) in increments of `step`. |

#### `DpiParameters` [#dpiparameters]

Current and default DPI parameters of a sensor, returned by `get_sensor_dpi_parameters`. `dpi_y` and `default_dpi_y` are `0` when the sensor does not support an independent Y axis.

| Field           | Type  | Description                                  |
| --------------- | ----- | -------------------------------------------- |
| `sensor_index`  | `u8`  | Index of the sensor.                         |
| `dpi_x`         | `u16` | Current X-axis DPI.                          |
| `default_dpi_x` | `u16` | Default X-axis DPI.                          |
| `dpi_y`         | `u16` | Current Y-axis DPI, or `0` when unsupported. |
| `default_dpi_y` | `u16` | Default Y-axis DPI, or `0` when unsupported. |
| `lod`           | `Lod` | Current lift-off distance.                   |

#### `SetDpiParameters` [#setdpiparameters]

DPI parameters to apply with `set_sensor_dpi_parameters`.

| Field   | Type  | Description                                                                     |
| ------- | ----- | ------------------------------------------------------------------------------- |
| `dpi_x` | `u16` | New X-axis DPI (`1..=57343`).                                                   |
| `dpi_y` | `u16` | New Y-axis DPI (`1..=57343`), or `0` when the sensor has no independent Y axis. |
| `lod`   | `Lod` | New lift-off distance.                                                          |

#### `ShowDpiStatus` [#showdpistatus]

Parameters for `show_sensor_dpi_status`.

| Field           | Type          | Description                                                      |
| --------------- | ------------- | ---------------------------------------------------------------- |
| `dpi_level`     | `u8`          | DPI level to display (`1..=dpi_level_count`).                    |
| `led_hold_type` | `LedHoldType` | How the device holds the DPI status LED.                         |
| `button_num`    | `u8`          | HID button number that initiated the DPI change (starts at `1`). |

#### `DpiCalibrationInfo` [#dpicalibrationinfo]

Calibration reference information returned by `get_dpi_calibration_info`.

| Field          | Type  | Description                                                     |
| -------------- | ----- | --------------------------------------------------------------- |
| `sensor_index` | `u8`  | Index of the sensor.                                            |
| `mouse_width`  | `u8`  | Device width in millimetres.                                    |
| `mouse_length` | `u16` | Device length in millimetres.                                   |
| `calib_dpi_x`  | `u16` | X-axis DPI configured for calibration.                          |
| `calib_dpi_y`  | `u16` | Y-axis DPI configured for calibration, or `0` when unsupported. |

#### `StartDpiCalibration` [#startdpicalibration]

Parameters for `start_dpi_calibration`.

| Field                | Type              | Description                                                                                  |
| -------------------- | ----------------- | -------------------------------------------------------------------------------------------- |
| `direction`          | `DpiDirection`    | Axis to calibrate.                                                                           |
| `expected_count`     | `u16`             | Expected pixel count for the calibration movement (ignored for `CalibrationType::Software`). |
| `calib_type`         | `CalibrationType` | Where the calibration is computed.                                                           |
| `start_timeout`      | `u8`              | Timeout in seconds for the calibration to start (`<= 60`).                                   |
| `hw_process_timeout` | `u8`              | Timeout in seconds for the hardware calibration process (`<= 60`).                           |
| `sw_process_timeout` | `u8`              | Timeout in seconds for the software calibration process (`<= 60`).                           |

#### `DpiCalibrationCorrection` [#dpicalibrationcorrection]

A DPI calibration correction to apply with `set_dpi_calibration`.

| Variant           | Description                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `Adjust(i16)`     | Scale the sensor resolution by `(1024 + value) / 1024`. Valid values are `-1023..=1023`; `0` reverts to the out-of-box setting. |
| `RevertToOob`     | Revert to the out-of-box (OOB) profile setting (wire value `0x0000`).                                                           |
| `RevertToProfile` | Revert to the setting stored in the current profile (wire value `0x8000`).                                                      |

### Events [#events]

`ExtendedDpiFeature` implements `EmittingFeature<ExtendedDpiEvent>`; call `listen()` to receive a channel of `ExtendedDpiEvent` values. The enum has two variants:

#### `DpiParametersChanged` [#dpiparameterschanged]

Payload of `ExtendedDpiEvent::ParametersChanged`. Fired when the sensor's DPI parameters change on the device (e.g. via a DPI button).

| Field          | Type  | Description                                                       |
| -------------- | ----- | ----------------------------------------------------------------- |
| `sensor_index` | `u8`  | Index of the sensor whose parameters changed.                     |
| `dpi_x`        | `u16` | New X-axis DPI.                                                   |
| `dpi_y`        | `u16` | New Y-axis DPI, or `0` when the sensor has no independent Y axis. |
| `lod`          | `Lod` | New lift-off distance.                                            |

#### `DpiCalibrationCompleted` [#dpicalibrationcompleted]

Payload of `ExtendedDpiEvent::CalibrationCompleted`. Fired when a DPI calibration finishes or times out.

| Field          | Type           | Description                                                                                            |
| -------------- | -------------- | ------------------------------------------------------------------------------------------------------ |
| `sensor_index` | `u8`           | Index of the sensor.                                                                                   |
| `direction`    | `DpiDirection` | Axis that was calibrated.                                                                              |
| `correction`   | `i16`          | Calibration correction value; `i16::MIN` (`0x8000`) signals a sensor-level failure (check `failed()`). |
| `delta`        | `i16`          | Perpendicular-axis displacement in pixel counts, useful for judging calibration quality.               |

## Wire format [#wire-format]

Most getter requests carry a 3-byte short payload; setter/calibration requests use a 16-byte long payload via `call_long`. Responses are always read from the 16-byte extended payload returned by `extend_payload()`.

### `get_sensor_count` (fn 0) [#get_sensor_count-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field          | Notes                                   |
| ---- | -------------- | --------------------------------------- |
| 0    | `sensor_count` | Number of motion sensors on the device. |

### `get_sensor_capabilities` (fn 1) [#get_sensor_capabilities-fn-1]

Request: `[sensor_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field             | Notes                                                                                                    |
| ---- | ----------------- | -------------------------------------------------------------------------------------------------------- |
| 0    | `sensor_index`    | Echoed sensor index.                                                                                     |
| 1    | `dpi_level_count` | Selectable DPI levels; `0` = unmanaged.                                                                  |
| 2    | `capabilities`    | `SensorCapabilities` bitfield: bit 0 = `DPI_Y`, bit 1 = `LOD`, bit 2 = `CALIBRATION`, bit 3 = `PROFILE`. |

### `get_sensor_dpi_ranges` (fn 2) [#get_sensor_dpi_ranges-fn-2]

Fetches pages until a `0x0000` end-of-list terminator is encountered (up to 16 pages).

Request: `[sensor_index, direction, page]` where `direction`: 0 = X, 1 = Y; `page` starts at 0.

Response (byte → field):

| Byte | Field                 | Notes                                                                                |
| ---- | --------------------- | ------------------------------------------------------------------------------------ |
| 0    | echoed `sensor_index` | Validated before trusting the page.                                                  |
| 1    | echoed `direction`    |                                                                                      |
| 2    | echoed `page`         |                                                                                      |
| 3–15 | range stream bytes    | Big-endian 16-bit words; top 3 bits `0b111` = hyphen (step); `0x0000` = end-of-list. |

### `get_sensor_dpi_list` (fn 3) [#get_sensor_dpi_list-fn-3]

Request: `[sensor_index, direction, 0x00]`

Response (byte → field):

| Byte | Field                 | Notes                                                  |
| ---- | --------------------- | ------------------------------------------------------ |
| 0    | echoed `sensor_index` | Skipped by the parser.                                 |
| 1    | echoed `direction`    | Skipped by the parser.                                 |
| 2–15 | DPI list words        | Big-endian `u16` values; `0x0000` terminates the list. |

### `get_sensor_lod_list` (fn 4) [#get_sensor_lod_list-fn-4]

Request: `[sensor_index, 0x00, 0x00]`

Response (byte → field):

| Byte                  | Field                 | Notes                                                                                                                                    |
| --------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| 0                     | echoed `sensor_index` | Skipped by the parser.                                                                                                                   |
| 1 … `dpi_level_count` | LOD entries           | One `u8` per level: 0 = `NotSupported`, 1 = `Low`, 2 = `Medium`, 3 = `High`. List length is supplied by the caller as `dpi_level_count`. |

### `get_sensor_dpi_parameters` (fn 5) [#get_sensor_dpi_parameters-fn-5]

Request: `[sensor_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field           | Notes                                                 |
| ---- | --------------- | ----------------------------------------------------- |
| 0    | `sensor_index`  |                                                       |
| 1–2  | `dpi_x`         | Big-endian `u16`.                                     |
| 3–4  | `default_dpi_x` | Big-endian `u16`.                                     |
| 5–6  | `dpi_y`         | Big-endian `u16`; `0` when Y-axis DPI is unsupported. |
| 7–8  | `default_dpi_y` | Big-endian `u16`; `0` when Y-axis DPI is unsupported. |
| 9    | `lod`           | `Lod` byte: 0–3.                                      |

### `set_sensor_dpi_parameters` (fn 6) [#set_sensor_dpi_parameters-fn-6]

Uses a 16-byte long request payload (`call_long`).

Request bytes 0–5 (remainder zero-padded):

| Byte | Field             | Notes                           |
| ---- | ----------------- | ------------------------------- |
| 0    | `sensor_index`    |                                 |
| 1    | `dpi_x` high byte |                                 |
| 2    | `dpi_x` low byte  |                                 |
| 3    | `dpi_y` high byte | `0` when no independent Y axis. |
| 4    | `dpi_y` low byte  | `0` when no independent Y axis. |
| 5    | `lod`             | `Lod` byte: 0–3.                |

Response: not read (acknowledged by the device's echo).

### `show_sensor_dpi_status` (fn 7) [#show_sensor_dpi_status-fn-7]

Uses a 16-byte long request payload (`call_long`).

Request bytes 0–3 (remainder zero-padded):

| Byte | Field           | Notes                                                                      |
| ---- | --------------- | -------------------------------------------------------------------------- |
| 0    | `sensor_index`  |                                                                            |
| 1    | `dpi_level`     | Level to display (`1..=dpi_level_count`).                                  |
| 2    | `led_hold_type` | 0 = `TimerBased`, 1 = `EventBased`, 2 = `SwControlOn`, 3 = `SwControlOff`. |
| 3    | `button_num`    | HID button number that triggered the change (starts at 1).                 |

Response: not read.

### `get_dpi_calibration_info` (fn 8) [#get_dpi_calibration_info-fn-8]

Request: `[sensor_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field          | Notes                                                           |
| ---- | -------------- | --------------------------------------------------------------- |
| 0    | `sensor_index` |                                                                 |
| 1    | `mouse_width`  | Device width in mm.                                             |
| 2–3  | `mouse_length` | Device length in mm, big-endian `u16`.                          |
| 4–5  | `calib_dpi_x`  | X-axis calibration DPI, big-endian `u16`.                       |
| 6–7  | `calib_dpi_y`  | Y-axis calibration DPI, big-endian `u16`; `0` when unsupported. |

### `start_dpi_calibration` (fn 9) [#start_dpi_calibration-fn-9]

Uses a 16-byte long request payload (`call_long`).

Request bytes 0–7 (remainder zero-padded):

| Byte | Field                      | Notes                                    |
| ---- | -------------------------- | ---------------------------------------- |
| 0    | `sensor_index`             |                                          |
| 1    | `direction`                | 0 = X, 1 = Y.                            |
| 2    | `expected_count` high byte | Ignored for `CalibrationType::Software`. |
| 3    | `expected_count` low byte  |                                          |
| 4    | `calib_type`               | 0 = `Hardware`, 1 = `Software`.          |
| 5    | `start_timeout`            | Seconds; `<= 60`.                        |
| 6    | `hw_process_timeout`       | Seconds; `<= 60`.                        |
| 7    | `sw_process_timeout`       | Seconds; `<= 60`.                        |

Response: not read (result delivered via `CalibrationCompleted` event).

### `set_dpi_calibration` (fn 10) [#set_dpi_calibration-fn-10]

Uses a 16-byte long request payload (`call_long`).

Request bytes 0–3 (remainder zero-padded):

| Byte | Field                | Notes                                                                                                                   |
| ---- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 0    | `sensor_index`       |                                                                                                                         |
| 1    | `direction`          | 0 = X, 1 = Y.                                                                                                           |
| 2    | correction high byte | `DpiCalibrationCorrection::to_wire()`: `0x8000` = `RevertToProfile`, `0x0000` = `RevertToOob`, signed value = `Adjust`. |
| 3    | correction low byte  |                                                                                                                         |

Response: not read.

### Events [#events-1]

#### `ParametersChanged` (sub-id 0) [#parameterschanged-sub-id-0]

| Byte | Field          | Notes                                                 |
| ---- | -------------- | ----------------------------------------------------- |
| 0    | `sensor_index` |                                                       |
| 1–2  | `dpi_x`        | Big-endian `u16`.                                     |
| 3–4  | `dpi_y`        | Big-endian `u16`; `0` when Y-axis DPI is unsupported. |
| 5    | `lod`          | `Lod` byte: 0–3.                                      |

#### `CalibrationCompleted` (sub-id 1) [#calibrationcompleted-sub-id-1]

| Byte | Field          | Notes                                                                        |
| ---- | -------------- | ---------------------------------------------------------------------------- |
| 0    | `sensor_index` |                                                                              |
| 1    | `direction`    | 0 = X, 1 = Y.                                                                |
| 2–3  | `correction`   | Signed big-endian `i16`; `0x8000` (`i16::MIN`) signals sensor-level failure. |
| 4–5  | `delta`        | Signed big-endian `i16`; perpendicular-axis pixel displacement.              |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::{
        extended_dpi::{
            DpiDirection, ExtendedDpiFeature, Lod, SetDpiParameters, StartDpiCalibration,
            CalibrationType,
        },
        EmittingFeature,
    },
};

// device: &mut Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ExtendedDpiFeature>() {
    // Query how many sensors the device has.
    let sensor_count = feat.get_sensor_count().await?;

    // Read the current DPI and lift-off distance for sensor 0.
    let params = feat.get_sensor_dpi_parameters(0).await?;
    println!("DPI X={} Y={} LOD={:?}", params.dpi_x, params.dpi_y, params.lod);

    // Set a new DPI (e.g. 1600 dpi on X, linked Y, medium lift-off).
    feat.set_sensor_dpi_parameters(0, SetDpiParameters {
        dpi_x: 1600,
        dpi_y: 0, // linked to X when sensor has no independent Y axis
        lod: Lod::Medium,
    }).await?;

    // Listen for unsolicited DPI-change events.
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("DPI event: {:?}", event);
    }
}
```


# 0x40a0 · fnInversion (/hidpp/features/x40a0-fn-inversion)



`0x40a0` fnInversion is the earliest HID++ 2.0 feature for controlling whether a
keyboard's F-row keys act as standard function keys or as their labeled secondary
actions (media controls, brightness, etc.) by default, the mechanism commonly
known as Fn-lock. It applies to keyboards and keyboard-bearing devices that expose
a physical F-row. This feature is the predecessor of `0x40A2`
fnInversionWithDefaultState and `0x40A3` fnInversionMultiHost, which OpenLogi
does implement; later devices report those instead.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x40a2 / 0x40a3 where applicable.
</Callout>

## What it does [#what-it-does]

* **Query the current inversion state** — ask the device whether Fn-lock is currently on or off.
* **Write a new inversion state** — flip Fn-lock on or off; the device stores the value in non-volatile memory so it persists across power cycles.
* **Single-host, no default-state field** — unlike `0x40A2`, this earlier revision does not report the factory default alongside the live state, and does not carry a per-host-slot parameter.

Because it predates the `0x40A2` / `0x40A3` revisions, it is found mainly on older Logitech wireless keyboards and does not expose the capabilities flags or multi-host extensions added in later variants.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x40a0` · &#x2A;*See also:** 0x40a2 / 0x40a3


# 0x40A2 · fnInversionWithDefaultState (/hidpp/features/x40a2-fn-inversion)



Controls whether a keyboard's function keys act as standard Fn keys or as their
secondary (media / action) labels, the Fn-lock inversion toggle.
`getGlobalFnInversion` reads the current state and the device's built-in
default; `setGlobalFnInversion` writes the new state and echoes back the
resulting `GlobalFnInversion`.

* **state** — `On`: Fn keys produce their secondary (media / action) function;
  `Off`: standard Fn behaviour.
* **default\_state** — the factory or device-stored default, returned alongside
  the live state so callers can offer a "reset to default" action.

`0x40A2&#x60; is the single-host predecessor of **`0x40A3` fnInversionForMultiHostDevices**,
which adds a `host` slot parameter and a `capabilities` field (`MANUAL_FN_LOCK`);
both share the same `FnInversionState` enum.

> **Spec:** Logitech HID++ 2.0 — *fnInversionWithDefaultState*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `FnInversionWithDefaultStateFeature` wrapper (`0x40a2`) exposes:

### Methods [#methods]

| Function                  | HID++ fn | Signature                   | Returns             |
| ------------------------- | -------- | --------------------------- | ------------------- |
| `get_global_fn_inversion` | 0        | `()`                        | `GlobalFnInversion` |
| `set_global_fn_inversion` | 1        | `(state: FnInversionState)` | `GlobalFnInversion` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `GlobalFnInversion` [#globalfninversion]

Global function-key inversion state, common to all keys.

| Field           | Type               | Description              |
| --------------- | ------------------ | ------------------------ |
| `state`         | `FnInversionState` | Current inversion state. |
| `default_state` | `FnInversionState` | Default inversion state. |

#### `FnInversionState` [#fninversionstate]

Function-key inversion state.

| Variant | Value | Description                         |
| ------- | ----- | ----------------------------------- |
| `Off`   | `0`   | Function-key inversion is disabled. |
| `On`    | `1`   | Function-key inversion is enabled.  |

## Wire format [#wire-format]

Both methods carry a 3-byte request payload and read state from the 16-byte extended response payload.

### `get_global_fn_inversion` (fn 0) [#get_global_fn_inversion-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters; all bytes are padding)

Response (byte → field):

| Byte | Field           | Notes               |
| ---- | --------------- | ------------------- |
| 0    | `state`         | `0` = Off, `1` = On |
| 1    | `default_state` | `0` = Off, `1` = On |
| 2–15 | —               | unused              |

### `set_global_fn_inversion` (fn 1) [#set_global_fn_inversion-fn-1]

Request: `[state, 0x00, 0x00]`

| Byte | Value             | Notes               |
| ---- | ----------------- | ------------------- |
| 0    | `u8::from(state)` | `0` = Off, `1` = On |
| 1–2  | `0x00`            | padding             |

Response (byte → field): identical layout to `get_global_fn_inversion` — byte 0 = `state`, byte 1 = `default_state`.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::fn_inversion::{FnInversionState, FnInversionWithDefaultStateFeature},
};

// mut device: Device, already created via Device::new(channel, index).await?
// enumerate_features requires &mut self.
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<FnInversionWithDefaultStateFeature>() {
    // Read current Fn-lock state and its factory default.
    let info = feat.get_global_fn_inversion().await?;
    println!("state={:?}  default={:?}", info.state, info.default_state);

    // Enable Fn-lock inversion and confirm the resulting state.
    let updated = feat.set_global_fn_inversion(FnInversionState::On).await?;
    println!("new state={:?}", updated.state);
}
```


# 0x40A3 · fnInversionMultiHost (/hidpp/features/x40a3-fn-inversion-multi-host)



**fnInversionForMultiHostDevices** controls whether F-row keys behave as standard
function keys or as their labeled media / special-key actions by default. Unlike
the single-host predecessor `0x40A2` (`fnInversionWithDefaultState`), this
variant stores an independent inversion state for each host slot.

`getGlobalFnInversion` (function 0) reads the current state, the factory-default
state, and the device capabilities for a given `HostIndex`. `setGlobalFnInversion`
(function 1) writes the desired `FnInversionState` for a host slot; the device
persists the value in its own non-volatile memory and returns the resulting
`FnInversionInfo`.

* **`FnInversionState`** — `Off` (F-keys behave as labeled function keys), `On`
  (F-keys behave as media / special-key actions).
* **`FnInversionCapabilities`** — `MANUAL_FN_LOCK` flag indicates the device
  supports manual Fn-lock control.
* **`FnInversionInfo`** — response struct carrying `host_index`, `state`,
  `default_state`, and `capabilities`.

> **Spec:** Logitech HID++ 2.0 — *fnInversionForMultiHostDevices*. &#x2A;*Used by:**
> Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `FnInversionMultiHostFeature` wrapper (`0x40a3`) exposes:

### Methods [#methods]

| Function                  | HID++ fn | Signature                                    | Returns           |
| ------------------------- | -------- | -------------------------------------------- | ----------------- |
| `get_global_fn_inversion` | 0        | `(host: HostIndex)`                          | `FnInversionInfo` |
| `set_global_fn_inversion` | 1        | `(host: HostIndex, state: FnInversionState)` | `FnInversionInfo` |

All methods are `async` and return `Result<_, Hidpp20Error>`.

### Types [#types]

#### `FnInversionInfo` [#fninversioninfo]

Function-key inversion state for a host slot.

| Field           | Type                      | Description                             |
| --------------- | ------------------------- | --------------------------------------- |
| `host_index`    | `HostIndex`               | Host slot index returned by the device. |
| `state`         | `FnInversionState`        | Current inversion state.                |
| `default_state` | `FnInversionState`        | Default inversion state.                |
| `capabilities`  | `FnInversionCapabilities` | Inversion capabilities.                 |

#### `FnInversionState` [#fninversionstate]

Function-key inversion state.

| Variant | Value | Description                         |
| ------- | ----- | ----------------------------------- |
| `Off`   | `0`   | Function-key inversion is disabled. |
| `On`    | `1`   | Function-key inversion is enabled.  |

#### `FnInversionCapabilities` [#fninversioncapabilities]

Function-key inversion capabilities (bitflags).

| Flag             | Bit/Value | Description                                 |
| ---------------- | --------- | ------------------------------------------- |
| `MANUAL_FN_LOCK` | `1 << 0`  | The device supports manual Fn-lock control. |

#### `HostIndex` [#hostindex]

A host slot selector (defined in `hosts_info`).

| Variant    | Value      | Description                                     |
| ---------- | ---------- | ----------------------------------------------- |
| `Current`  | `0xff`     | The host slot currently selected by the device. |
| `Slot(u8)` | `0`–`0xfe` | A zero-based host slot index.                   |

## Wire format [#wire-format]

Requests carry a 3-byte payload; responses are parsed from the 16-byte long payload returned by `extend_payload()`.

### `get_global_fn_inversion` (fn 0) [#get_global_fn_inversion-fn-0]

Request: `[host_index, 0x00, 0x00]`

* Byte 0: `host_index` — `u8` encoding of `HostIndex` (`0xff` = current slot, `0x00`–`0xfe` = zero-based slot index)
* Bytes 1–2: reserved, always `0x00`

Response (byte → field):

| Byte | Field           | Notes                              |
| ---- | --------------- | ---------------------------------- |
| 0    | `host_index`    | `HostIndex` returned by the device |
| 1    | `state`         | `0` = `Off`, `1` = `On`            |
| 2    | `default_state` | `0` = `Off`, `1` = `On`            |
| 3    | `capabilities`  | Bitflags: bit 0 = `MANUAL_FN_LOCK` |
| 4–15 | —               | Reserved / unused                  |

### `set_global_fn_inversion` (fn 1) [#set_global_fn_inversion-fn-1]

Request: `[host_index, state, 0x00]`

* Byte 0: `host_index` — `u8` encoding of `HostIndex`
* Byte 1: `state` — `u8` encoding of `FnInversionState` (`0` = `Off`, `1` = `On`)
* Byte 2: reserved, always `0x00`

Response (byte → field): identical layout to `get_global_fn_inversion` — the device echoes back the resulting `FnInversionInfo` for the host slot.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::fn_inversion::{FnInversionMultiHostFeature, FnInversionState},
};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<FnInversionMultiHostFeature>() {
    // Read the current state for the active host slot.
    let info = feat.get_global_fn_inversion(hidpp::feature::hosts_info::HostIndex::Current).await?;
    println!("state={:?}, default={:?}, caps={:?}", info.state, info.default_state, info.capabilities);

    // Enable Fn-inversion on host slot 0.
    let updated = feat
        .set_global_fn_inversion(hidpp::feature::hosts_info::HostIndex::Slot(0), FnInversionState::On)
        .await?;
    println!("new state={:?}", updated.state);
}
```


# 0x4301 · solarKeyboardDashboard (/hidpp/features/x4301-solar-keyboard-dashboard)



Battery and ambient-light monitoring for Logitech solar keyboards such as the K750.
`set_light_measure` schedules periodic light reports by specifying the number of
reports and their interval in seconds; passing `0` for either value cancels an
in-progress schedule. `set_led` overrides the firmware's CheckLight LED to a chosen
color for a firmware-defined duration; call it within the 250 ms window after a
`CheckLightButton` event to intercept before the firmware renders its own status.

All three broadcast events carry a `SolarStatus` payload with `battery_level`
(percentage) and `light_level` (lux, `0`–`511`):

* **`Battery`** — spontaneous report fired roughly every 90 s, at power-up, and on
  reconnect; `light_level` is always `0`.
* **`LightMeasure`** — one report per period set by `set_light_measure`.
* **`CheckLightButton`** — fired when the user presses the CheckLight button,
  carrying the latest battery and light readings.

`set_led` accepts a `LedId` variant: `Off`, `Red`, `Orange`, or `Green`.

> **Spec:** Logitech HID++ 2.0 — *solarKeyboardDashboard*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `SolarDashboardFeature` wrapper (`0x4301`) exposes:

### Methods [#methods]

| Function            | HID++ fn | Signature                              | Returns                               |
| ------------------- | -------- | -------------------------------------- | ------------------------------------- |
| `set_light_measure` | 0        | `(max_reports: u8, report_period: u8)` | `()`                                  |
| `set_led`           | 1        | `(led: LedId)`                         | `()`                                  |
| `listen`            | event    | `()`                                   | `async_channel::Receiver<SolarEvent>` |

`set_light_measure` and `set_led` are `async` and return `Result<(), Hidpp20Error>`. `listen` is synchronous and returns an `async_channel::Receiver<SolarEvent>` that yields decoded broadcast events.

### Types [#types]

#### `LedId` [#ledid]

A CheckLight LED color passed to `set_led`.

| Variant  | Value | Description   |
| -------- | ----- | ------------- |
| `Off`    | 0     | All LEDs off. |
| `Red`    | 1     | Red.          |
| `Orange` | 2     | Orange.       |
| `Green`  | 3     | Green.        |

#### `SolarStatus` [#solarstatus]

Battery and light readings shared by every solar-dashboard event.

| Field           | Type  | Description                                  |
| --------------- | ----- | -------------------------------------------- |
| `battery_level` | `u8`  | Remaining battery capacity, as a percentage. |
| `light_level`   | `u16` | Current light measure in lux (`0..=511`).    |

### Events [#events]

`SolarDashboardFeature` implements `EmittingFeature<SolarEvent>`; call `listen()` to subscribe. Each variant wraps a `SolarStatus` payload.

| Variant                         | Description                                                                                                             |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `Battery(SolarStatus)`          | Spontaneous battery report (every \~90 s, at power-up, and on reconnect). `light_level` is always `0` for this variant. |
| `LightMeasure(SolarStatus)`     | Battery and light report sent per the `set_light_measure` schedule.                                                     |
| `CheckLightButton(SolarStatus)` | The CheckLight button was pressed; carries the latest battery and light readings.                                       |

## Wire format [#wire-format]

`set_light_measure` and `set_led` each carry a 3-byte short request payload with no meaningful response bytes; both functions are fire-and-acknowledge. Broadcast events arrive as 16-byte long payloads decoded by `SolarStatus::from_payload`.

### `set_light_measure` (fn 0) [#set_light_measure-fn-0]

Request: `[max_reports, report_period, 0x00]`

| Byte | Field           | Notes                                                             |
| ---- | --------------- | ----------------------------------------------------------------- |
| 0    | `max_reports`   | Number of `LightMeasure` reports to send. `0` cancels scheduling. |
| 1    | `report_period` | Interval between reports in seconds. `0` cancels scheduling.      |
| 2    | —               | Padding, always `0x00`.                                           |

Response: acknowledged only; no response fields are read.

### `set_led` (fn 1) [#set_led-fn-1]

Request: `[led, 0x00, 0x00]`

| Byte | Field | Notes                                                     |
| ---- | ----- | --------------------------------------------------------- |
| 0    | `led` | `LedId` as `u8`: `Off`=0, `Red`=1, `Orange`=2, `Green`=3. |
| 1–2  | —     | Padding, always `0x00`.                                   |

Response: acknowledged only; no response fields are read.

### Events (fn 0–2) [#events-fn-02]

All three event variants share the same 16-byte payload layout decoded by `SolarStatus::from_payload`.

| Byte | Field           | Notes                                               |
| ---- | --------------- | --------------------------------------------------- |
| 0    | `battery_level` | Remaining battery capacity as a percentage (`u8`).  |
| 1–2  | `light_level`   | Ambient light in lux, big-endian `u16` (`0..=511`). |
| 3–15 | —               | Unused.                                             |

Event sub-id (the function nibble on the broadcast message) selects the variant:

| Sub-id | Variant                        |
| ------ | ------------------------------ |
| 0      | `SolarEvent::Battery`          |
| 1      | `SolarEvent::LightMeasure`     |
| 2      | `SolarEvent::CheckLightButton` |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::solar_dashboard::{LedId, SolarDashboardFeature, SolarEvent}};

// device: mut Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<SolarDashboardFeature>() {
    // Schedule 10 light-measure reports every 30 seconds.
    feat.set_light_measure(10, 30).await?;

    // Subscribe to broadcast events.
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        match event {
            SolarEvent::Battery(s) => println!("battery: {}%", s.battery_level),
            SolarEvent::LightMeasure(s) => println!("battery: {}%, lux: {}", s.battery_level, s.light_level),
            SolarEvent::CheckLightButton(s) => {
                println!("check-light pressed: battery {}%, lux {}", s.battery_level, s.light_level);
                // Override the firmware LED within the 250 ms window.
                feat.set_led(LedId::Green).await?;
            }
        }
    }
}
```


# 0x4520 · keyboardLayout (/hidpp/features/x4520-keyboard-layout)



**keyboardLayout** reports, and on some devices configures, the physical layout a keyboard is set to: QWERTY, AZERTY, QWERTZ, and other regional arrangements. It appears on Logitech keyboards that ship in multiple regional variants. The declared layout tells host software which key-cap legends are physically present, which matters for key-remapping UIs and on-screen overlays. Associating a layout with each paired host is the job of `0x4540` keyboardInternationalLayouts.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x4540 keyboardInternationalLayouts where applicable.
</Callout>

## What it does [#what-it-does]

* Queries the layout stored in the device, typically a locale or region code naming the physical key arrangement.
* Some versions can set or override the reported layout, for keyboards whose firmware default does not match the key caps actually fitted.
* The layout is informational: it does not alter which HID scan codes the device generates.
* `0x4521` disableKeys and `0x4522` disableKeysByUsage act on individual keys; this feature concerns the layout as a whole.
* For per-host layout preferences across a multi-host device, prefer `0x4540` keyboardInternationalLayouts.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x4520` · &#x2A;*See also:** 0x4540 keyboardInternationalLayouts


# 0x4521 · disableKeys (/hidpp/features/x4521-disable-keys)



Disables a fixed set of lock and system keys. `getCapabilities` returns the
`DisableableKeys` bitmask of keys the device allows software to control.
`getDisabledKeys` reads the currently active mask; `setDisabledKeys` replaces
it atomically and echoes the accepted value back; passing an empty mask
re-enables every key. The device rejects any key flag it cannot disable.

The five controllable keys are:

* **CAPS\_LOCK** — Caps Lock.
* **NUM\_LOCK** — Num Lock.
* **SCROLL\_LOCK** — Scroll Lock.
* **INSERT** — Insert.
* **WINDOWS** — Windows / Start key.

For disabling arbitrary keys by HID usage, see `0x4522` disableKeysByUsage.

> **Spec:** Logitech HID++ 2.0 — *disableKeys*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `DisableKeysFeature` wrapper (`0x4521`) exposes:

### Methods [#methods]

| Function            | HID++ fn | Signature                 | Returns           |
| ------------------- | -------- | ------------------------- | ----------------- |
| `get_capabilities`  | 0        | `()`                      | `DisableableKeys` |
| `get_disabled_keys` | 1        | `()`                      | `DisableableKeys` |
| `set_disabled_keys` | 2        | `(keys: DisableableKeys)` | `DisableableKeys` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `DisableableKeys` [#disableablekeys]

The set of keys a `DisableKeysFeature` device can disable. Used both for the device's capabilities and for the currently disabled keys.

| Flag          | Bit/Value | Description              |
| ------------- | --------- | ------------------------ |
| `CAPS_LOCK`   | `1 << 0`  | The Caps Lock key.       |
| `NUM_LOCK`    | `1 << 1`  | The Num Lock key.        |
| `SCROLL_LOCK` | `1 << 2`  | The Scroll Lock key.     |
| `INSERT`      | `1 << 3`  | The Insert key.          |
| `WINDOWS`     | `1 << 4`  | The Windows / Start key. |

## Wire format [#wire-format]

All three functions carry a short 3-byte request payload and return a 16-byte long-payload response. Only the first response byte is meaningful in every case.

### `get_capabilities` (fn 0) [#get_capabilities-fn-0]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are padding.

Response (byte → field):

| Byte | Field                     | Notes                                                                                               |
| ---- | ------------------------- | --------------------------------------------------------------------------------------------------- |
| 0    | `DisableableKeys` bitmask | Bit 0 = `CAPS_LOCK`, bit 1 = `NUM_LOCK`, bit 2 = `SCROLL_LOCK`, bit 3 = `INSERT`, bit 4 = `WINDOWS` |
| 1–15 | —                         | Unused                                                                                              |

### `get_disabled_keys` (fn 1) [#get_disabled_keys-fn-1]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are padding.

Response (byte → field):

| Byte | Field                     | Notes                                                        |
| ---- | ------------------------- | ------------------------------------------------------------ |
| 0    | `DisableableKeys` bitmask | Currently active disabled-key mask; same bit layout as above |
| 1–15 | —                         | Unused                                                       |

### `set_disabled_keys` (fn 2) [#set_disabled_keys-fn-2]

Request: `[keys, 0x00, 0x00]`

| Byte | Source        | Notes                                                    |
| ---- | ------------- | -------------------------------------------------------- |
| 0    | `keys.bits()` | Full replacement mask; pass `0x00` to re-enable all keys |
| 1–2  | `0x00`        | Padding                                                  |

Response (byte → field):

| Byte | Field                     | Notes                                                                    |
| ---- | ------------------------- | ------------------------------------------------------------------------ |
| 0    | `DisableableKeys` bitmask | Device echo of the accepted mask (may differ if some bits were rejected) |
| 1–15 | —                         | Unused                                                                   |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::disable_keys::{DisableKeysFeature, DisableableKeys}};

// mut device: Device, obtained via Device::new(arc_channel, device_index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<DisableKeysFeature>() {
    // Query which keys this device can disable
    let caps = feat.get_capabilities().await?;
    println!("Disableable keys: {:?}", caps);

    // Disable Caps Lock and the Windows key
    let mask = DisableableKeys::CAPS_LOCK | DisableableKeys::WINDOWS;
    let accepted = feat.set_disabled_keys(mask).await?;
    println!("Accepted mask: {:?}", accepted);

    // Read back the current state
    let current = feat.get_disabled_keys().await?;
    println!("Currently disabled: {:?}", current);
}
```


# 0x4522 · disableKeysByUsage (/hidpp/features/x4522-disable-keys-by-usage)



Selectively disable arbitrary keyboard keys by their 8-bit HID usage code. Unlike `0x4521`
(`disableKeys`), which toggles a fixed set of lock keys, this feature operates on any usage in the
standard keyboard page and accumulates changes across calls. `getCapabilities` (function 0) reports
the maximum number of usages the device can hold disabled at once.

`disableKeys` (function 1) adds usages to the disabled set without replacing it; `enableKeys`
(function 2) removes specific usages from the set; `enableAllKeys` (function 3) clears the entire
set in one call. Usages are sent in long-report packets of up to 16 bytes each, with `0x00` acting
as the list terminator; it cannot itself be disabled.

> **Spec:** Logitech HID++ 2.0 — *disableKeysByUsage*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `DisableKeysByUsageFeature` wrapper (`0x4522`) exposes:

### Methods [#methods]

| Function           | HID++ fn | Signature         | Returns |
| ------------------ | -------- | ----------------- | ------- |
| `get_capabilities` | 0        | `()`              | `u8`    |
| `disable_keys`     | 1        | `(usages: &[u8])` | `()`    |
| `enable_keys`      | 2        | `(usages: &[u8])` | `()`    |
| `enable_all_keys`  | 3        | `()`              | `()`    |

All methods are `async` and return `Result<…, Hidpp20Error>`.

`disable_keys` and `enable_keys` split the usage slice into long-report packets of up to 16 bytes
each and send them sequentially; the device accumulates the results. A usage value of `0x00` acts
as the list terminator and is rejected with `InvalidArgument` before any packet is sent.

## Wire format [#wire-format]

`get_capabilities` and `enable_all_keys` use the 7-byte short report (3-byte payload). `disable_keys`
and `enable_keys` use the 20-byte long report (16-byte payload) and may issue multiple requests for
large usage lists. Responses are always read from the 16-byte extended payload (`extend_payload()`).

### `get_capabilities` (fn 0) [#get_capabilities-fn-0]

Request: `[0x00, 0x00, 0x00]` — no arguments.

Response (byte → field):

| Byte | Field        | Notes                                                                |
| ---- | ------------ | -------------------------------------------------------------------- |
| 0    | `max_usages` | Maximum number of usages the device can hold disabled simultaneously |
| 1–15 | —            | Unused / zero                                                        |

### `disable_keys` (fn 1) [#disable_keys-fn-1]

One long-report request is sent per chunk of up to 16 usages. Requests are sent sequentially; the device accumulates all received usages into its disabled set.

Request (16-byte long payload per packet):

| Bytes | Field           | Notes                                                                                                                                                   |
| ----- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0–15  | `usages[0..16]` | Up to 16 HID usage bytes; remaining bytes in a partial chunk are `0x00` (end-of-list terminator). A full 16-byte packet carries no explicit terminator. |

Response: not inspected by the crate (the call succeeds if no error is returned).

### `enable_keys` (fn 2) [#enable_keys-fn-2]

Same wire layout as `disable_keys` (fn 1), but removes the listed usages from the disabled set instead of adding them. Enabling a usage that is not currently disabled is a no-op on the device.

Request (16-byte long payload per packet):

| Bytes | Field           | Notes                                       |
| ----- | --------------- | ------------------------------------------- |
| 0–15  | `usages[0..16]` | Same packetization rules as `disable_keys`. |

Response: not inspected by the crate.

### `enable_all_keys` (fn 3) [#enable_all_keys-fn-3]

Request: `[0x00, 0x00, 0x00]` — no arguments; clears the entire disabled-usage set in one call.

Response: not inspected by the crate.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::disable_keys_by_usage::DisableKeysByUsageFeature};

// device: mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<DisableKeysByUsageFeature>() {
    // Query how many usages the device can disable simultaneously.
    let max_usages = feat.get_capabilities().await?;
    println!("Device supports up to {max_usages} disabled usages");

    // Disable F1–F3 (HID usages 0x3a, 0x3b, 0x3c) — cumulative, does not clear existing set.
    feat.disable_keys(&[0x3a, 0x3b, 0x3c]).await?;

    // Re-enable F2 only.
    feat.enable_keys(&[0x3b]).await?;

    // Clear all disabled keys at once.
    feat.enable_all_keys().await?;
}
```


# 0x4530 · dualPlatform (/hidpp/features/x4530-dual-platform)



The **dualPlatform** feature lets a device persist its HID key-code table across two OS families. The selection is set during pairing or by short-pressing a dedicated OS-selection button. `getPlatform` (function 1) reads the active setting; `setPlatform` (function 2) writes it and echoes the committed value back; it does not trigger a `PlatformChanged` notification. A `PlatformChanged` event is emitted only when the user switches via the hardware button.

`0x4530` is the predecessor of `0x4531` `multiPlatform`; a device exposing `0x4531` should be driven through that feature instead.

* **`IosOrMac`** (`0`) — iOS or macOS HID key-code table.
* **`AndroidOrWindows`** (`1`) — Android or Windows HID key-code table.

> **Spec:** Logitech HID++ 2.0 — *dualPlatform*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `DualPlatformFeature` wrapper (`0x4530`) exposes:

### Methods [#methods]

| Function       | HID++ fn | Signature                           | Returns                                      |
| -------------- | -------- | ----------------------------------- | -------------------------------------------- |
| `get_platform` | 1        | `()`                                | `DualPlatformSelection`                      |
| `set_platform` | 2        | `(platform: DualPlatformSelection)` | `DualPlatformSelection`                      |
| `listen`       | —        | `()`                                | `async_channel::Receiver<DualPlatformEvent>` |

`get_platform` and `set_platform` are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and infallible.

### Types [#types]

#### `DualPlatformSelection` [#dualplatformselection]

The platform a device is configured for. The selection is persistent, chosen during pairing or by short-pressing an OS-selection button; there is no default.

| Variant            | Value | Description                            |
| ------------------ | ----- | -------------------------------------- |
| `IosOrMac`         | `0`   | iOS or macOS HID key-code table.       |
| `AndroidOrWindows` | `1`   | Android or Windows HID key-code table. |

### Events [#events]

`DualPlatformFeature` implements `EmittingFeature<DualPlatformEvent>`. Call `listen()` to obtain an `async_channel::Receiver<DualPlatformEvent>` that receives hardware-button switch events. `set_platform` does **not** trigger this event.

| Variant           | Payload                 | Description                                               |
| ----------------- | ----------------------- | --------------------------------------------------------- |
| `PlatformChanged` | `DualPlatformSelection` | The user changed the platform via an OS-selection button. |

## Wire format [#wire-format]

Each request carries a 3-byte payload; responses are read from the returned long payload via `extend_payload()`. The event payload is parsed from the raw HID++ message.

### `get_platform` (fn 1) [#get_platform-fn-1]

Request: `[0x00, 0x00, 0x00]` — all bytes are unused padding.

Response (byte → field):

| Byte | Field                   | Notes                                      |
| ---- | ----------------------- | ------------------------------------------ |
| 0    | `DualPlatformSelection` | `0` = `IosOrMac`, `1` = `AndroidOrWindows` |

### `set_platform` (fn 2) [#set_platform-fn-2]

Request: `[platform, 0x00, 0x00]` — byte 0 is the `u8` representation of the new `DualPlatformSelection`; bytes 1–2 are unused padding.

Response (byte → field):

| Byte | Field                   | Notes                              |
| ---- | ----------------------- | ---------------------------------- |
| 0    | `DualPlatformSelection` | Device echo of the committed value |

### `PlatformChanged` event (sub-id 0) [#platformchanged-event-sub-id-0]

Emitted by the hardware OS-selection button. Decoded from the raw HID++ event message when the feature index and `func.to_lo() == 0` match.

| Byte | Field                   | Notes                                      |
| ---- | ----------------------- | ------------------------------------------ |
| 0    | `DualPlatformSelection` | `0` = `IosOrMac`, `1` = `AndroidOrWindows` |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::dual_platform::{DualPlatformFeature, DualPlatformEvent, DualPlatformSelection}};

// chan: Arc<HidppChannel>, device_index: u8
let mut device = Device::new(chan, device_index).await?;
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<DualPlatformFeature>() {
    // Read the current platform setting.
    let current = feat.get_platform().await?;
    println!("Current platform: {:?}", current);

    // Switch to Android/Windows key-code table and confirm the echo.
    let committed = feat.set_platform(DualPlatformSelection::AndroidOrWindows).await?;
    println!("Committed: {:?}", committed);

    // Listen for hardware-button platform-switch events.
    let rx = feat.listen();
    if let Ok(DualPlatformEvent::PlatformChanged(p)) = rx.recv().await {
        println!("Hardware switch → {:?}", p);
    }
}
```


# 0x4531 · multiPlatform (/hidpp/features/x4531-multi-platform)



The **MultiPlatform** feature lets a multi-host device associate each of its host slots with a specific OS platform, allowing the firmware to apply per-host key-mapping and behaviour adjustments automatically. `getFeatureInfos` reports feature capabilities, the number of available platforms and descriptor rows, the host count, and the currently active host slot. `getPlatformDescriptor` fetches a single descriptor row, which encodes the covered operating systems (`OsMask`) and the OS-version range the row applies to. `getHostPlatform` reads the platform currently assigned to a given host slot along with how that selection was made (`PlatformSource`).

* **`MultiPlatformCapabilities`** — `OS_DETECTION` (device can auto-detect the host OS), `SET_HOST_PLATFORM` (host software may override the selection).
* **`OsMask`** — bitfield covering Windows, WindowsEmbedded, Linux, ChromeOS, Android, macOS, iOS, webOS, and Tizen.
* **`PlatformSource`** — `Default`, `Auto` (device-detected), `Manual` (set on-device), or `Software` (set by host software).

> **Spec:** Logitech HID++ 2.0 — *multiPlatform*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `MultiPlatformFeature` wrapper (`0x4531`) exposes:

### Methods [#methods]

| Function                  | HID++ fn | Signature                | Returns              |
| ------------------------- | -------- | ------------------------ | -------------------- |
| `get_feature_infos`       | 0        | `()`                     | `MultiPlatformInfo`  |
| `get_platform_descriptor` | 1        | `(descriptor_index: u8)` | `PlatformDescriptor` |
| `get_host_platform`       | 2        | `(host: HostIndex)`      | `HostPlatform`       |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `MultiPlatformInfo` [#multiplatforminfo]

Static feature information returned by `get_feature_infos`.

| Field                   | Type                        | Description                                   |
| ----------------------- | --------------------------- | --------------------------------------------- |
| `capabilities`          | `MultiPlatformCapabilities` | Feature capabilities.                         |
| `platform_count`        | `u8`                        | Number of platform IDs.                       |
| `descriptor_count`      | `u8`                        | Number of platform descriptor rows.           |
| `host_count`            | `u8`                        | Number of host slots.                         |
| `current_host`          | `HostIndex`                 | Current host slot.                            |
| `current_host_platform` | `Option<u8>`                | Platform index selected for the current host. |

#### `MultiPlatformCapabilities` [#multiplatformcapabilities]

Capabilities reported by `MultiPlatform`.

| Flag                | Bit/Value | Description                                      |
| ------------------- | --------- | ------------------------------------------------ |
| `OS_DETECTION`      | `1 << 0`  | The device can detect the host OS automatically. |
| `SET_HOST_PLATFORM` | `1 << 1`  | Software can set the host platform.              |

#### `PlatformDescriptor` [#platformdescriptor]

A platform descriptor row returned by `get_platform_descriptor`.

| Field              | Type     | Description                                |
| ------------------ | -------- | ------------------------------------------ |
| `platform_index`   | `u8`     | Platform index this descriptor belongs to. |
| `descriptor_index` | `u8`     | Descriptor row index.                      |
| `os_mask`          | `OsMask` | Covered operating systems.                 |
| `from_version`     | `u8`     | First supported OS major version.          |
| `from_revision`    | `u8`     | First supported OS revision.               |
| `to_version`       | `u8`     | Last supported OS major version.           |
| `to_revision`      | `u8`     | Last supported OS revision.                |

#### `OsMask` [#osmask]

Operating systems covered by a platform descriptor.

| Flag               | Bit/Value | Description        |
| ------------------ | --------- | ------------------ |
| `WINDOWS`          | `1 << 0`  | Microsoft Windows. |
| `WINDOWS_EMBEDDED` | `1 << 1`  | Windows Embedded.  |
| `LINUX`            | `1 << 2`  | Linux.             |
| `CHROME`           | `1 << 3`  | ChromeOS.          |
| `ANDROID`          | `1 << 4`  | Android.           |
| `MACOS`            | `1 << 5`  | macOS.             |
| `IOS`              | `1 << 6`  | iOS.               |
| `WEBOS`            | `1 << 7`  | webOS.             |
| `TIZEN`            | `1 << 8`  | Tizen.             |

#### `HostPlatform` [#hostplatform]

Platform selection for a host slot returned by `get_host_platform`.

| Field                   | Type             | Description                                              |
| ----------------------- | ---------------- | -------------------------------------------------------- |
| `host_index`            | `HostIndex`      | Host slot index returned by the device.                  |
| `status`                | `u8`             | Raw host status byte.                                    |
| `platform_index`        | `Option<u8>`     | Selected platform, or `None` when undefined.             |
| `source`                | `PlatformSource` | Source of the platform selection.                        |
| `auto_platform_index`   | `Option<u8>`     | Automatically detected platform, if available.           |
| `auto_descriptor_index` | `Option<u8>`     | Automatically matched platform descriptor, if available. |

#### `PlatformSource` [#platformsource]

Source of a host-platform selection.

| Variant    | Value | Description                           |
| ---------- | ----- | ------------------------------------- |
| `Default`  | 0     | Device default.                       |
| `Auto`     | 1     | Automatically detected by the device. |
| `Manual`   | 2     | Manually selected on the device.      |
| `Software` | 3     | Set by host software.                 |

#### `HostIndex` [#hostindex]

A host slot selector (defined in `hosts_info`).

| Variant    | Value      | Description                                     |
| ---------- | ---------- | ----------------------------------------------- |
| `Current`  | `0xff`     | The host slot currently selected by the device. |
| `Slot(u8)` | `0`–`0xfe` | A zero-based host slot index.                   |

## Wire format [#wire-format]

All three functions use a short 3-byte request payload; responses are read from the 16-byte long payload returned by `extend_payload()`.

### `get_feature_infos` (fn 0) [#get_feature_infos-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                   | Notes                                         |
| ---- | ----------------------- | --------------------------------------------- |
| 0–1  | `capabilities`          | `MultiPlatformCapabilities` as big-endian u16 |
| 2    | `platform_count`        | Number of platform IDs                        |
| 3    | `descriptor_count`      | Number of descriptor rows                     |
| 4    | `host_count`            | Number of host slots                          |
| 5    | `current_host`          | `HostIndex::from(byte)` — `0xff` → `Current`  |
| 6    | `current_host_platform` | `0xff` → `None`; otherwise `Some(byte)`       |

### `get_platform_descriptor` (fn 1) [#get_platform_descriptor-fn-1]

Request: `[descriptor_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field              | Notes                                    |
| ---- | ------------------ | ---------------------------------------- |
| 0    | `platform_index`   | Platform index the descriptor belongs to |
| 1    | `descriptor_index` | Descriptor row index (echoed back)       |
| 2–3  | `os_mask`          | `OsMask` as big-endian u16               |
| 4    | `from_version`     | First supported OS major version         |
| 5    | `from_revision`    | First supported OS revision              |
| 6    | `to_version`       | Last supported OS major version          |
| 7    | `to_revision`      | Last supported OS revision               |

### `get_host_platform` (fn 2) [#get_host_platform-fn-2]

Request: `[u8::from(host), 0x00, 0x00]` — `HostIndex::Current` encodes as `0xff`; `HostIndex::Slot(n)` encodes as `n`.

Response (byte → field):

| Byte | Field                   | Notes                                                              |
| ---- | ----------------------- | ------------------------------------------------------------------ |
| 0    | `host_index`            | `HostIndex::from(byte)`                                            |
| 1    | `status`                | Raw host status byte                                               |
| 2    | `platform_index`        | `0xff` → `None`; otherwise `Some(byte)`                            |
| 3    | `source`                | `PlatformSource` — 0 = Default, 1 = Auto, 2 = Manual, 3 = Software |
| 4    | `auto_platform_index`   | `0xff` → `None`; otherwise `Some(byte)`                            |
| 5    | `auto_descriptor_index` | `0xff` → `None`; otherwise `Some(byte)`                            |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::multi_platform::{MultiPlatformFeature, OsMask},
    feature::hosts_info::HostIndex,
};

// mut device: Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<MultiPlatformFeature>() {
    let info = feat.get_feature_infos().await?;
    println!("hosts={}, platforms={}", info.host_count, info.platform_count);

    // Read the descriptor at index 0
    let desc = feat.get_platform_descriptor(0).await?;
    println!("os_mask={:?}, versions={}.{}–{}.{}", desc.os_mask,
        desc.from_version, desc.from_revision, desc.to_version, desc.to_revision);

    // Read the platform for the current host
    let hp = feat.get_host_platform(HostIndex::Current).await?;
    println!("platform={:?}, source={:?}", hp.platform_index, hp.source);
}
```


# 0x4600 · crown (/hidpp/features/x4600-crown)



The **Crown** feature controls the rotary crown on the MX Master series.
`get_info` returns the hardware's `CrownControlCapabilities` (button, long-press,
mechanized ratchet, configurable rotation timeout, short-long timeout, and
double-tap speed) plus `CrownSensorCapabilities` (proximity, touch, tap, and
double-tap gestures), together with the slot and ratchet counts per revolution.
`get_mode` and `set_mode` read and write the active `CrownMode`: whether events
are delivered over the native HID channel or diverted into HID++
(`ReportingMode`), the ratchet vs. free-spin choice (`RatchetMode`), and three
configurable timings: rotation timeout, short-long press threshold, and
double-tap speed, all in 10 ms steps. Every `SetCrownMode` field uses a zero /
`NoChange` sentinel so individual settings can be updated without disturbing the
others.

While diverted, the crown emits a single `CrownEvent::Update` carrying a
`CrownUpdate` snapshot:

* **`rotation_state`*&#x2A; / &#x2A;*`relative_slot_rotation`*&#x2A; / &#x2A;*`relative_ratchet_rotation`*&#x2A; / &#x2A;*`speed`** — `RotationState` phase, signed delta slot and ratchet counts, and current speed in slots/s.
* **`proximity`*&#x2A; / &#x2A;*`touch`** — `ActivityState` lifecycle phases (`Start` → `Active` → `Stop`).
* **`gesture`** — `CrownGesture`: `Tap`, `DoubleTap`, or `None`.
* **`button`** — `ButtonState`: `Inactive`, `Press`, `ShortPressActive`, `LongPress`, `LongPressActive`, or `Release`.

> **Spec:** Logitech HID++ 2.0 — *0x4600 crown*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `CrownFeature` wrapper (`0x4600`) exposes:

### Methods [#methods]

| Function   | HID++ fn | Signature              | Returns                |
| ---------- | -------- | ---------------------- | ---------------------- |
| `get_info` | 0        | `()`                   | `CrownInfo`            |
| `get_mode` | 1        | `()`                   | `CrownMode`            |
| `set_mode` | 2        | `(mode: SetCrownMode)` | `CrownMode`            |
| `listen`   | event    | `()`                   | `Receiver<CrownEvent>` |

`get_info`, `get_mode`, and `set_mode` are `async` and return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns an `async_channel::Receiver<CrownEvent>` directly.

### Types [#types]

#### `CrownInfo` [#crowninfo]

Crown capabilities and hardware constants returned by `get_info`.

| Field      | Type                       | Description                        |
| ---------- | -------------------------- | ---------------------------------- |
| `controls` | `CrownControlCapabilities` | Control capabilities.              |
| `sensors`  | `CrownSensorCapabilities`  | Sensor capabilities.               |
| `slots`    | `u16`                      | Number of slots per revolution.    |
| `ratchets` | `u16`                      | Number of ratchets per revolution. |

#### `CrownControlCapabilities` [#crowncontrolcapabilities]

Crown control capabilities, from `get_info`.

| Flag                              | Bit/Value | Description                                    |
| --------------------------------- | --------- | ---------------------------------------------- |
| `BUTTON`                          | `1 << 0`  | The crown has a button.                        |
| `BUTTON_LONG_PRESS`               | `1 << 1`  | The button reports long presses.               |
| `MECHANIZED_RATCHET`              | `1 << 2`  | The ratchet is mechanized (no manual control). |
| `ROTATION_TIMEOUT_CONFIGURABLE`   | `1 << 3`  | The rotation timeout is configurable.          |
| `SHORT_LONG_TIMEOUT_CONFIGURABLE` | `1 << 4`  | The short-long timeout is configurable.        |
| `DOUBLE_TAP_SPEED_CONFIGURABLE`   | `1 << 5`  | The double-tap speed is configurable.          |

#### `CrownSensorCapabilities` [#crownsensorcapabilities]

Crown sensor capabilities, from `get_info`.

| Flag                 | Bit/Value | Description                            |
| -------------------- | --------- | -------------------------------------- |
| `PROXIMITY`          | `1 << 0`  | The crown has a proximity sensor.      |
| `TOUCH`              | `1 << 1`  | The crown has a touch sensor.          |
| `TAP_GESTURE`        | `1 << 2`  | The crown detects tap gestures.        |
| `DOUBLE_TAP_GESTURE` | `1 << 3`  | The crown detects double-tap gestures. |

#### `CrownMode` [#crownmode]

The crown's current mode, returned by `get_mode` and echoed by `set_mode`.

| Field                | Type            | Description                               |
| -------------------- | --------------- | ----------------------------------------- |
| `diverting`          | `ReportingMode` | How events are reported.                  |
| `ratchet_mode`       | `RatchetMode`   | Ratchet mode.                             |
| `rotation_timeout`   | `u8`            | Rotation timeout, in 10 ms steps.         |
| `short_long_timeout` | `u8`            | Short-long press timeout, in 10 ms steps. |
| `double_tap_speed`   | `u8`            | Double-tap speed, in 10 ms steps.         |

#### `SetCrownMode` [#setcrownmode]

Mode settings written by `set_mode`. Every field uses `0` / `ReportingMode::NoChange` / `RatchetMode::NoChange` as a "leave unchanged" sentinel. The rotation timeout is clipped to `0x40`.

| Field                | Type            | Description                                                   |
| -------------------- | --------------- | ------------------------------------------------------------- |
| `diverting`          | `ReportingMode` | How events are reported, or `ReportingMode::NoChange`.        |
| `ratchet_mode`       | `RatchetMode`   | Ratchet mode, or `RatchetMode::NoChange`.                     |
| `rotation_timeout`   | `u8`            | Rotation timeout in 10 ms steps, or `0` to leave unchanged.   |
| `short_long_timeout` | `u8`            | Short-long timeout in 10 ms steps, or `0` to leave unchanged. |
| `double_tap_speed`   | `u8`            | Double-tap speed in 10 ms steps, or `0` to leave unchanged.   |

#### `ReportingMode` [#reportingmode]

How crown events are reported.

| Variant    | Value | Description                                               |
| ---------- | ----- | --------------------------------------------------------- |
| `NoChange` | 0     | Leave the setting unchanged (write-only sentinel).        |
| `Hid`      | 1     | Events go to the native HID channel.                      |
| `Diverted` | 2     | Events are diverted to HID++ (required for `CrownEvent`). |

#### `RatchetMode` [#ratchetmode]

The crown's ratchet mode.

| Variant    | Value | Description                                        |
| ---------- | ----- | -------------------------------------------------- |
| `NoChange` | 0     | Leave the setting unchanged (write-only sentinel). |
| `Free`     | 1     | Free-spinning mode.                                |
| `Ratchet`  | 2     | Ratchet (detented) mode.                           |

#### `RotationState` [#rotationstate]

Rotation phase reported in a `CrownUpdate`.

| Variant    | Value | Description                     |
| ---------- | ----- | ------------------------------- |
| `Inactive` | 0     | Not rotating (or not diverted). |
| `Start`    | 1     | Rotation started.               |
| `Active`   | 2     | Rotation ongoing.               |
| `Stop`     | 3     | Rotation stopped.               |

#### `ActivityState` [#activitystate]

Proximity or touch activity phase reported in a `CrownUpdate`.

| Variant    | Value | Description |
| ---------- | ----- | ----------- |
| `Inactive` | 0     | Inactive.   |
| `Start`    | 1     | Started.    |
| `Active`   | 2     | Ongoing.    |
| `Stop`     | 3     | Stopped.    |

#### `CrownGesture` [#crowngesture]

Touch gesture reported in a `CrownUpdate`.

| Variant     | Value | Description |
| ----------- | ----- | ----------- |
| `None`      | 0     | No gesture. |
| `Tap`       | 1     | Single tap. |
| `DoubleTap` | 2     | Double tap. |

#### `ButtonState` [#buttonstate]

Crown button state reported in a `CrownUpdate`.

| Variant            | Value | Description                            |
| ------------------ | ----- | -------------------------------------- |
| `Inactive`         | 0     | Inactive (or not diverted).            |
| `Press`            | 1     | Press started.                         |
| `ShortPressActive` | 2     | Short press active.                    |
| `LongPress`        | 3     | Long press reached the time threshold. |
| `LongPressActive`  | 4     | Long press active.                     |
| `Release`          | 5     | Released.                              |

### Events [#events]

`CrownFeature` implements `EmittingFeature<CrownEvent>`; call `listen()` to receive a channel of `CrownEvent` values. Events are only delivered while the crown is diverted (`ReportingMode::Diverted`).

#### `CrownEvent` [#crownevent]

| Variant               | Description                                                               |
| --------------------- | ------------------------------------------------------------------------- |
| `Update(CrownUpdate)` | The crown's rotation, proximity, touch, gesture, or button state changed. |

#### `CrownUpdate` [#crownupdate]

Payload of `CrownEvent::Update`.

| Field                       | Type            | Description                                           |
| --------------------------- | --------------- | ----------------------------------------------------- |
| `rotation_state`            | `RotationState` | Current rotation phase.                               |
| `relative_slot_rotation`    | `i8`            | Slots rotated since the last event (`-127..=127`).    |
| `relative_ratchet_rotation` | `i8`            | Ratchets rotated since the last event (`-127..=127`). |
| `proximity`                 | `ActivityState` | Proximity-sensor phase.                               |
| `touch`                     | `ActivityState` | Touch-sensor phase.                                   |
| `gesture`                   | `CrownGesture`  | Touch gesture detected.                               |
| `button`                    | `ButtonState`   | Button state.                                         |
| `speed`                     | `i16`           | Crown speed in slots per second (signed).             |

## Wire format [#wire-format]

Getter requests carry a 3-byte zero payload (`call`). `set_mode` uses a 16-byte long payload (`call_long`). All responses are read from the 16-byte extended payload returned by `extend_payload()`. Events arrive as unsolicited 16-byte messages on function index 0.

### `get_info` (fn 0) [#get_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field      | Notes                                      |
| ---- | ---------- | ------------------------------------------ |
| 0    | `controls` | `CrownControlCapabilities` bitfield        |
| 1    | `sensors`  | `CrownSensorCapabilities` bitfield         |
| 2–3  | `slots`    | Big-endian `u16` — slots per revolution    |
| 4–5  | `ratchets` | Big-endian `u16` — ratchets per revolution |

### `get_mode` (fn 1) [#get_mode-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field                | Notes                                                      |
| ---- | -------------------- | ---------------------------------------------------------- |
| 0    | `diverting`          | `ReportingMode` enum (`0`=NoChange, `1`=Hid, `2`=Diverted) |
| 1    | `ratchet_mode`       | `RatchetMode` enum (`0`=NoChange, `1`=Free, `2`=Ratchet)   |
| 2    | `rotation_timeout`   | u8, in 10 ms steps                                         |
| 3    | `short_long_timeout` | u8, in 10 ms steps                                         |
| 4    | `double_tap_speed`   | u8, in 10 ms steps                                         |

### `set_mode` (fn 2) [#set_mode-fn-2]

Request (16-byte long payload — bytes 5–15 are `0x00`):

| Byte | Field                | Notes                 |
| ---- | -------------------- | --------------------- |
| 0    | `diverting`          | `ReportingMode as u8` |
| 1    | `ratchet_mode`       | `RatchetMode as u8`   |
| 2    | `rotation_timeout`   | u8, in 10 ms steps    |
| 3    | `short_long_timeout` | u8, in 10 ms steps    |
| 4    | `double_tap_speed`   | u8, in 10 ms steps    |

Response: same layout as `get_mode` — the device echoes the resulting active mode.

### `CrownEvent::Update` (event fn 0) [#crowneventupdate-event-fn-0]

Unsolicited event emitted while the crown is diverted:

| Byte  | Field                       | Notes                                        |
| ----- | --------------------------- | -------------------------------------------- |
| 0     | `rotation_state`            | `RotationState` enum                         |
| 1     | `relative_slot_rotation`    | `i8` — signed slot delta since last event    |
| 2     | `relative_ratchet_rotation` | `i8` — signed ratchet delta since last event |
| 3     | `proximity`                 | `ActivityState` enum                         |
| 4     | `touch`                     | `ActivityState` enum                         |
| 5     | `gesture`                   | `CrownGesture` enum                          |
| 6     | `button`                    | `ButtonState` enum                           |
| 7–13  | *(reserved)*                | not decoded by the driver                    |
| 14–15 | `speed`                     | Big-endian `i16` — slots per second          |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::crown::{CrownFeature, ReportingMode, RatchetMode, SetCrownMode},
};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<CrownFeature>() {
    let info = feat.get_info().await?;
    println!("slots/rev={}, ratchets/rev={}", info.slots, info.ratchets);

    // Divert crown events to HID++ and switch to ratchet mode
    let mode = feat.set_mode(SetCrownMode {
        diverting: ReportingMode::Diverted,
        ratchet_mode: RatchetMode::Ratchet,
        rotation_timeout: 0,    // leave unchanged
        short_long_timeout: 0,  // leave unchanged
        double_tap_speed: 0,    // leave unchanged
    }).await?;
    println!("active mode: {:?}", mode.diverting);

    // Receive crown events
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("{:?}", event);
    }
}
```


# 0x6020 · tapEnable (/hidpp/features/x6020-tap-enable)



Controls whether tap-to-click is active on an integrated or standalone touchpad.
This feature is found on Logitech touchpad devices and laptops with a Logitech
touchpad module. It sits alongside other touchpad features in the 0x6xxx range,
such as `0x6100` touchpadRawXY and `0x6110` touchMouseRaw, but addresses
higher-level gesture recognition rather than raw sensor data.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* Reads whether tap-to-click is currently enabled.
* Enables or disables tap-to-click, overriding the firmware default or any
  earlier host-set value.
* Some devices add persistence controls, so the setting survives power cycles,
  or session-scoped override semantics.

It exists because the OS touchpad driver alone may not be able to manage the
tap behaviour.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x6020`


# 0x6100 · touchpadRawXY (/hidpp/features/x6100-touchpad-raw-xy)



Raw multi-touch data from an integrated touchpad. `getTouchpadInfo` (function 0)
returns static pad characteristics: physical dimensions (`x_size` / `y_size` in
native units), `max_finger_count`, native sensor `dpi`, coordinate `origin`, and
pen-input support. `getRawReportState` / `setRawReportState` (functions 1–2) read
and write the active reporting flags; setting `RawReportFlags::RAW` enables the
`DualXy` event stream.

Each `DualXy` event carries a `DualXyData` frame: a running `timestamp`, up to two
`TouchPoint` contacts, a physical-button flag, an `end_of_frame` flag, and the
total `finger_count`. Frames covering more than two fingers are split across
consecutive events sharing the same timestamp. The `z` and `area` bytes of each
`TouchPoint` are mode-dependent; the active `RawReportFlags` can repurpose them
for 16-bit force, width/height, or major/minor/orientation data.

* **`RawReportFlags`** — bitflags: `RAW` (enable raw reporting), `ENHANCED`,
  `WIDTH_HEIGHT`, `NATIVE_GESTURE`, `MAJOR_MINOR`, `WIDTH_HEIGHT_8BIT`,
  `FORCE_ADD` (deprecated).
* **`Origin`** — coordinate origin viewed from above: `LowerLeft`, `LowerRight`,
  `UpperLeft`, `UpperRight`.
* **`TouchPoint`** — 14-bit `x`/`y` coordinates, 4-bit `finger_id`,
  `contact_type`, `contact_status`, plus mode-dependent `z`/`area`.

> **Spec:** Logitech HID++ 2.0 — *touchpadRawXY*. &#x2A;*Used by:** Typed wrapper in
> `openlogi-hidpp`.

## Function reference [#function-reference]

The `TouchpadRawXyFeature` wrapper (`0x6100`) exposes:

### Methods [#methods]

| Function               | HID++ fn | Signature                 | Returns                      |
| ---------------------- | -------- | ------------------------- | ---------------------------- |
| `get_touchpad_info`    | 0        | `()`                      | `TouchpadInfo`               |
| `get_raw_report_state` | 1        | `()`                      | `RawReportFlags`             |
| `set_raw_report_state` | 2        | `(flags: RawReportFlags)` | `()`                         |
| `listen`               | event    | `()`                      | `Receiver<TouchpadRawEvent>` |

All async methods return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver directly.

### Types [#types]

#### `TouchpadInfo` [#touchpadinfo]

Touchpad characteristics returned by `get_touchpad_info`.

| Field                        | Type     | Description                                    |
| ---------------------------- | -------- | ---------------------------------------------- |
| `x_size`                     | `u16`    | Pad width in native coordinate units.          |
| `y_size`                     | `u16`    | Pad height in native coordinate units.         |
| `z_data_range`               | `u8`     | Z-data range (`0x00` = none, `0x0f` = 16-bit). |
| `area_data_range`            | `u8`     | Area-data range (`0x0f` = 16-bit).             |
| `timestamp_units`            | `u8`     | Timestamp increment, in units of 0.1 ms.       |
| `max_finger_count`           | `u8`     | Maximum number of fingers that can be tracked. |
| `origin`                     | `Origin` | Position of the coordinate origin.             |
| `pen_support`                | `bool`   | Whether pen input is supported.                |
| `raw_report_mapping_version` | `u8`     | Raw-report mapping version.                    |
| `dpi`                        | `u16`    | Native sensor DPI.                             |

#### `RawReportFlags` [#rawreportflags]

Raw-report mode flags used by `get_raw_report_state` and `set_raw_report_state`. Some combinations are mutually exclusive; common valid bitmaps are `0x00` (off), `0x05`, `0x09`, `0x21`, and `0x41`.

| Flag                | Bit/Value | Description                                          |
| ------------------- | --------- | ---------------------------------------------------- |
| `RAW`               | `1 << 0`  | Raw reporting enabled.                               |
| `FORCE_ADD`         | `1 << 1`  | Add force data to 16-bit reporting (deprecated).     |
| `ENHANCED`          | `1 << 2`  | Enhanced reporting enabled.                          |
| `WIDTH_HEIGHT`      | `1 << 3`  | Report width/height instead of area.                 |
| `NATIVE_GESTURE`    | `1 << 4`  | Report native gestures.                              |
| `MAJOR_MINOR`       | `1 << 5`  | Report major/minor/orientation.                      |
| `WIDTH_HEIGHT_8BIT` | `1 << 6`  | Report 8-bit width and height bytes instead of area. |

#### `Origin` [#origin]

The position of a touchpad's coordinate origin, viewed from above.

| Variant      | Value | Description         |
| ------------ | ----- | ------------------- |
| `LowerLeft`  | `1`   | Lower-left corner.  |
| `LowerRight` | `2`   | Lower-right corner. |
| `UpperLeft`  | `3`   | Upper-left corner.  |
| `UpperRight` | `4`   | Upper-right corner. |

#### `DualXyData` [#dualxydata]

A frame of raw touch data carrying up to two touch points. Frames describing more than two fingers are split across several events sharing the same `timestamp`; the last packet sets `end_of_frame`.

| Field          | Type         | Description                                                          |
| -------------- | ------------ | -------------------------------------------------------------------- |
| `timestamp`    | `u16`        | Running frame timestamp (unit from `TouchpadInfo::timestamp_units`). |
| `touch1`       | `TouchPoint` | First touch point.                                                   |
| `touch2`       | `TouchPoint` | Second touch point.                                                  |
| `button`       | `bool`       | Whether the physical switch under the surface is pressed.            |
| `end_of_frame` | `bool`       | Whether this is the last event for the frame.                        |
| `finger_count` | `u8`         | Total number of fingers in the frame.                                |

#### `TouchPoint` [#touchpoint]

One touch point within a `DualXyData` frame. `x`/`y` are 14-bit device coordinates. `z` and `area` are mode-dependent; the active `RawReportFlags` can repurpose them for 16-bit force, width/height, or major/minor data.

| Field            | Type  | Description                                                             |
| ---------------- | ----- | ----------------------------------------------------------------------- |
| `contact_type`   | `u8`  | Contact type (2-bit): `0` = finger, others reserved.                    |
| `contact_status` | `u8`  | Contact status (2-bit): `0` = hover, `1` = touch, others reserved.      |
| `x`              | `u16` | 14-bit X coordinate of the touch centre.                                |
| `y`              | `u16` | 14-bit Y coordinate of the touch centre.                                |
| `finger_id`      | `u8`  | Unique finger ID (4-bit).                                               |
| `z`              | `u8`  | Z distance, or force MSB in 16-bit-force mode (mode-dependent).         |
| `area`           | `u8`  | Touch area, or width/height / major-minor / force LSB (mode-dependent). |

### Events [#events]

`TouchpadRawXyFeature` implements `EmittingFeature<TouchpadRawEvent>`; call `listen()` to obtain an `async_channel::Receiver<TouchpadRawEvent>`. Raw events are only delivered while `RawReportFlags::RAW` is set via `set_raw_report_state`.

#### `TouchpadRawEvent` [#touchpadrawevent]

An event emitted by `TouchpadRawXyFeature`.

| Variant              | Description                                                       |
| -------------------- | ----------------------------------------------------------------- |
| `DualXy(DualXyData)` | A new frame of raw touch data (up to two touch points per event). |

## Wire format [#wire-format]

All three functions use a **3-byte request payload**; responses are read from the **16-byte long payload** returned by the device.

### `get_touchpad_info` (fn 0) [#get_touchpad_info-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte(s) | Field                        | Notes                                         |
| ------- | ---------------------------- | --------------------------------------------- |
| 0–1     | `x_size`                     | Big-endian `u16` pad width in native units    |
| 2–3     | `y_size`                     | Big-endian `u16` pad height in native units   |
| 4       | `z_data_range`               | `0x00` = none, `0x0f` = 16-bit                |
| 5       | `area_data_range`            | `0x0f` = 16-bit                               |
| 6       | `timestamp_units`            | Increment in units of 0.1 ms                  |
| 7       | `max_finger_count`           | Maximum trackable fingers                     |
| 8       | `origin`                     | `Origin` enum: `1`=LowerLeft … `4`=UpperRight |
| 9       | `pen_support`                | Non-zero = pen supported                      |
| 10–11   | *(reserved)*                 | Not read by the crate                         |
| 12      | `raw_report_mapping_version` | Report layout version                         |
| 13–14   | `dpi`                        | Big-endian `u16` native sensor DPI            |
| 15      | *(reserved)*                 | Not read by the crate                         |

### `get_raw_report_state` (fn 1) [#get_raw_report_state-fn-1]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field            | Notes                            |
| ---- | ---------------- | -------------------------------- |
| 0    | `RawReportFlags` | Bitmask; remaining bytes ignored |

### `set_raw_report_state` (fn 2) [#set_raw_report_state-fn-2]

Request: `[flags, 0x00, 0x00]` where `flags` = `RawReportFlags::bits()`.

Response: acknowledged but not read by the crate (`Ok(())`).

### `DualXy` event (sub-id 0) [#dualxy-event-sub-id-0]

Emitted asynchronously with a **16-byte payload**. Both touch points use the same encoding; `coord14(high, low)` = `(high & 0x3f) << 8 | low` (14-bit coordinate).

**Frame-level fields:**

| Byte           | Field          | Decoding                     |
| -------------- | -------------- | ---------------------------- |
| 0–1            | `timestamp`    | Big-endian `u16`             |
| 8 `bit[2]`     | `button`       | `payload[8] & (1 << 2) != 0` |
| 8 `bit[0]`     | `end_of_frame` | `payload[8] & 1 != 0`        |
| 15 `bits[3:0]` | `finger_count` | `payload[15] & 0x0f`         |

**Touch point 1 (`touch1`):**

| Byte              | Field            | Decoding                          |
| ----------------- | ---------------- | --------------------------------- |
| 2 `bits[7:6]`     | `contact_type`   | `payload[2] >> 6` (2-bit)         |
| 2 `bits[5:0]` + 3 | `x`              | `coord14(payload[2], payload[3])` |
| 4 `bits[7:6]`     | `contact_status` | `payload[4] >> 6` (2-bit)         |
| 4 `bits[5:0]` + 5 | `y`              | `coord14(payload[4], payload[5])` |
| 6                 | `z`              | Raw byte (mode-dependent)         |
| 7                 | `area`           | Raw byte (mode-dependent)         |
| 8 `bits[7:4]`     | `finger_id`      | `payload[8] >> 4` (4-bit)         |

**Touch point 2 (`touch2`):**

| Byte                | Field            | Decoding                            |
| ------------------- | ---------------- | ----------------------------------- |
| 9 `bits[7:6]`       | `contact_type`   | `payload[9] >> 6` (2-bit)           |
| 9 `bits[5:0]` + 10  | `x`              | `coord14(payload[9], payload[10])`  |
| 11 `bits[7:6]`      | `contact_status` | `payload[11] >> 6` (2-bit)          |
| 11 `bits[5:0]` + 12 | `y`              | `coord14(payload[11], payload[12])` |
| 13                  | `z`              | Raw byte (mode-dependent)           |
| 14                  | `area`           | Raw byte (mode-dependent)           |
| 15 `bits[7:4]`      | `finger_id`      | `payload[15] >> 4` (4-bit)          |

## Usage (Rust) [#usage-rust]

```rust
use std::sync::Arc;
use hidpp::{channel::HidppChannel, device::Device, feature::touchpad_raw_xy::{TouchpadRawXyFeature, RawReportFlags}};
use hidpp::feature::EmittingFeature;

// channel: Arc<HidppChannel>, already set up
let mut device = Device::new(Arc::clone(&channel), 0x02).await?;
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<TouchpadRawXyFeature>() {
    // Query static touchpad characteristics.
    let info = feat.get_touchpad_info().await?;
    println!("pad {}x{} at {} dpi, up to {} fingers",
        info.x_size, info.y_size, info.dpi, info.max_finger_count);

    // Enable raw reporting and subscribe to touch events.
    feat.set_raw_report_state(RawReportFlags::RAW).await?;
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        println!("{event:?}");
    }
}
```


# 0x6110 · touchMouseRawTouchPoints (/hidpp/features/x6110-touch-mouse-raw)



Exposes the raw touch-point data produced by a touch mouse's pad surface.
`get_touchpad_info` returns the pad characteristics: X/Y extent in dots,
sensor resolution in DPI, coordinate `Origin`, maximum finger count, and the
width/height data range. `get_raw_mode` / `set_raw_mode` control whether the
firmware delivers native gestures, filtered raw data, or one of three
unfiltered modes (including a Z-axis variant).

Once a raw mode is active, the feature emits up to four simultaneous touch
points per `RawData` event, each carrying 12-bit X/Y coordinates and 4-bit
contact-width fields; a lifted finger is `None`. `StatusChanged` reports
`MOUSE_LIFTED` and `BUTTON_DOWN` flag transitions independently of raw-data
reporting.

* **`RawMode`** — `NativeGestures` · `RawFiltered` · `RawUnfilteredAndGestures` · `RawUnfilteredAlways` · `RawUnfilteredWithZ`.
* **`TouchMousePoint`** — 12-bit `x` / `y`, 4-bit `width_x` / `width_y` (replaced by Z in `RawUnfilteredWithZ` mode).
* **`TouchMouseStatus`** — bitflags: `MOUSE_LIFTED`, `BUTTON_DOWN`.
* **`Origin`** — corner of the pad that holds the coordinate origin.

> **Spec:** Logitech HID++ 2.0 — *touchMouseRawTouchPoints*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `TouchMouseRawFeature` wrapper (`0x6110`) exposes:

### Methods [#methods]

| Function            | HID++ fn | Signature         | Returns                                       |
| ------------------- | -------- | ----------------- | --------------------------------------------- |
| `get_touchpad_info` | 0        | `()`              | `TouchMouseInfo`                              |
| `get_raw_mode`      | 1        | `()`              | `RawMode`                                     |
| `set_raw_mode`      | 2        | `(mode: RawMode)` | `()`                                          |
| `listen`            | event    | `()`              | `async_channel::Receiver<TouchMouseRawEvent>` |

All `async` methods return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver.

### Types [#types]

#### `TouchMouseInfo` [#touchmouseinfo]

Touch-mouse characteristics returned by `get_touchpad_info`.

| Field                     | Type     | Description                                           |
| ------------------------- | -------- | ----------------------------------------------------- |
| `x_max_count`             | `u16`    | Maximum X count in dots.                              |
| `y_max_count`             | `u16`    | Maximum Y count in dots.                              |
| `resolution_dpi`          | `u16`    | Sensor resolution in DPI (assumed equal for X and Y). |
| `origin`                  | `Origin` | Position of the coordinate origin.                    |
| `max_finger_count`        | `u8`     | Maximum number of reported fingers.                   |
| `width_height_data_range` | `u8`     | Maximum value of the touch-point width/height data.   |

#### `RawMode` [#rawmode]

The raw-reporting mode of a touch mouse.

| Variant                    | Value | Description                                                               |
| -------------------------- | ----- | ------------------------------------------------------------------------- |
| `NativeGestures`           | 0     | Native gestures only (out of the box).                                    |
| `RawFiltered`              | 1     | Filtered raw data.                                                        |
| `RawUnfilteredAndGestures` | 2     | Unfiltered raw data plus native gestures.                                 |
| `RawUnfilteredAlways`      | 3     | Unfiltered raw data, sent even while lifted or with a button active.      |
| `RawUnfilteredWithZ`       | 4     | Like `RawUnfilteredAndGestures` but with Z information in place of width. |

#### `Origin` [#origin]

The position of the touch surface's coordinate origin, viewed from above.

| Variant      | Value | Description         |
| ------------ | ----- | ------------------- |
| `LowerLeft`  | 1     | Lower-left corner.  |
| `LowerRight` | 2     | Lower-right corner. |
| `UpperLeft`  | 3     | Upper-left corner.  |
| `UpperRight` | 4     | Upper-right corner. |

### Events [#events]

`TouchMouseRawFeature` implements `EmittingFeature<TouchMouseRawEvent>`. Call `listen()` to receive a channel of `TouchMouseRawEvent` values.

#### `TouchMouseRawEvent` [#touchmouserawevent]

| Variant                           | Description                                                                                                                  |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `RawData { touches }`             | Raw touch-point data for up to four simultaneous fingers; a lifted finger is `None`. Only emitted when a raw mode is active. |
| `StatusChanged(TouchMouseStatus)` | A mouse status flag changed.                                                                                                 |

#### `TouchMousePoint` [#touchmousepoint]

A single touch point in a `RawData` event. The finger ID is the point's index (`0..4`) in the `touches` array.

| Field     | Type  | Description                                                       |
| --------- | ----- | ----------------------------------------------------------------- |
| `x`       | `u16` | 12-bit X coordinate.                                              |
| `y`       | `u16` | 12-bit Y coordinate.                                              |
| `width_x` | `u8`  | Contact width along X (4-bit), or Z in `RawUnfilteredWithZ` mode. |
| `width_y` | `u8`  | Contact width along Y (4-bit).                                    |

#### `TouchMouseStatus` [#touchmousestatus]

Mouse status flags carried by `StatusChanged`.

| Flag           | Bit/Value | Description                          |
| -------------- | --------- | ------------------------------------ |
| `MOUSE_LIFTED` | `1 << 0`  | The mouse is lifted off the surface. |
| `BUTTON_DOWN`  | `1 << 1`  | A mouse button is pressed.           |

## Wire format [#wire-format]

Getter requests carry a 3-byte zero payload; `set_raw_mode` encodes the mode in the first byte. All responses use the 16-byte long payload. Events are pushed by the device unsolicited; no request is sent.

### `get_touchpad_info` (fn 0) [#get_touchpad_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte(s) | Field                     | Notes                       |
| ------- | ------------------------- | --------------------------- |
| 0–1     | `x_max_count`             | Big-endian `u16`            |
| 2–3     | `y_max_count`             | Big-endian `u16`            |
| 4–5     | `resolution_dpi`          | Big-endian `u16`, DPI       |
| 6       | `origin`                  | `Origin` enum (`1`–`4`)     |
| 7       | `max_finger_count`        | Raw `u8`                    |
| 8       | `width_height_data_range` | Raw `u8`; bytes 9–15 unused |

### `get_raw_mode` (fn 1) [#get_raw_mode-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field     | Notes                                                          |
| ---- | --------- | -------------------------------------------------------------- |
| 0    | `RawMode` | `0`=NativeGestures … `4`=RawUnfilteredWithZ; bytes 1–15 unused |

### `set_raw_mode` (fn 2) [#set_raw_mode-fn-2]

Request: `[mode, 0x00, 0x00]` — `mode` is the `u8` representation of `RawMode`.

Response: acknowledged (no payload fields used).

### Event — `RawData` (event fn 0) [#event--rawdata-event-fn-0]

The 16-byte payload carries four touch points packed consecutively (4 bytes each, indices `0..3`).

Per touch point (bytes `i*4` … `i*4+3`):

| Offset | Byte          | Field                                                                | Encoding                        |
| ------ | ------------- | -------------------------------------------------------------------- | ------------------------------- |
| 0      | `x_high`      | X coordinate, bits 11–4                                              | `0xff` = finger lifted (`None`) |
| 1      | `y_high`      | Y coordinate, bits 11–4                                              | —                               |
| 2      | `low_nibbles` | X bits 3–0 in `low_nibbles & 0x0f`; Y bits 3–0 in `low_nibbles >> 4` | —                               |
| 3      | `widths`      | `width_x = widths & 0x0f`; `width_y = widths >> 4`                   | 4-bit each                      |

Assembled: `x = (x_high << 4) \| (low_nibbles & 0x0f)` (12-bit); `y = (y_high << 4) \| (low_nibbles >> 4)` (12-bit).

### Event — `StatusChanged` (event fn 1) [#event--statuschanged-event-fn-1]

| Byte | Bit          | Flag           |
| ---- | ------------ | -------------- |
| 0    | 0 (`1 << 0`) | `MOUSE_LIFTED` |
| 0    | 1 (`1 << 1`) | `BUTTON_DOWN`  |

Bytes 1–15 are unused.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::{
        EmittingFeature,
        touch_mouse_raw::{RawMode, TouchMouseRawEvent, TouchMouseRawFeature},
    },
};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<TouchMouseRawFeature>() {
    // Read pad geometry and capabilities.
    let info = feat.get_touchpad_info().await?;
    println!("pad {}x{} dots @ {} DPI", info.x_max_count, info.y_max_count, info.resolution_dpi);

    // Enable filtered raw-data reporting.
    feat.set_raw_mode(RawMode::RawFiltered).await?;

    // Receive raw touch events.
    let rx = feat.listen();
    while let Ok(event) = rx.recv().await {
        match event {
            TouchMouseRawEvent::RawData { touches } => {
                for (id, point) in touches.iter().enumerate() {
                    if let Some(p) = point {
                        println!("finger {} at ({}, {})", id, p.x, p.y);
                    }
                }
            }
            TouchMouseRawEvent::StatusChanged(status) => {
                println!("status: {:?}", status);
            }
        }
    }
}
```


# 0x6500 · gestures (/hidpp/features/x6500-gestures1)



`0x6500` gestures (also written `Gestures1`) is a HID++ 2.0 feature on devices
with touch surfaces: touch mice, touchpads, and multi-touch pointing devices.
The firmware classifies touches (swipes, taps, pinches, and similar
multi-finger interactions) and delivers them to the host as gesture
identifiers rather than raw coordinates. Its successor `0x6501` gestures2 has
a more capable and configurable gesture model; where a device advertises both,
OpenLogi targets `0x6501`.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x6501 gestures2 where applicable.
</Callout>

## What it does [#what-it-does]

* Reports firmware-classified gesture events (swipe direction, tap count,
  pinch/spread) to the host. Raw touch coordinates are the domain of `0x6100`
  touchpadRawXY and `0x6110` touchMouseRawTouchPoints.
* Queries which gesture types the device recognises, and enables or disables
  their delivery to the host.
* Gesture notifications arrive asynchronously with an identifier and
  parameters such as direction or finger count, so host software needs no
  gesture recogniser of its own.
* Some devices may expose per-gesture configuration that maps a gesture to an
  action stored on the device.

`0x6501` gestures2 supersedes it with a richer gesture vocabulary and more
flexible configuration, so `0x6500` mostly turns up on older touch mice and
touchpads.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x6500` · &#x2A;*See also:** 0x6501 gestures2


# 0x6501 · gestures2 (/hidpp/features/x6501-gestures2)



`0x6501` gestures2 is Logitech's legacy gesture-configuration feature: a device
publishes a descriptor table of gesture ids, each flagged as present, enabled,
and — sometimes — divertable to the host.

OpenLogi implements exactly the slice it needs: the **MX Master 2S exposes its
horizontal thumb wheel as gesture id 46** under `0x6501`, not through the newer
dedicated [`0x2150` thumbwheel](/hidpp/features/x2150-thumbwheel) feature. The
wrapper walks the descriptor table to find that gesture, works out its diversion
index, and can divert or restore it.

> **Spec:** Logitech HID++ 2.0 — *x6501 gestures2* (partial). &#x2A;*Used by:** thumb
> wheel capture on legacy MX mice.

## Function reference [#function-reference]

The `Gestures2Feature` wrapper (`0x6501`) exposes:

### Methods [#methods]

| Function                  | HID++ fn  | Signature          | Returns                     |
| ------------------------- | --------- | ------------------ | --------------------------- |
| `thumbwheel`              | 0 (paged) | `()`               | `Option<ThumbwheelGesture>` |
| `has_thumbwheel`          | 0 (paged) | `()`               | `bool`                      |
| `thumbwheel_diverted`     | 0, 3      | `()`               | `Option<bool>`              |
| `set_thumbwheel_diverted` | 0, 4      | `(diverted: bool)` | `bool`                      |

All are `async` and return `Result<…, Hidpp20Error>`. Merely exposing `0x6501`
is not enough to assume a thumb wheel: touchpads and other gesture devices expose
the feature without one, which is why every call starts from the descriptor
table.

### Types [#types]

#### `ThumbwheelGesture` [#thumbwheelgesture]

| Field             | Type          | Description                                                                                                               |
| ----------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `diversion_index` | `Option<u16>` | Sequential index among gestures that advertise the divertable bit. `None` means gesture 46 exists but cannot be diverted. |

#### Constants [#constants]

| Name                    | Value | Description                                     |
| ----------------------- | ----- | ----------------------------------------------- |
| `THUMBWHEEL_GESTURE_ID` | `46`  | The gestures2 id of the horizontal thumb wheel. |

## Wire format [#wire-format]

### Descriptor scan (fn 0) [#descriptor-scan-fn-0]

Request: `[hi, lo, 0x00]` — the big-endian index of the first descriptor field
on this page. The response carries up to eight two-byte fields:

| Byte | Meaning                                                                                            |
| ---- | -------------------------------------------------------------------------------------------------- |
| high | `0x01` marks the end of the table. Bit `7` set marks a gesture entry; bit `1` marks it divertable. |
| low  | The gesture id.                                                                                    |

The walk advances eight fields at a time, counting divertable gestures as it
goes: that running count **is** the diversion index of the next divertable
gesture. It stops at the end marker, at gesture 46, or after 1024 fields; the
bound stops a malformed table from causing an unbounded probe loop.

### Diversion state (fn 3) and write (fn 4) [#diversion-state-fn-3-and-write-fn-4]

The diversion bit for index `i` lives at byte `i >> 3`, mask `1 << (i & 7)`:

| Function  | Payload                       | Notes                                                                   |
| --------- | ----------------------------- | ----------------------------------------------------------------------- |
| 3 (read)  | `[offset, 0x01, mask]`        | Response byte 0 AND mask → currently diverted.                          |
| 4 (write) | `[offset, 0x01, mask, value]` | `value` is `mask` to divert, `0` to restore. Needs a long HID++ report. |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::gestures2::Gestures2Feature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;

if let Some(feat) = device.get_feature::<Gestures2Feature>() {
    if feat.has_thumbwheel().await? {
        // Divert the wheel to the host; false when it isn't divertable.
        let diverted = feat.set_thumbwheel_diverted(true).await?;
        println!("thumb wheel diverted: {diverted}");
    }
}
```

**See also:** [0x2150 thumbwheel](/hidpp/features/x2150-thumbwheel),
[0x6500 gestures1](/hidpp/features/x6500-gestures1).


# 0x8010 · gamingGKeys (/hidpp/features/x8010-gaming-g-keys)



The `gamingGKeys` feature exposes control over the dedicated G-key row found on
Logitech gaming keyboards. G-keys are programmable macro or shortcut keys
separate from the standard key matrix; this feature lets a host query how many
G-keys the device has, read their current assignments, and map them to actions
or macro payloads stored in device memory. It exists only on gaming keyboards
with a physical G-key column; mice, headsets, touchpads, and receivers never
report it.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Capability discovery** — the number of G-keys and any mode or profile
  constraints.
* **Assignments** — read each G-key's current binding and write new ones: a
  macro reference, a keystroke, or a media action.
* **Profiles** — assignments are typically profile-scoped; the feature may
  select which profile's bindings are active, alongside 0x8020 gamingMKeys for
  M-key mode selection.
* **Events** — G-key presses and releases can arrive as HID++ events, so the
  host can act on them in software instead of leaving everything to onboard
  macro playback.
* **Onboard vs. host mode** — some devices toggle between fully onboard macro
  execution and a host-driven mode where software intercepts each G-key event.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8010` · &#x2A;*See also:** 0x8020 gamingMKeys, 0x8030 macroRecord


# 0x8020 · gamingMKeys (/hidpp/features/x8020-gaming-m-keys)



The `gamingMKeys` feature exposes control over the M-key (mode) buttons found
on Logitech gaming keyboards. M-keys switch the active macro profile bank,
typically labelled M1, M2, and M3, so the G-keys and other programmable
bindings carry different assignments per bank with no host software involved
in the switch. Only gaming keyboards with physical M-keys report it; it works
closely with 0x8010 gamingGKeys, which governs the per-bank G-key assignments.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Bank discovery** — how many banks the device supports and which is active.
* **Bank selection** — set the active bank programmatically, the same effect
  as pressing a physical M-key.
* **Indicator control** — a row of LEDs typically mirrors the M-key state; the
  feature may read or override which is lit, independent of the hardware
  default.
* **Events** — an M-key press can arrive as a HID++ event, so the host learns
  of the bank change without polling and can refresh what it displays.
* **G-key coordination** — a bank change decides which G-key assignments
  (managed by 0x8010 gamingGKeys) are active; the two features are meant to be
  used together.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8020` · &#x2A;*See also:** 0x8010 gamingGKeys


# 0x8030 · macroRecord (/hidpp/features/x8030-macro-record)



`macroRecord` is the HID++ interface for on-the-fly macro recording on
Logitech gaming keyboards. The host, or the device itself, opens a recording
session; subsequent keystrokes are captured and stored as a macro, typically
bound to a G-key managed by `0x8010` gamingGKeys. It applies only to gaming
keyboards with hardware macro recording, not to mice, headsets, touchpads, or
receivers.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x8010 gamingGKeys where applicable.
</Callout>

## What it does [#what-it-does]

* **Session control** — start and stop a recording session; while it runs, the
  device buffers key events instead of acting on them.
* **Target key** — the host names which programmable key (a G-key slot)
  receives the macro when the session ends.
* **State events** — the device reports recording-state changes (idle,
  recording, full, error) over HID++ so software can track them.
* **Storage** — recorded sequences land in onboard memory and play back later
  with no host involved, under the `0x8010` gamingGKeys assignment model.
* **Timing** — capable devices preserve inter-keystroke delays, so playback
  reproduces the original timing.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8030` · &#x2A;*See also:** 0x8010 gamingGKeys


# 0x8040 · brightnessControl (/hidpp/features/x8040-brightness-control)



Controls the display or backlight brightness of a device. `getInfo` (function 0)
returns a `BrightnessInfo` describing the min/max brightness, the number of
discrete steps, and a `BrightnessCapabilities` bitmask. `getBrightness` /
`setBrightness` (functions 1–2) read and write the current brightness level as a
`u16`. When the `ILLUMINATION` capability is set, `getIllumination` /
`setIllumination` (functions 3–4) independently query and toggle the backlight
on or off without changing the stored brightness value.

* **`HARDWARE_BRIGHTNESS`** — hardware can change brightness without a host command.
* **`EVENTS`** — the device emits brightness or illumination change events.
* **`ILLUMINATION`** — illumination can be queried and controlled separately from brightness.
* **`HARDWARE_ON_OFF`** — hardware can toggle illumination on and off directly.
* **`TRANSIENT`** — brightness is not persisted by the device across power cycles.

> **Spec:** Logitech HID++ 2.0 — *brightnessControl*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `BrightnessControlFeature` wrapper (`0x8040`) exposes:

### Methods [#methods]

| Function           | HID++ fn | Signature           | Returns          |
| ------------------ | -------- | ------------------- | ---------------- |
| `get_info`         | 0        | `()`                | `BrightnessInfo` |
| `get_brightness`   | 1        | `()`                | `u16`            |
| `set_brightness`   | 2        | `(brightness: u16)` | `()`             |
| `get_illumination` | 3        | `()`                | `bool`           |
| `set_illumination` | 4        | `(enabled: bool)`   | `()`             |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `BrightnessInfo` [#brightnessinfo]

Brightness range and capability information.

| Field            | Type                     | Description                                          |
| ---------------- | ------------------------ | ---------------------------------------------------- |
| `min_brightness` | `u16`                    | Minimum accepted brightness.                         |
| `max_brightness` | `u16`                    | Maximum accepted brightness.                         |
| `steps`          | `u16`                    | Number of brightness steps advertised by the device. |
| `capabilities`   | `BrightnessCapabilities` | Feature capabilities.                                |

#### `BrightnessCapabilities` [#brightnesscapabilities]

Capabilities reported by `BrightnessControl`.

| Flag                  | Bit/Value | Description                                                            |
| --------------------- | --------- | ---------------------------------------------------------------------- |
| `HARDWARE_BRIGHTNESS` | `1 << 0`  | Hardware can change brightness directly.                               |
| `EVENTS`              | `1 << 1`  | The device emits brightness or illumination change events.             |
| `ILLUMINATION`        | `1 << 2`  | Illumination can be queried and controlled separately from brightness. |
| `HARDWARE_ON_OFF`     | `1 << 3`  | Hardware can toggle illumination on and off directly.                  |
| `TRANSIENT`           | `1 << 4`  | Brightness is transient and not persisted by the device.               |

## Wire format [#wire-format]

Short requests carry a 3-byte payload; `getInfo` / `getBrightness` / `getIllumination` responses are read from the 16-byte extended payload returned by `extend_payload()`.

### `get_info` (fn 0) [#get_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (extended 16-byte payload — byte → field):

| Byte(s) | Field               | Notes                                                    |
| ------- | ------------------- | -------------------------------------------------------- |
| 0–1     | `max_brightness`    | Big-endian `u16`.                                        |
| 2       | `steps` (low byte)  | Combined with byte 6 — see note below.                   |
| 3       | `capabilities`      | `BrightnessCapabilities` bitmask (see flag table above). |
| 4–5     | `min_brightness`    | Big-endian `u16`.                                        |
| 6       | `steps` (high byte) | `steps = u16::from_be_bytes([payload[6], payload[2]])`.  |

> The `steps` field is split across two non-adjacent bytes: high byte at index 6, low byte at index 2.

### `get_brightness` (fn 1) [#get_brightness-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (extended 16-byte payload — byte → field):

| Byte(s) | Field      | Notes                                      |
| ------- | ---------- | ------------------------------------------ |
| 0–1     | brightness | Big-endian `u16` current brightness value. |

### `set_brightness` (fn 2) [#set_brightness-fn-2]

Request: `[hi, lo, 0x00]` where `hi` and `lo` are the big-endian bytes of the `u16` brightness value.

| Byte | Content                   |
| ---- | ------------------------- |
| 0    | `(brightness >> 8) as u8` |
| 1    | `brightness as u8`        |
| 2    | `0x00` (padding)          |

No response payload is used.

### `get_illumination` (fn 3) [#get_illumination-fn-3]

Request: `[0x00, 0x00, 0x00]`

Response (extended 16-byte payload — byte → field):

| Byte | Field                | Notes                                                   |
| ---- | -------------------- | ------------------------------------------------------- |
| 0    | illumination enabled | Bit 0: `payload[0] & 1 != 0`. `true` = illumination on. |

### `set_illumination` (fn 4) [#set_illumination-fn-4]

Request: `[enabled, 0x00, 0x00]`

| Byte | Content                                                     |
| ---- | ----------------------------------------------------------- |
| 0    | `0x01` if illumination should be enabled, `0x00` to disable |
| 1–2  | `0x00` (padding)                                            |

No response payload is used.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::brightness_control::BrightnessControlFeature};

// mut device: Device, already created via Device::new(channel, index)?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<BrightnessControlFeature>() {
    let info = feat.get_info().await?;
    println!("Brightness range: {}–{} ({} steps)", info.min_brightness, info.max_brightness, info.steps);

    let current = feat.get_brightness().await?;
    println!("Current brightness: {current}");

    // Set brightness to 75 % of the reported maximum
    let target = (info.max_brightness as u32 * 75 / 100) as u16;
    feat.set_brightness(target).await?;

    if info.capabilities.contains(hidpp::feature::brightness_control::BrightnessCapabilities::ILLUMINATION) {
        let on = feat.get_illumination().await?;
        println!("Illumination currently: {on}");
        feat.set_illumination(true).await?;
    }
}
```


# 0x8060 · adjustableReportRate (/hidpp/features/x8060-report-rate)



The legacy **adjustableReportRate** feature controls how often the device sends
HID reports to the host, expressed in milliseconds. `get_report_rate_list`
(function 0) returns a `ReportRateList` bitfield that encodes every interval the
device supports; `get_report_rate` (function 1) reads the current active
interval; `set_report_rate` (function 2) writes a new one; the device rejects
unsupported values with `InvalidArgument`.

* **`ReportRateList`** — one bit per supported interval: `MS_1` through `MS_8`
  (1 ms–8 ms). A set bit means that interval is available on this device.

> **Spec:** Logitech HID++ 2.0 — *adjustableReportRate*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `ReportRateFeature` wrapper (`0x8060`) exposes:

### Methods [#methods]

| Function               | HID++ fn | Signature              | Returns          |
| ---------------------- | -------- | ---------------------- | ---------------- |
| `get_report_rate_list` | 0        | `()`                   | `ReportRateList` |
| `get_report_rate`      | 1        | `()`                   | `u8`             |
| `set_report_rate`      | 2        | `(report_rate_ms: u8)` | `()`             |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `ReportRateList` [#reportratelist]

Report-rate values supported by a `0x8060` device, encoded as milliseconds.

| Flag   | Bit/Value | Description           |
| ------ | --------- | --------------------- |
| `MS_1` | `1 << 0`  | 1 ms report interval. |
| `MS_2` | `1 << 1`  | 2 ms report interval. |
| `MS_3` | `1 << 2`  | 3 ms report interval. |
| `MS_4` | `1 << 3`  | 4 ms report interval. |
| `MS_5` | `1 << 4`  | 5 ms report interval. |
| `MS_6` | `1 << 5`  | 6 ms report interval. |
| `MS_7` | `1 << 6`  | 7 ms report interval. |
| `MS_8` | `1 << 7`  | 8 ms report interval. |

## Wire format [#wire-format]

All three functions use a 3-byte request payload and return a 16-byte long response; only the first byte of the response payload carries meaningful data for the two getters.

### `get_report_rate_list` (fn 0) [#get_report_rate_list-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                     | Notes                                                                                      |
| ---- | ------------------------- | ------------------------------------------------------------------------------------------ |
| 0    | `ReportRateList` bitfield | Each bit corresponds to a supported interval: bit 0 = 1 ms, bit 1 = 2 ms, …, bit 7 = 8 ms. |

### `get_report_rate` (fn 1) [#get_report_rate-fn-1]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                | Notes                                             |
| ---- | -------------------- | ------------------------------------------------- |
| 0    | `report_rate_ms: u8` | Currently active report interval in milliseconds. |

### `set_report_rate` (fn 2) [#set_report_rate-fn-2]

Request: `[report_rate_ms, 0x00, 0x00]`

| Byte | Field                | Notes                                                                                                                                          |
| ---- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | `report_rate_ms: u8` | Desired report interval in milliseconds. Must be a value advertised by `get_report_rate_list`; the device returns `InvalidArgument` otherwise. |

Response: no meaningful payload bytes.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::report_rate::ReportRateFeature};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ReportRateFeature>() {
    // Query which intervals this device supports.
    let supported = feat.get_report_rate_list().await?;
    println!("Supported intervals: {:?}", supported);

    // Read the current active interval.
    let current_ms = feat.get_report_rate().await?;
    println!("Current report rate: {} ms", current_ms);

    // Switch to 1 ms (1000 Hz) if the device advertises it.
    use hidpp::feature::report_rate::ReportRateList;
    if supported.contains(ReportRateList::MS_1) {
        feat.set_report_rate(1).await?;
    }
}
```


# 0x8061 · extendedAdjustableReportRate (/hidpp/features/x8061-extended-report-rate)



High-frequency polling control. The feature extends the basic `0x8060` by supporting rates from
125 Hz up to **8000 Hz** and by letting the caller query capabilities per connection type — wired
USB vs. Logitech gaming wireless — independently. `get_device_capabilities` returns the
`ExtendedReportRateList` bitmask for a given `ConnectionType`; `get_actual_report_rate_list`
narrows that to the rates available on the device's **current** connection. `get_report_rate` reads
the active `ExtendedReportRate` for a connection type; `set_report_rate` applies a new rate to the
current host-side connection.

* **`ConnectionType`** — `Wired` (USB) or `GamingWireless`.
* **`ExtendedReportRateList`** — bitflags for each supported step: `HZ_125` (8 ms) · `HZ_250`
  (4 ms) · `HZ_500` (2 ms) · `HZ_1000` (1 ms) · `HZ_2000` (500 µs) · `HZ_4000` (250 µs) ·
  `HZ_8000` (125 µs).
* **`ExtendedReportRate`** — a concrete rate enum covering the same seven steps, used as the
  argument to `set_report_rate` and the return value of `get_report_rate`.

> **Spec:** Logitech HID++ 2.0 — *extendedAdjustableReportRate*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `ExtendedReportRateFeature` wrapper (`0x8061`) exposes:

### Methods [#methods]

| Function                      | HID++ fn | Signature                           | Returns                  |
| ----------------------------- | -------- | ----------------------------------- | ------------------------ |
| `get_device_capabilities`     | 0        | `(connection_type: ConnectionType)` | `ExtendedReportRateList` |
| `get_actual_report_rate_list` | 1        | `()`                                | `ExtendedReportRateList` |
| `get_report_rate`             | 2        | `(connection_type: ConnectionType)` | `ExtendedReportRate`     |
| `set_report_rate`             | 3        | `(report_rate: ExtendedReportRate)` | `()`                     |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `ExtendedReportRateList` [#extendedreportratelist]

Report-rate values supported by a `0x8061` device, as a bitflags bitmask returned by `get_device_capabilities` and `get_actual_report_rate_list`.

| Flag      | Bit/Value | Description                                      |
| --------- | --------- | ------------------------------------------------ |
| `HZ_125`  | `1 << 0`  | 125 Hz, equivalent to an 8 ms report interval.   |
| `HZ_250`  | `1 << 1`  | 250 Hz, equivalent to a 4 ms report interval.    |
| `HZ_500`  | `1 << 2`  | 500 Hz, equivalent to a 2 ms report interval.    |
| `HZ_1000` | `1 << 3`  | 1000 Hz, equivalent to a 1 ms report interval.   |
| `HZ_2000` | `1 << 4`  | 2000 Hz, equivalent to a 500 µs report interval. |
| `HZ_4000` | `1 << 5`  | 4000 Hz, equivalent to a 250 µs report interval. |
| `HZ_8000` | `1 << 6`  | 8000 Hz, equivalent to a 125 µs report interval. |

#### `ConnectionType` [#connectiontype]

A connection type used when querying capabilities or the active report rate.

| Variant          | Value | Description                          |
| ---------------- | ----- | ------------------------------------ |
| `Wired`          | `0`   | Wired USB connection.                |
| `GamingWireless` | `1`   | Logitech gaming wireless connection. |

#### `ExtendedReportRate` [#extendedreportrate]

A concrete report-rate setting used as the argument to `set_report_rate` and the return value of `get_report_rate`.

| Variant  | Value | Description                                      |
| -------- | ----- | ------------------------------------------------ |
| `Hz125`  | `0`   | 125 Hz, equivalent to an 8 ms report interval.   |
| `Hz250`  | `1`   | 250 Hz, equivalent to a 4 ms report interval.    |
| `Hz500`  | `2`   | 500 Hz, equivalent to a 2 ms report interval.    |
| `Hz1000` | `3`   | 1000 Hz, equivalent to a 1 ms report interval.   |
| `Hz2000` | `4`   | 2000 Hz, equivalent to a 500 µs report interval. |
| `Hz4000` | `5`   | 4000 Hz, equivalent to a 250 µs report interval. |
| `Hz8000` | `6`   | 8000 Hz, equivalent to a 125 µs report interval. |

## Wire format [#wire-format]

All four functions use a 3-byte short request payload. Capability and rate-list responses are read
from the first two bytes of the 16-byte long response payload (big-endian u16 bitmask); the active
rate response is read from byte 0 only.

### `get_device_capabilities` (fn 0) [#get_device_capabilities-fn-0]

Request: `[connection_type as u8, 0x00, 0x00]`

Response (byte → field):

| Byte | Field                              | Notes                                  |
| ---- | ---------------------------------- | -------------------------------------- |
| 0    | `ExtendedReportRateList` high byte | Combined with byte 1 as big-endian u16 |
| 1    | `ExtendedReportRateList` low byte  | Bit n set ↔ rate `1 << n` is supported |

### `get_actual_report_rate_list` (fn 1) [#get_actual_report_rate_list-fn-1]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field                              | Notes                                              |
| ---- | ---------------------------------- | -------------------------------------------------- |
| 0    | `ExtendedReportRateList` high byte | Same big-endian u16 bitmask as fn 0                |
| 1    | `ExtendedReportRateList` low byte  | Reflects rates available on the current connection |

### `get_report_rate` (fn 2) [#get_report_rate-fn-2]

Request: `[connection_type as u8, 0x00, 0x00]`

Response (byte → field):

| Byte | Field                             | Notes                                                                      |
| ---- | --------------------------------- | -------------------------------------------------------------------------- |
| 0    | `ExtendedReportRate` discriminant | `0`=Hz125 … `6`=Hz8000; other values → `Hidpp20Error::UnsupportedResponse` |

### `set_report_rate` (fn 3) [#set_report_rate-fn-3]

Request: `[report_rate as u8, 0x00, 0x00]`

Response: acknowledged (no fields read).

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::extended_report_rate::{
        ConnectionType, ExtendedReportRate, ExtendedReportRateFeature,
    },
};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ExtendedReportRateFeature>() {
    // Query which rates the wired connection supports.
    let caps = feat.get_device_capabilities(ConnectionType::Wired).await?;
    println!("Wired capabilities: {:?}", caps);

    // Read the rate currently active on whichever connection is live.
    let current = feat.get_report_rate(ConnectionType::Wired).await?;
    println!("Current rate: {:?}", current);

    // Switch to 1000 Hz on the current host-side connection.
    feat.set_report_rate(ExtendedReportRate::Hz1000).await?;
}
```


# 0x8070 · colorLedEffects (/hidpp/features/x8070-color-led-effects)



The per-zone RGB effect engine used by Logitech keyboards and mice. A device
exposes one or more LED **zones** (primary, logo, left/right side, …); each zone
independently supports a set of **effects** such as fixed color, breathing, color
wave, starlight, or ripple. `get_info` reports the zone count and capability
bitmasks; `get_zone_info` and `get_zone_effect_info` enumerate each zone's
physical location and the effects it supports. An effect is applied with
`set_zone_effect`, which accepts ten effect-specific parameter bytes (e.g. R, G, B
for `FixedColor`) and a `Persistence` value choosing between RAM-only or EEPROM
storage. `get_sw_control` / `set_sw_control` hand the LED engine to software or
back to firmware; when sync events are enabled the device emits a `SyncEffect`
event each period, allowing timing drift to be corrected via `synchronize_effect`.

* **`EffectId`** — `Disabled`, `FixedColor`, `PulsingBreathingLegacy`, `Cycling`,
  `ColorWave`, `Starlight`, `LightOnPress`, `PulsingBreathingWaveform`, `Ripple`,
  and more.
* **`LocationEffect`** — `Primary`, `Logo`, `LeftSide`, `RightSide`, `Combined`,
  or per-numbered primary zones (`Primary1`–`Primary6`).
* **`Persistence`** — `Volatile` (RAM only, lost on power cycle),
  `VolatileAndNonVolatile` (RAM + EEPROM), `NonVolatileOnly` (EEPROM only).
* **`ExtCapabilities`** — feature flags read from `get_info`:
  `GET_ZONE_EFFECT`, `NO_GET_EFFECT_SETTINGS`, `SET_LED_BIN_INFO`,
  `MONOCHROME_ONLY`, `NO_SYNCHRONIZE_EFFECT`.

> **Spec:** Logitech HID++ 2.0 — *colorLedEffects*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `ColorLedEffectsFeature` wrapper (`0x8070`) exposes:

### Methods [#methods]

| Function                | HID++ fn | Signature                                                                             | Returns                                         |
| ----------------------- | -------- | ------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `get_info`              | 0        | `()`                                                                                  | `ColorLedInfo`                                  |
| `get_zone_info`         | 1        | `(zone_index: u8)`                                                                    | `ZoneInfo`                                      |
| `get_zone_effect_info`  | 2        | `(zone_index: u8, zone_effect_index: u8)`                                             | `ZoneEffectInfo`                                |
| `set_zone_effect`       | 3        | `(zone_index: u8, zone_effect_index: u8, params: [u8; 10], persistence: Persistence)` | `()`                                            |
| `get_nv_config`         | 4        | `(capability: NvCapabilities)`                                                        | `NvConfig`                                      |
| `set_nv_config`         | 5        | `(capability: NvCapabilities, state: NvCapabilityState, param1: u8, param2: u8)`      | `()`                                            |
| `get_led_bin_info`      | 6        | `(zone_index: u8, led_bin_index: LedBinIndex)`                                        | `LedBinInfo`                                    |
| `get_sw_control`        | 7        | `()`                                                                                  | `SwControlState`                                |
| `set_sw_control`        | 8        | `(control: SwControl, sync_events: bool)`                                             | `()`                                            |
| `get_effect_settings`   | 9        | `(zone_index: u8, source: PersistenceSource)`                                         | `EffectSettings`                                |
| `clear_effect_settings` | 10       | `(zone_index: u8)`                                                                    | `()`                                            |
| `set_cycling_direction` | 11       | `(direction: CyclingDirection)`                                                       | `()`                                            |
| `get_current_color`     | 12       | `(zone_index: u8)`                                                                    | `Rgb`                                           |
| `synchronize_effect`    | 13       | `(zone_index: u8, drift_value: i16)`                                                  | `()`                                            |
| `get_zone_effect`       | 14       | `(zone_index: u8, source: PersistenceSource)`                                         | `ZoneEffect`                                    |
| `set_led_bin_info`      | 15       | `(info: &LedBinInfo)`                                                                 | `LedBinInfo`                                    |
| `listen`                | —        | `()`                                                                                  | `async_channel::Receiver<ColorLedEffectsEvent>` |

All `async fn` methods return `Result<_, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver for unsolicited events.

### Types [#types]

#### `ColorLedInfo` [#colorledinfo]

General feature information returned by `get_info`.

| Field              | Type              | Description                          |
| ------------------ | ----------------- | ------------------------------------ |
| `zone_count`       | `u8`              | Number of LED zones.                 |
| `nv_capabilities`  | `NvCapabilities`  | Supported non-volatile capabilities. |
| `ext_capabilities` | `ExtCapabilities` | Extended capabilities.               |

#### `ZoneInfo` [#zoneinfo]

Information about one zone, returned by `get_zone_info`.

| Field            | Type                      | Description                           |
| ---------------- | ------------------------- | ------------------------------------- |
| `zone_index`     | `u8`                      | Index of the zone.                    |
| `location`       | `LocationEffect`          | Physical location the zone covers.    |
| `effects_number` | `u8`                      | Number of effects the zone supports.  |
| `persistency`    | `PersistencyCapabilities` | Persistency capabilities of the zone. |

#### `ZoneEffectInfo` [#zoneeffectinfo]

Information about one effect of a zone, returned by `get_zone_effect_info`.

| Field                 | Type       | Description                                                 |
| --------------------- | ---------- | ----------------------------------------------------------- |
| `zone_index`          | `u8`       | Index of the zone.                                          |
| `zone_effect_index`   | `u8`       | Index of the effect within the zone.                        |
| `effect_id`           | `EffectId` | The effect type.                                            |
| `effect_capabilities` | `u16`      | Effect capability bitmask (meaning depends on `effect_id`). |
| `effect_period`       | `u16`      | Effect period in milliseconds, or `0` when not available.   |

#### `ZoneEffect` [#zoneeffect]

The configured effect of a zone, returned by `get_zone_effect`.

| Field               | Type       | Description                                                         |
| ------------------- | ---------- | ------------------------------------------------------------------- |
| `zone_index`        | `u8`       | Index of the zone.                                                  |
| `zone_effect_index` | `u8`       | Index of the configured effect within the zone.                     |
| `params`            | `[u8; 10]` | The effect parameters (meaning depends on the effect's `EffectId`). |

#### `EffectSettings` [#effectsettings]

Effect settings of a zone, returned by `get_effect_settings`.

| Field        | Type  | Description                    |
| ------------ | ----- | ------------------------------ |
| `zone_index` | `u8`  | Index of the zone.             |
| `color`      | `Rgb` | Effect color.                  |
| `period`     | `u16` | Effect period in milliseconds. |
| `brightness` | `u8`  | Effect brightness.             |
| `param`      | `u8`  | Effect-specific parameter.     |

#### `SwControlState` [#swcontrolstate]

Software-control state, returned by `get_sw_control`.

| Field         | Type        | Description                                  |
| ------------- | ----------- | -------------------------------------------- |
| `control`     | `SwControl` | Whether firmware or software owns the LEDs.  |
| `sync_events` | `bool`      | Whether the device emits sync-effect events. |

#### `NvConfig` [#nvconfig]

A non-volatile configuration entry, returned by `get_nv_config`.

| Field        | Type                | Description                                     |
| ------------ | ------------------- | ----------------------------------------------- |
| `capability` | `NvCapabilities`    | The single capability bit this entry addresses. |
| `state`      | `NvCapabilityState` | The capability's state.                         |
| `param1`     | `u8`                | First capability-specific parameter.            |
| `param2`     | `u8`                | Second capability-specific parameter.           |

#### `LedBinInfo` [#ledbininfo]

Manufacturing LED bin information, returned by `get_led_bin_info` / `set_led_bin_info`.

| Field           | Type          | Description                  |
| --------------- | ------------- | ---------------------------- |
| `zone_index`    | `u8`          | Index of the zone.           |
| `led_bin_index` | `LedBinIndex` | Which bin parameter this is. |
| `red`           | `u16`         | Red bin value.               |
| `green`         | `u16`         | Green bin value.             |
| `blue`          | `u16`         | Blue bin value.              |
| `white`         | `u16`         | White bin value.             |

#### `Rgb` [#rgb]

An 8-bit-per-channel RGB color.

| Field   | Type | Description    |
| ------- | ---- | -------------- |
| `red`   | `u8` | Red channel.   |
| `green` | `u8` | Green channel. |
| `blue`  | `u8` | Blue channel.  |

#### `EffectId` [#effectid]

Identifies the type of a zone effect.

| Variant                    | Value | Description                                   |
| -------------------------- | ----- | --------------------------------------------- |
| `Disabled`                 | 0     | No effect / LEDs off.                         |
| `FixedColor`               | 1     | A fixed single color.                         |
| `PulsingBreathingLegacy`   | 2     | Legacy pulsing/breathing effect.              |
| `Cycling`                  | 3     | Color cycling through the color wheel.        |
| `ColorWave`                | 4     | A traveling color wave.                       |
| `Starlight`                | 5     | Twinkling "starlight" effect.                 |
| `LightOnPress`             | 6     | Light up keys on press.                       |
| `AudioVisualizer`          | 7     | Audio visualizer (reserved).                  |
| `BootUp`                   | 8     | Boot-up effect.                               |
| `DemoMode`                 | 9     | Demo mode.                                    |
| `PulsingBreathingWaveform` | 10    | Pulsing/breathing with a selectable waveform. |
| `Ripple`                   | 11    | Ripple effect.                                |

#### `LocationEffect` [#locationeffect]

The physical location a zone covers.

| Variant     | Value | Description       |
| ----------- | ----- | ----------------- |
| `Primary`   | 1     | The primary zone. |
| `Logo`      | 2     | The logo.         |
| `LeftSide`  | 3     | The left side.    |
| `RightSide` | 4     | The right side.   |
| `Combined`  | 5     | A combined zone.  |
| `Primary1`  | 6     | Primary zone 1.   |
| `Primary2`  | 7     | Primary zone 2.   |
| `Primary3`  | 8     | Primary zone 3.   |
| `Primary4`  | 9     | Primary zone 4.   |
| `Primary5`  | 10    | Primary zone 5.   |
| `Primary6`  | 11    | Primary zone 6.   |

#### `Persistence` [#persistence]

Storage persistence for `set_zone_effect`.

| Variant                  | Value | Description                                         |
| ------------------------ | ----- | --------------------------------------------------- |
| `Volatile`               | 0     | Volatile: applied to RAM only, lost on power cycle. |
| `VolatileAndNonVolatile` | 1     | Applied to RAM and stored in EEPROM.                |
| `NonVolatileOnly`        | 2     | Stored in EEPROM only.                              |

#### `PersistenceSource` [#persistencesource]

Which storage a read function should read from.

| Variant  | Value | Description                                |
| -------- | ----- | ------------------------------------------ |
| `Ram`    | 0     | The actively playing configuration in RAM. |
| `Eeprom` | 1     | The saved configuration in EEPROM.         |

#### `SwControl` [#swcontrol]

Whether the firmware or software owns the LEDs.

| Variant    | Value | Description                 |
| ---------- | ----- | --------------------------- |
| `Firmware` | 0     | The firmware owns all LEDs. |
| `Software` | 1     | Software owns all LEDs.     |

#### `CyclingDirection` [#cyclingdirection]

Direction of color cycling.

| Variant         | Value | Description                            |
| --------------- | ----- | -------------------------------------- |
| `Clockwise`     | 0     | Clockwise through the color wheel.     |
| `Anticlockwise` | 1     | Anticlockwise through the color wheel. |

#### `LedBinIndex` [#ledbinindex]

Selects which LED bin parameter a `get_led_bin_info` / `set_led_bin_info` call addresses.

| Variant              | Value | Description            |
| -------------------- | ----- | ---------------------- |
| `BinValueBrightness` | 0     | Bin value: brightness. |
| `BinValueColor`      | 1     | Bin value: color.      |
| `CalibrationFactors` | 2     | Calibration factors.   |
| `Brightness`         | 3     | Brightness.            |
| `ColorimetricX`      | 4     | Colorimetric X.        |
| `ColorimetricY`      | 5     | Colorimetric Y.        |

#### `NvCapabilityState` [#nvcapabilitystate]

State of a non-volatile configuration capability.

| Variant    | Value | Description                                                                           |
| ---------- | ----- | ------------------------------------------------------------------------------------- |
| `NoChange` | 0     | The stored value has never been explicitly set (read-only sentinel, enabled assumed). |
| `Enabled`  | 1     | The capability is enabled.                                                            |
| `Disabled` | 2     | The capability is disabled.                                                           |

#### `NvCapabilities` [#nvcapabilities]

Supported non-volatile configuration capabilities, from `get_info`.

| Flag             | Bit/Value | Description                         |
| ---------------- | --------- | ----------------------------------- |
| `BOOT_UP_EFFECT` | `1 << 0`  | A boot-up effect can be configured. |
| `DEMO`           | `1 << 1`  | Demo mode is supported.             |
| `USER_DEMO_MODE` | `1 << 2`  | User demo mode is supported.        |

#### `ExtCapabilities` [#extcapabilities]

Extended capabilities from `get_info`. Several flags are "NOT supported" flags whose set state removes a function.

| Flag                     | Bit/Value | Description                                                      |
| ------------------------ | --------- | ---------------------------------------------------------------- |
| `GET_ZONE_EFFECT`        | `1 << 0`  | `getZoneEffect` is supported.                                    |
| `NO_GET_EFFECT_SETTINGS` | `1 << 1`  | `getEffectSettings` is not supported.                            |
| `SET_LED_BIN_INFO`       | `1 << 2`  | `setLedBinInfo` is supported.                                    |
| `MONOCHROME_ONLY`        | `1 << 3`  | Only monochrome effects are supported.                           |
| `NO_SYNCHRONIZE_EFFECT`  | `1 << 4`  | `synchronizeEffect` and the sync-effect event are not supported. |

#### `PersistencyCapabilities` [#persistencycapabilities]

Persistency capabilities of a zone, from `get_zone_info`. A value of zero means persistency is not supported.

| Flag          | Bit/Value | Description                                  |
| ------------- | --------- | -------------------------------------------- |
| `ALWAYS_ON`   | `1 << 0`  | The zone can persist an "always on" state.   |
| `ALWAYS_OFF`  | `1 << 1`  | The zone can persist an "always off" state.  |
| `ON_THEN_OFF` | `1 << 2`  | The zone can persist an "on then off" state. |

### Events [#events]

`ColorLedEffectsFeature` implements `EmittingFeature<ColorLedEffectsEvent>`. Call `listen()` to receive an `async_channel::Receiver<ColorLedEffectsEvent>`.

#### `ColorLedEffectsEvent` [#colorledeffectsevent]

| Variant      | Fields                                  | Description                                                                                                                                                                                                                        |
| ------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SyncEffect` | `zone_index: u8`, `effect_counter: u16` | A period effect reached a synchronization point. Emitted once per period while sync events are enabled. `zone_index` of `0xff` means all zones; `effect_counter` is the current timing position within the period in milliseconds. |

## Wire format [#wire-format]

Most getters use a 3-byte short request and read a 16-byte long response payload. The four write-heavy functions (`set_zone_effect`, `set_nv_config`, `synchronize_effect`, `set_led_bin_info`) send a 16-byte long request. All multi-byte fields are big-endian.

### `get_info` (fn 0) [#get_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field              | Notes                              |
| ---- | ------------------ | ---------------------------------- |
| 0    | `zone_count`       | Number of LED zones                |
| 1–2  | `nv_capabilities`  | `NvCapabilities` bitmask (BE u16)  |
| 3–4  | `ext_capabilities` | `ExtCapabilities` bitmask (BE u16) |

### `get_zone_info` (fn 1) [#get_zone_info-fn-1]

Request: `[zone_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field            | Notes                                |
| ---- | ---------------- | ------------------------------------ |
| 0    | `zone_index`     | Echo                                 |
| 1–2  | `location`       | `LocationEffect` as BE u16           |
| 3    | `effects_number` | Number of effects this zone supports |
| 4    | `persistency`    | `PersistencyCapabilities` bitmask    |

### `get_zone_effect_info` (fn 2) [#get_zone_effect_info-fn-2]

Request: `[zone_index, zone_effect_index, 0x00]`

Response (byte → field):

| Byte | Field                 | Notes                                   |
| ---- | --------------------- | --------------------------------------- |
| 0    | `zone_index`          | Echo                                    |
| 1    | `zone_effect_index`   | Echo                                    |
| 2–3  | `effect_id`           | `EffectId` as BE u16                    |
| 4–5  | `effect_capabilities` | Effect capability bitmask (BE u16)      |
| 6–7  | `effect_period`       | Period in ms, 0 if unavailable (BE u16) |

### `set_zone_effect` (fn 3) [#set_zone_effect-fn-3]

16-byte long request:

| Byte  | Field               | Notes                                                    |
| ----- | ------------------- | -------------------------------------------------------- |
| 0     | `zone_index`        |                                                          |
| 1     | `zone_effect_index` |                                                          |
| 2–11  | `params`            | 10 effect-specific bytes (e.g. R, G, B for `FixedColor`) |
| 12    | `persistence`       | `Persistence` as u8                                      |
| 13–15 | —                   | Padding (0x00)                                           |

No response body (acknowledges with an empty reply).

### `get_nv_config` (fn 4) [#get_nv_config-fn-4]

Request: `[cap_hi, cap_lo, 0x00]` — `cap_hi:cap_lo` is the single-bit `NvCapabilities` value as BE u16.

Response (byte → field):

| Byte | Field        | Notes                                    |
| ---- | ------------ | ---------------------------------------- |
| 0–1  | `capability` | `NvCapabilities` bitmask echoed (BE u16) |
| 2    | `state`      | `NvCapabilityState` as u8                |
| 3    | `param1`     | Capability-specific parameter            |
| 4    | `param2`     | Capability-specific parameter            |

### `set_nv_config` (fn 5) [#set_nv_config-fn-5]

16-byte long request:

| Byte | Field        | Notes                                 |
| ---- | ------------ | ------------------------------------- |
| 0–1  | `capability` | Single-bit `NvCapabilities` as BE u16 |
| 2    | `state`      | `NvCapabilityState` as u8             |
| 3    | `param1`     |                                       |
| 4    | `param2`     |                                       |
| 5–15 | —            | Padding (0x00)                        |

No response body.

### `get_led_bin_info` (fn 6) [#get_led_bin_info-fn-6]

Request: `[zone_index, led_bin_index, 0x00]` — `led_bin_index` is `LedBinIndex` as u8.

Response (byte → field):

| Byte | Field           | Notes               |
| ---- | --------------- | ------------------- |
| 0    | `zone_index`    | Echo                |
| 1    | `led_bin_index` | `LedBinIndex` as u8 |
| 2–3  | `red`           | BE u16              |
| 4–5  | `green`         | BE u16              |
| 6–7  | `blue`          | BE u16              |
| 8–9  | `white`         | BE u16              |

### `get_sw_control` (fn 7) [#get_sw_control-fn-7]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field         | Notes                                      |
| ---- | ------------- | ------------------------------------------ |
| 0    | `control`     | `SwControl` as u8 (0=Firmware, 1=Software) |
| 1    | `sync_events` | 0x00=false, any other=true                 |

### `set_sw_control` (fn 8) [#set_sw_control-fn-8]

Request: `[control, sync_events_u8, 0x00]` — `control` is `SwControl` as u8; `sync_events_u8` is 0 or 1.

No response body.

### `get_effect_settings` (fn 9) [#get_effect_settings-fn-9]

Request: `[zone_index, source, 0x00]` — `source` is `PersistenceSource` as u8.

Response (byte → field):

| Byte | Field         | Notes                        |
| ---- | ------------- | ---------------------------- |
| 0    | `zone_index`  | Echo                         |
| 1    | `color.red`   |                              |
| 2    | `color.green` |                              |
| 3    | `color.blue`  |                              |
| 4–5  | `period`      | Effect period in ms (BE u16) |
| 6    | `brightness`  |                              |
| 7    | `param`       | Effect-specific parameter    |

### `clear_effect_settings` (fn 10) [#clear_effect_settings-fn-10]

Request: `[zone_index, 0x00, 0x00]`

No response body.

### `set_cycling_direction` (fn 11) [#set_cycling_direction-fn-11]

Request: `[direction, 0x00, 0x00]` — `direction` is `CyclingDirection` as u8 (0=Clockwise, 1=Anticlockwise).

No response body.

### `get_current_color` (fn 12) [#get_current_color-fn-12]

Request: `[zone_index, 0x00, 0x00]`

Response (byte → field):

| Byte | Field   | Notes              |
| ---- | ------- | ------------------ |
| 0    | —       | Not used for color |
| 1    | `red`   |                    |
| 2    | `green` |                    |
| 3    | `blue`  |                    |

### `synchronize_effect` (fn 13) [#synchronize_effect-fn-13]

16-byte long request:

| Byte | Field                   | Notes                    |
| ---- | ----------------------- | ------------------------ |
| 0    | `zone_index`            | 0xff = all zones         |
| 1    | —                       | Padding (0x00)           |
| 2    | `drift_value` high byte | `drift_value: i16` as BE |
| 3    | `drift_value` low byte  |                          |
| 4–15 | —                       | Padding (0x00)           |

No response body. Valid only while sync events are enabled.

### `get_zone_effect` (fn 14) [#get_zone_effect-fn-14]

Request: `[zone_index, source, 0x00]` — `source` is `PersistenceSource` as u8.

Response (byte → field):

| Byte | Field               | Notes                    |
| ---- | ------------------- | ------------------------ |
| 0    | `zone_index`        | Echo                     |
| 1    | `zone_effect_index` |                          |
| 2–11 | `params`            | 10 effect-specific bytes |

### `set_led_bin_info` (fn 15) [#set_led_bin_info-fn-15]

16-byte long request:

| Byte  | Field           | Notes               |
| ----- | --------------- | ------------------- |
| 0     | `zone_index`    |                     |
| 1     | `led_bin_index` | `LedBinIndex` as u8 |
| 2–3   | `red`           | BE u16              |
| 4–5   | `green`         | BE u16              |
| 6–7   | `blue`          | BE u16              |
| 8–9   | `white`         | BE u16              |
| 10–15 | —               | Padding (0x00)      |

Response has the same byte layout as `get_led_bin_info` (device echoes the written values).

### `SyncEffect` event (event fn 0) [#synceffect-event-event-fn-0]

Unsolicited message emitted once per effect period while sync events are enabled.

| Byte | Field            | Notes                                             |
| ---- | ---------------- | ------------------------------------------------- |
| 0    | `zone_index`     | 0xff = all zones                                  |
| 1–2  | `effect_counter` | Timing position within the period, in ms (BE u16) |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::color_led_effects::{
        ColorLedEffectsFeature, Persistence, SwControl,
    },
};

// mut device: Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ColorLedEffectsFeature>() {
    // Read overall feature info (zone count, capability flags).
    let info = feat.get_info().await?;
    println!("zones: {}, ext_caps: {:?}", info.zone_count, info.ext_capabilities);

    // Take software control so we can drive the LEDs directly.
    feat.set_sw_control(SwControl::Software, true).await?;

    // Apply a fixed red color to zone 0, stored in RAM only.
    let mut params = [0u8; 10];
    params[0] = 0xff; // R
    params[1] = 0x00; // G
    params[2] = 0x00; // B
    feat.set_zone_effect(0, 0, params, Persistence::Volatile).await?;

    // Listen for sync-effect events to correct timing drift.
    let rx = feat.listen();
    if let Ok(event) = rx.recv().await {
        println!("sync event: {:?}", event);
    }

    // Return LED control to the firmware.
    feat.set_sw_control(SwControl::Firmware, false).await?;
}
```


# 0x8071 · rgbEffects (/hidpp/features/x8071-rgb-effects)



The modern per-cluster RGB effect engine, the successor to `0x8070`
colorLedEffects. A device groups its LEDs into *clusters*, each advertising a
set of *effects*. `get_device_info`, `get_cluster_info`, and `get_effect_info`
query the three info modes of the polymorphic `getInfo` function (device,
cluster, and per-effect detail respectively). `set_rgb_cluster_effect` applies a
chosen effect, with up to 10 effect-specific parameter bytes, a
`RgbPersistence` flag (volatile RAM or non-volatile EEPROM), and a
`PowerModeTarget` (full-power or power-save mode).

Software must call `set_sw_control` first to claim ownership of clusters and/or
power modes before any write call will be accepted. `get_nv_config` /
`set_nv_config` read and write named non-volatile capabilities (boot-up effect,
demo, active dimming, etc.). Power-mode timeouts are managed with
`get_power_mode_config` / `set_power_mode_config`; the current mode with
`get_power_mode` / `set_power_mode`. Three unsolicited events are decoded:

* **`EffectSync`** — a periodic effect reached a synchronization point; carries
  the cluster index and current timing position in milliseconds.
* **`UserActivity`** — activity detected or the no-activity timeout was reached
  (`ActivityEventType`).
* **`ClusterChanged`** — mirrors a `setRgbClusterEffect` request; carries the
  full params, persistence, and power-mode target.

Key types: `RgbDeviceInfo`, `RgbClusterInfo`, `RgbEffectInfo`, `RgbSwControl`,
`RgbNvConfig`, `RgbPowerModeConfig`, `RgbPersistence`, `RgbPowerMode`,
`SwControlFlags`, `RgbNvCapabilities`, `RgbExtCapabilities`.

> **Spec:** Logitech HID++ 2.0 — *rgbEffects*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `RgbEffectsFeature` wrapper (`0x8071`) exposes methods for querying and configuring per-cluster RGB effects, managing software control and non-volatile capabilities, and receiving unsolicited lighting events.

### Methods [#methods]

| Function                        | HID++ fn | Signature                                                                                                                   | Returns                                    |
| ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `get_device_info`               | 0        | `()`                                                                                                                        | `RgbDeviceInfo`                            |
| `get_cluster_info`              | 0        | `(cluster_index: u8)`                                                                                                       | `RgbClusterInfo`                           |
| `get_effect_info`               | 0        | `(cluster_index: u8, cluster_effect_index: u8)`                                                                             | `RgbEffectInfo`                            |
| `get_onboard_effect_info`       | 0        | `(cluster_index: u8, cluster_effect_index: u8, slot: u8, slot_info_type: SlotInfoType)`                                     | `[u8; 13]`                                 |
| `set_rgb_cluster_effect`        | 1        | `(cluster_index: u8, cluster_effect_index: u8, params: [u8; 10], persistence: RgbPersistence, power_mode: PowerModeTarget)` | `()`                                       |
| `set_multi_led_cluster_pattern` | 2        | `(cluster_index: u8, pattern: u8)`                                                                                          | `()`                                       |
| `get_nv_config`                 | 3        | `(capability: RgbNvCapabilities)`                                                                                           | `RgbNvConfig`                              |
| `set_nv_config`                 | 3        | `(capability: RgbNvCapabilities, state: u8, param1: u8, param2: u8)`                                                        | `()`                                       |
| `get_led_bin_info`              | 4        | `(cluster_index: u8, led_bin_index: LedBinIndex, backup: bool)`                                                             | `[u8; 8]`                                  |
| `set_led_bin_info`              | 4        | `(cluster_index: u8, led_bin_index: LedBinIndex, params: [u8; 8])`                                                          | `()`                                       |
| `get_sw_control`                | 5        | `()`                                                                                                                        | `RgbSwControl`                             |
| `set_sw_control`                | 5        | `(control: SwControlFlags, events: EventsNotificationFlags)`                                                                | `()`                                       |
| `set_effect_sync_correction`    | 6        | `(cluster_index: u8, drift_value: i16)`                                                                                     | `()`                                       |
| `get_power_mode_config`         | 7        | `()`                                                                                                                        | `RgbPowerModeConfig`                       |
| `set_power_mode_config`         | 7        | `(config: RgbPowerModeConfig)`                                                                                              | `()`                                       |
| `get_power_mode`                | 8        | `()`                                                                                                                        | `RgbPowerMode`                             |
| `set_power_mode`                | 8        | `(mode: RgbPowerMode)`                                                                                                      | `()`                                       |
| `shutdown`                      | 9        | `()`                                                                                                                        | `()`                                       |
| `listen`                        | —        | `()`                                                                                                                        | `async_channel::Receiver<RgbEffectsEvent>` |

All `pub async fn` methods return `Result<…, Hidpp20Error>`. `listen` is synchronous and returns a channel receiver directly.

### Types [#types]

#### `RgbDeviceInfo` [#rgbdeviceinfo]

Device-level information from `get_device_info`.

| Field                       | Type                 | Description                          |
| --------------------------- | -------------------- | ------------------------------------ |
| `cluster_count`             | `u8`                 | Number of RGB clusters.              |
| `nv_capabilities`           | `RgbNvCapabilities`  | Supported non-volatile capabilities. |
| `ext_capabilities`          | `RgbExtCapabilities` | Extended capabilities.               |
| `multicluster_effect_count` | `u8`                 | Number of multi-cluster effects.     |

#### `RgbClusterInfo` [#rgbclusterinfo]

Cluster-level information from `get_cluster_info`.

| Field                 | Type                             | Description                                                    |
| --------------------- | -------------------------------- | -------------------------------------------------------------- |
| `cluster_index`       | `u8`                             | Index of the cluster.                                          |
| `location`            | `u16`                            | Physical location of the cluster (raw `locationEffect` value). |
| `effects_number`      | `u8`                             | Number of effects the cluster supports.                        |
| `display_persistency` | `DisplayPersistencyCapabilities` | Display persistency capabilities.                              |
| `effect_persistency`  | `bool`                           | Whether effect persistency to EEPROM is supported.             |
| `multiled_pattern`    | `bool`                           | Whether multi-LED patterns are supported.                      |

#### `RgbEffectInfo` [#rgbeffectinfo]

Effect-level information from `get_effect_info`.

| Field                  | Type  | Description                                                                                          |
| ---------------------- | ----- | ---------------------------------------------------------------------------------------------------- |
| `cluster_index`        | `u8`  | Index of the cluster.                                                                                |
| `cluster_effect_index` | `u8`  | Index of the effect within the cluster.                                                              |
| `effect_id`            | `u16` | The effect type identifier (raw `effectID`).                                                         |
| `effect_capabilities`  | `u16` | Effect capability bitmask (meaning depends on `effect_id`; `0` means Raptor-compatibility defaults). |
| `effect_period`        | `u16` | Effect period in milliseconds, or `0` when not available.                                            |

#### `RgbSwControl` [#rgbswcontrol]

Software-control state from `get_sw_control`.

| Field     | Type                      | Description               |
| --------- | ------------------------- | ------------------------- |
| `control` | `SwControlFlags`          | Software-control flags.   |
| `events`  | `EventsNotificationFlags` | Event-notification flags. |

#### `RgbNvConfig` [#rgbnvconfig]

A non-volatile configuration entry from `get_nv_config`.

| Field        | Type                | Description                                                                     |
| ------------ | ------------------- | ------------------------------------------------------------------------------- |
| `capability` | `RgbNvCapabilities` | The capability this entry addresses.                                            |
| `state`      | `u8`                | The capability state (commonly `0` = no change, `1` = enabled, `2` = disabled). |
| `param1`     | `u8`                | First capability-specific parameter.                                            |
| `param2`     | `u8`                | Second capability-specific parameter.                                           |

#### `RgbPowerModeConfig` [#rgbpowermodeconfig]

Power-mode configuration from `get_power_mode_config`.

| Field                               | Type  | Description                                                 |
| ----------------------------------- | ----- | ----------------------------------------------------------- |
| `flags`                             | `u16` | Power-mode flags (raw).                                     |
| `no_activity_timeout_to_power_save` | `u16` | No-activity timeout before entering power-save, in seconds. |
| `no_activity_timeout_to_off`        | `u16` | No-activity timeout before turning off, in seconds.         |

#### `RgbPowerMode` [#rgbpowermode]

An overall RGB power mode.

| Variant     | Value | Description |
| ----------- | ----- | ----------- |
| `FullRgb`   | `1`   | Full RGB.   |
| `PowerSave` | `2`   | Power-save. |
| `PowerOff`  | `3`   | Power-off.  |

#### `PowerModeTarget` [#powermodetarget]

The power-mode target an effect applies to, packed into `set_rgb_cluster_effect`.

| Variant     | Value | Description      |
| ----------- | ----- | ---------------- |
| `FullPower` | `0`   | Full-power mode. |
| `PowerSave` | `1`   | Power-save mode. |

#### `SlotInfoType` [#slotinfotype]

The kind of slot information requested for an onboard-stored effect.

| Variant            | Value | Description                     |
| ------------------ | ----- | ------------------------------- |
| `SlotState`        | `0`   | Slot state (validity, length).  |
| `Defaults`         | `1`   | Default playback parameters.    |
| `Uuid0To10`        | `2`   | UUID bytes 0..=10.              |
| `Uuid11To16`       | `3`   | UUID bytes 11..=16.             |
| `EffectName0To10`  | `4`   | Effect name characters 0..=10.  |
| `EffectName11To21` | `5`   | Effect name characters 11..=21. |
| `EffectName21To31` | `6`   | Effect name characters 21..=31. |

#### `LedBinIndex` [#ledbinindex]

Selects which LED bin parameter a LED-bin call addresses.

| Variant              | Value | Description            |
| -------------------- | ----- | ---------------------- |
| `BinValueBrightness` | `0`   | Bin value: brightness. |
| `BinValueColor`      | `1`   | Bin value: color.      |
| `CalibrationFactors` | `2`   | Calibration factors.   |
| `Brightness`         | `3`   | Brightness.            |
| `ColorimetricX`      | `4`   | Colorimetric X.        |
| `ColorimetricY`      | `5`   | Colorimetric Y.        |

#### `ActivityEventType` [#activityeventtype]

The kind of user-activity event.

| Variant                    | Value | Description                          |
| -------------------------- | ----- | ------------------------------------ |
| `NoActivityTimeoutReached` | `0`   | The no-activity timeout was reached. |
| `UserActivityDetected`     | `1`   | User activity was detected.          |

#### `RgbPersistence` [#rgbpersistence]

Persistence of a cluster effect, packed into the low two bits of the `set_rgb_cluster_effect` flags byte.

| Flag           | Bit/Value | Description                   |
| -------------- | --------- | ----------------------------- |
| `VOLATILE`     | `1 << 0`  | Apply to volatile RAM.        |
| `NON_VOLATILE` | `1 << 1`  | Store in non-volatile EEPROM. |

#### `RgbExtCapabilities` [#rgbextcapabilities]

Extended device capabilities from `get_device_info`.

| Flag                    | Bit/Value | Description                                        |
| ----------------------- | --------- | -------------------------------------------------- |
| `GET_ZONE_EFFECT`       | `1 << 0`  | `getInfo` for stored effects is supported.         |
| `SET_LED_BIN_INFO`      | `1 << 2`  | Setting LED bin info is supported.                 |
| `MONOCHROME_ONLY`       | `1 << 3`  | Only monochrome effects are supported.             |
| `NO_EFFECT_SYNC`        | `1 << 4`  | Effect-sync correction / events are not supported. |
| `SHUTDOWN`              | `1 << 5`  | The shutdown function is supported.                |
| `CLUSTER_CHANGED_EVENT` | `1 << 6`  | The cluster-changed event is supported.            |

#### `RgbNvCapabilities` [#rgbnvcapabilities]

Supported non-volatile capabilities from `get_device_info`.

| Flag               | Bit/Value | Description       |
| ------------------ | --------- | ----------------- |
| `BOOT_UP_EFFECT`   | `1 << 0`  | Boot-up effect.   |
| `DEMO`             | `1 << 1`  | Demo mode.        |
| `USER_DEMO_MODE`   | `1 << 2`  | User demo mode.   |
| `EVENTS_DISPLAY`   | `1 << 3`  | Events display.   |
| `ACTIVE_DIMMING`   | `1 << 4`  | Active dimming.   |
| `RAMP_DOWN_TO_OFF` | `1 << 5`  | Ramp down to off. |
| `SHUTDOWN_EFFECT`  | `1 << 6`  | Shutdown effect.  |

#### `SwControlFlags` [#swcontrolflags]

Software-control flags for `set_sw_control`.

| Flag           | Bit/Value | Description                                                                    |
| -------------- | --------- | ------------------------------------------------------------------------------ |
| `ALL_CLUSTERS` | `1 << 0`  | Software controls all RGB clusters (required before `set_rgb_cluster_effect`). |
| `POWER_MODES`  | `1 << 1`  | Software controls power modes (required before `set_power_mode`).              |

#### `EventsNotificationFlags` [#eventsnotificationflags]

Event-notification flags for `set_sw_control`.

| Flag                       | Bit/Value | Description                           |
| -------------------------- | --------- | ------------------------------------- |
| `EFFECTS_SYNC`             | `1 << 0`  | Emit effect-sync events.              |
| `USER_ACTIVITY`            | `1 << 1`  | Emit user-activity events.            |
| `NO_USER_ACTIVITY_TIMEOUT` | `1 << 2`  | Emit no-user-activity-timeout events. |

#### `DisplayPersistencyCapabilities` [#displaypersistencycapabilities]

Display-persistency capabilities of a cluster from `get_cluster_info`.

| Flag          | Bit/Value | Description                         |
| ------------- | --------- | ----------------------------------- |
| `ALWAYS_ON`   | `1 << 0`  | Can persist an "always on" state.   |
| `ALWAYS_OFF`  | `1 << 1`  | Can persist an "always off" state.  |
| `ON_THEN_OFF` | `1 << 2`  | Can persist an "on then off" state. |

### Events [#events]

`RgbEffectsFeature` implements `EmittingFeature<RgbEffectsEvent>`; call `listen()` to receive an `async_channel::Receiver<RgbEffectsEvent>` channel.

#### `RgbEffectsEvent` [#rgbeffectsevent]

| Variant          | Fields                                                                                                                            | Description                                                                                                                                            |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `EffectSync`     | `cluster_index: u8`, `effect_counter: u16`                                                                                        | A periodic effect reached a synchronization point. `cluster_index` `0xff` means all clusters; `effect_counter` is the timing position in milliseconds. |
| `UserActivity`   | `ActivityEventType`                                                                                                               | User activity started or the no-activity timeout was reached.                                                                                          |
| `ClusterChanged` | `cluster_index: u8`, `cluster_effect_index: u8`, `params: [u8; 10]`, `persistence: RgbPersistence`, `power_mode: PowerModeTarget` | A cluster's effect changed; mirrors a `set_rgb_cluster_effect` request.                                                                                |

## Wire format [#wire-format]

Short (3-byte) requests use `call`; long (16-byte) requests use `call_long`. All multi-byte fields are big-endian. Responses are read from the 16-byte extended payload.

### `get_device_info` (fn 0) [#get_device_info-fn-0]

Request: `[0xff, 0xff, 0x00]` (`ALL_CLUSTERS`, `ALL_EFFECTS`, `typeOfInfo=0`)

Response (byte → field):

| Byte | Field                       | Notes                                            |
| ---- | --------------------------- | ------------------------------------------------ |
| 2    | `cluster_count`             | Number of clusters.                              |
| 3–4  | `nv_capabilities`           | `RgbNvCapabilities` bitfield, big-endian `u16`.  |
| 5–6  | `ext_capabilities`          | `RgbExtCapabilities` bitfield, big-endian `u16`. |
| 7    | `multicluster_effect_count` | Number of multi-cluster effects.                 |

### `get_cluster_info` (fn 0) [#get_cluster_info-fn-0]

Request: `[cluster_index, 0xff, 0x00]` (`ALL_EFFECTS`, `typeOfInfo=0`)

Response (byte → field):

| Byte | Field                 | Notes                                      |
| ---- | --------------------- | ------------------------------------------ |
| 0    | `cluster_index`       | Echo of requested cluster.                 |
| 2–3  | `location`            | Big-endian `u16` location identifier.      |
| 4    | `effects_number`      | Number of supported effects.               |
| 5    | `display_persistency` | `DisplayPersistencyCapabilities` bitfield. |
| 6    | `effect_persistency`  | Non-zero = EEPROM persistency supported.   |
| 7    | `multiled_pattern`    | Non-zero = multi-LED patterns supported.   |

### `get_effect_info` (fn 0) [#get_effect_info-fn-0]

Request: `[cluster_index, cluster_effect_index, 0x00]` (`typeOfInfo=0`)

Response (byte → field):

| Byte | Field                  | Notes                                               |
| ---- | ---------------------- | --------------------------------------------------- |
| 0    | `cluster_index`        | Echo.                                               |
| 1    | `cluster_effect_index` | Echo.                                               |
| 2–3  | `effect_id`            | Big-endian `u16` effect type identifier.            |
| 4–5  | `effect_capabilities`  | Big-endian `u16` capability bitmask.                |
| 6–7  | `effect_period`        | Big-endian `u16` period in ms; `0` = not available. |

### `get_onboard_effect_info` (fn 0) [#get_onboard_effect_info-fn-0]

Request (long, 16 bytes): `[cluster_index, cluster_effect_index, 0x01, slot, slot_info_type, 0…]`  (`typeOfInfo=0x01`)

Response: bytes 3–15 → raw `[u8; 13]` onboard params (meaning depends on `SlotInfoType`).

### `set_rgb_cluster_effect` (fn 1) [#set_rgb_cluster_effect-fn-1]

Request (long, 16 bytes):

| Byte  | Value                                                                                                        |
| ----- | ------------------------------------------------------------------------------------------------------------ |
| 0     | `cluster_index`                                                                                              |
| 1     | `cluster_effect_index`                                                                                       |
| 2–11  | `params[0..10]` (effect-specific)                                                                            |
| 12    | `persistence.bits() \| (u8::from(power_mode) << 2)` — persistence in bits 0–1, power-mode target in bits 2–3 |
| 13–15 | `0x00`                                                                                                       |

Response: acknowledged (no payload fields used).

### `set_multi_led_cluster_pattern` (fn 2) [#set_multi_led_cluster_pattern-fn-2]

Request: `[cluster_index, pattern, 0x00]`

Response: acknowledged.

### `get_nv_config` (fn 3) [#get_nv_config-fn-3]

Request: `[0x00, cap_hi, cap_lo]` — `getOrSet=0`, capability bits as big-endian `u16`.

Response (byte → field):

| Byte | Field        | Notes                                 |
| ---- | ------------ | ------------------------------------- |
| 1–2  | `capability` | `RgbNvCapabilities` big-endian `u16`. |
| 3    | `state`      | Capability state byte.                |
| 4    | `param1`     | First capability parameter.           |
| 5    | `param2`     | Second capability parameter.          |

### `set_nv_config` (fn 3) [#set_nv_config-fn-3]

Request (long, 16 bytes): `[0x01, cap_hi, cap_lo, state, param1, param2, 0…]` — `getOrSet=1`.

Response: acknowledged.

### `get_led_bin_info` (fn 4) [#get_led_bin_info-fn-4]

Request: `[get_or_set, cluster_index, led_bin_index]` — `get_or_set` is `0x00` (active) or `0x02` (backup).

Response: bytes 3–10 → raw `[u8; 8]` LED bin params.

### `set_led_bin_info` (fn 4) [#set_led_bin_info-fn-4]

Request (long, 16 bytes): `[0x01, cluster_index, led_bin_index, params[0..8], 0…]`

Response: acknowledged.

### `get_sw_control` (fn 5) [#get_sw_control-fn-5]

Request: `[0x00, 0x00, 0x00]` — `getOrSet=0`.

Response (byte → field):

| Byte | Field     | Notes                               |
| ---- | --------- | ----------------------------------- |
| 1    | `control` | `SwControlFlags` bitfield.          |
| 2    | `events`  | `EventsNotificationFlags` bitfield. |

### `set_sw_control` (fn 5) [#set_sw_control-fn-5]

Request: `[0x01, control.bits(), events.bits()]` — `getOrSet=1`.

Response: acknowledged.

### `set_effect_sync_correction` (fn 6) [#set_effect_sync_correction-fn-6]

Request (long, 16 bytes): `[cluster_index, 0x00, drift_hi, drift_lo, 0…]` — `drift_value` as big-endian `i16`; `cluster_index=0xff` targets all clusters.

Response: acknowledged.

### `get_power_mode_config` (fn 7) [#get_power_mode_config-fn-7]

Request: `[0x00, 0x00, 0x00]` — `getOrSet=0`.

Response (byte → field):

| Byte | Field                               | Notes                               |
| ---- | ----------------------------------- | ----------------------------------- |
| 1–2  | `flags`                             | Power-mode flags, big-endian `u16`. |
| 3–4  | `no_activity_timeout_to_power_save` | Seconds, big-endian `u16`.          |
| 5–6  | `no_activity_timeout_to_off`        | Seconds, big-endian `u16`.          |

### `set_power_mode_config` (fn 7) [#set_power_mode_config-fn-7]

Request (long, 16 bytes): `[0x01, flags_hi, flags_lo, psave_hi, psave_lo, off_hi, off_lo, 0…]` — `getOrSet=1`.

Response: acknowledged.

### `get_power_mode` (fn 8) [#get_power_mode-fn-8]

Request: `[0x00, 0x00, 0x00]` — `getOrSet=0`.

Response: byte 1 → `RgbPowerMode` (`1`=`FullRgb`, `2`=`PowerSave`, `3`=`PowerOff`).

### `set_power_mode` (fn 8) [#set_power_mode-fn-8]

Request: `[0x01, mode as u8, 0x00]` — `getOrSet=1`.

Response: acknowledged.

### `shutdown` (fn 9) [#shutdown-fn-9]

Request: `[0x00, 0x00, 0x00]`

Response: acknowledged.

### Events (unsolicited) [#events-unsolicited]

| Sub-ID | Event            | Byte layout                                                                                                                                                    |
| ------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0      | `EffectSync`     | byte 0 = `cluster_index`; bytes 1–2 = `effect_counter` big-endian `u16` (ms)                                                                                   |
| 1      | `UserActivity`   | byte 0 = `ActivityEventType` (`0`=timeout, `1`=detected)                                                                                                       |
| 2      | `ClusterChanged` | byte 0 = `cluster_index`; byte 1 = `cluster_effect_index`; bytes 2–11 = `params[0..10]`; byte 12 = flags (`persistence` in bits 0–1, `power_mode` in bits 2–3) |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{
    device::Device,
    feature::rgb_effects::{
        RgbEffectsFeature, RgbPersistence, PowerModeTarget, SwControlFlags,
        EventsNotificationFlags,
    },
};

// device: Device, obtained via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<RgbEffectsFeature>() {
    // Claim software control of all clusters and enable effect-sync events.
    feat.set_sw_control(
        SwControlFlags::ALL_CLUSTERS,
        EventsNotificationFlags::EFFECTS_SYNC,
    )
    .await?;

    // Query cluster 0 and its first effect.
    let cluster = feat.get_cluster_info(0).await?;
    let effect = feat.get_effect_info(0, 0).await?;
    println!("cluster 0 has {} effects; first effect_id={:#06x}", cluster.effects_number, effect.effect_id);

    // Apply effect 0 to cluster 0 with default params, volatile, full-power mode.
    feat.set_rgb_cluster_effect(
        0,
        0,
        [0u8; 10],
        RgbPersistence::VOLATILE,
        PowerModeTarget::FullPower,
    )
    .await?;

    // Listen for unsolicited events.
    let rx = feat.listen();
    if let Ok(event) = rx.recv().await {
        println!("event: {event:?}");
    }
}
```


# 0x8080 · perKeyLighting (/hidpp/features/x8080-per-key-lighting)



An earlier HID++ 2.0 per-key (per-zone) RGB lighting control feature targeting
RGB keyboards and other Logitech peripherals with individually addressable LED
zones. It defines a staged commit model — zone colours are written in one or
more requests and then committed to the device display in a single frame-end
call — and a mechanism for querying which zone IDs are physically present on
the device. `0x8081` perKeyLighting2 is the revised successor to this feature
and is what OpenLogi's vendored `hidpp` crate implements.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour. OpenLogi instead relies on 0x8081 perKeyLighting2 where applicable.
</Callout>

## What it does [#what-it-does]

It is the protocol `0x8081` was derived from:

* **Zone presence** — ask which zone IDs are populated, so only valid targets
  are addressed.
* **Colour staging** — write RGB values to single zones or groups without
  immediately changing the display.
* **Frame commit** — a dedicated call flushes the staged colours to the hardware
  and controls frame-timing parameters, with an option to persist the result to
  non-volatile storage.
* **Setter variants** — like its successor, it likely has setters trading
  zones-per-request against selection flexibility (individual, consecutive,
  range-based).

Because `0x8081` is the deployed successor, devices that report `0x8080` are
older models; the two features share the same conceptual model but differ in
the precise function table and payload layout. Do not rely on this page for
wire-format details; consult the official Logitech HID++ specification for
`0x8080` directly.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8080` · &#x2A;*See also:** 0x8081 perKeyLighting2


# 0x8081 · perKeyLighting (/hidpp/features/x8081-per-key-lighting)



Per-key (per-zone) RGB lighting control. Zone colours are **staged** with one of
six setter functions and then **committed** to the display via `frame_end`, which
also sets frame-animation timing (`current_frame` / `frames_till_next_change`) and
whether the result is stored in EEPROM. `get_rgb_zone_presence` queries which zone
IDs exist on the device, 112 zones at a time across three pages.

The six setters trade capacity for addressing flexibility: `set_individual_rgb_zones`
handles up to 4 arbitrary zones; `set_consecutive_rgb_zones` sets 5 sequential zones
in full 24-bit colour; the delta variants compress 8 zones (5-bit signed deltas) or
10 zones (4-bit signed deltas) into a single request; `set_range_rgb_zones` fills
up to 3 inclusive ranges with one colour each; `set_rgb_zones_single_value` paints
up to 13 arbitrary zones with one colour. Zone IDs `0x00` and `0xFF` are reserved
end-of-list sentinels.

* **`Rgb`** — 8-bit-per-channel colour (`red`, `green`, `blue`).
* **`RgbZone`** — `zone_id` + `color`; sentinel IDs are rejected at call time.
* **`RgbZoneRange`** — `first_zone_id`, `last_zone_id`, `color` (inclusive).
* **`ZonePresencePage`** — `Zones0To111`, `Zones112To223`, `Zones224To255`.
* **`FramePersistence`** — `Volatile` (RAM only) or `VolatileAndNonVolatile` (RAM + EEPROM).

> **Spec:** Logitech HID++ 2.0 — *perKeyLighting*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `PerKeyLightingFeature` wrapper (`0x8081`) exposes:

### Methods [#methods]

| Function                               | HID++ fn | Signature                                                                           | Returns    |
| -------------------------------------- | -------- | ----------------------------------------------------------------------------------- | ---------- |
| `get_rgb_zone_presence`                | 0        | `(page: ZonePresencePage)`                                                          | `[u8; 14]` |
| `set_individual_rgb_zones`             | 1        | `(zones: &[RgbZone])`                                                               | `()`       |
| `set_consecutive_rgb_zones`            | 2        | `(first_zone_id: u8, colors: [Rgb; 5])`                                             | `()`       |
| `set_consecutive_rgb_zones_delta_5bit` | 3        | `(first_zone_id: u8, packed: [u8; 15])`                                             | `()`       |
| `set_consecutive_rgb_zones_delta_4bit` | 4        | `(first_zone_id: u8, packed: [u8; 15])`                                             | `()`       |
| `set_range_rgb_zones`                  | 5        | `(ranges: &[RgbZoneRange])`                                                         | `()`       |
| `set_rgb_zones_single_value`           | 6        | `(color: Rgb, zone_ids: &[u8])`                                                     | `()`       |
| `frame_end`                            | 7        | `(persistence: FramePersistence, current_frame: u16, frames_till_next_change: u16)` | `()`       |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `Rgb` [#rgb]

An 8-bit-per-channel RGB color.

| Field   | Type | Description    |
| ------- | ---- | -------------- |
| `red`   | `u8` | Red channel.   |
| `green` | `u8` | Green channel. |
| `blue`  | `u8` | Blue channel.  |

#### `RgbZone` [#rgbzone]

A single zone and the color to apply to it.

| Field     | Type  | Description                                                         |
| --------- | ----- | ------------------------------------------------------------------- |
| `zone_id` | `u8`  | Zone identifier (`0` and `255` are reserved end-of-list sentinels). |
| `color`   | `Rgb` | Color to apply.                                                     |

#### `RgbZoneRange` [#rgbzonerange]

A contiguous range of zones to fill with one color.

| Field           | Type  | Description                                     |
| --------------- | ----- | ----------------------------------------------- |
| `first_zone_id` | `u8`  | First zone identifier in the range (inclusive). |
| `last_zone_id`  | `u8`  | Last zone identifier in the range (inclusive).  |
| `color`         | `Rgb` | Color to apply across the range.                |

#### `ZonePresencePage` [#zonepresencepage]

Which page of zone IDs a presence query covers.

| Variant         | Value | Description         |
| --------------- | ----- | ------------------- |
| `Zones0To111`   | `0`   | Zone IDs 0..=111.   |
| `Zones112To223` | `1`   | Zone IDs 112..=223. |
| `Zones224To255` | `2`   | Zone IDs 224..=255. |

#### `FramePersistence` [#framepersistence]

Storage persistence for `frame_end`.

| Variant                  | Value | Description                          |
| ------------------------ | ----- | ------------------------------------ |
| `Volatile`               | `0`   | Volatile: applied to RAM only.       |
| `VolatileAndNonVolatile` | `1`   | Applied to RAM and stored in EEPROM. |

## Wire format [#wire-format]

Getter functions use a **short (3-byte) request payload** and return a **long (16-byte) response payload** read via `extend_payload()`. All setter functions (fn 1–7) use a **long (16-byte) request payload** and return no meaningful response bytes.

### `get_rgb_zone_presence` (fn 0) [#get_rgb_zone_presence-fn-0]

Request: `[ 0x00, page, 0x00 ]` — byte 0 = `typeOfInfo` (always `0x00`), byte 1 = `ZonePresencePage` discriminant, byte 2 = padding.

Response (byte → field):

| Bytes | Field                  | Notes                                                                        |
| ----- | ---------------------- | ---------------------------------------------------------------------------- |
| 2–15  | zone presence bitfield | 14 bytes; bit `i` (LSB-first within each byte) = zone `page_base + i` exists |

### `set_individual_rgb_zones` (fn 1) [#set_individual_rgb_zones-fn-1]

Request: 16-byte long payload, up to 4 slots of 4 bytes each. Unused slots are zeroed.

| Bytes        | Field     | Notes                          |
| ------------ | --------- | ------------------------------ |
| `slot*4 + 0` | `zone_id` | Zone identifier for slot (0–3) |
| `slot*4 + 1` | `red`     | Red channel                    |
| `slot*4 + 2` | `green`   | Green channel                  |
| `slot*4 + 3` | `blue`    | Blue channel                   |

Response: none (ack only).

### `set_consecutive_rgb_zones` (fn 2) [#set_consecutive_rgb_zones-fn-2]

Request: 16-byte long payload for exactly 5 sequential zones.

| Bytes     | Field               | Notes            |
| --------- | ------------------- | ---------------- |
| 0         | `first_zone_id`     | Starting zone ID |
| `1 + i*3` | `red` of zone `i`   | i = 0..4         |
| `2 + i*3` | `green` of zone `i` |                  |
| `3 + i*3` | `blue` of zone `i`  |                  |

Response: none (ack only).

### `set_consecutive_rgb_zones_delta_5bit` (fn 3) [#set_consecutive_rgb_zones_delta_5bit-fn-3]

Request: 16-byte long payload — 8 consecutive zones encoded as 5-bit signed per-channel deltas.

| Bytes | Field           | Notes                                                                           |
| ----- | --------------- | ------------------------------------------------------------------------------- |
| 0     | `first_zone_id` | Starting zone ID                                                                |
| 1–15  | `packed`        | 15-byte verbatim delta payload (8×3 5-bit deltas, MSB-first, zone-then-channel) |

Response: none (ack only).

### `set_consecutive_rgb_zones_delta_4bit` (fn 4) [#set_consecutive_rgb_zones_delta_4bit-fn-4]

Request: 16-byte long payload — 10 consecutive zones encoded as 4-bit signed per-channel deltas.

| Bytes | Field           | Notes                                                                                      |
| ----- | --------------- | ------------------------------------------------------------------------------------------ |
| 0     | `first_zone_id` | Starting zone ID                                                                           |
| 1–15  | `packed`        | 15-byte verbatim delta payload (10×3 4-bit signed deltas, two per byte, high nibble first) |

Response: none (ack only).

### `set_range_rgb_zones` (fn 5) [#set_range_rgb_zones-fn-5]

Request: 16-byte long payload, up to 3 slots of 5 bytes each. Unused slots are zeroed.

| Bytes        | Field           | Notes                                        |
| ------------ | --------------- | -------------------------------------------- |
| `slot*5 + 0` | `first_zone_id` | First zone ID in range (inclusive), slot 0–2 |
| `slot*5 + 1` | `last_zone_id`  | Last zone ID in range (inclusive)            |
| `slot*5 + 2` | `red`           | Red channel                                  |
| `slot*5 + 3` | `green`         | Green channel                                |
| `slot*5 + 4` | `blue`          | Blue channel                                 |

Response: none (ack only).

### `set_rgb_zones_single_value` (fn 6) [#set_rgb_zones_single_value-fn-6]

Request: 16-byte long payload — one color applied to up to 13 individually addressed zones.

| Bytes | Field    | Notes                                  |
| ----- | -------- | -------------------------------------- |
| 0     | `red`    | Red channel of the single color        |
| 1     | `green`  | Green channel                          |
| 2     | `blue`   | Blue channel                           |
| 3–15  | zone IDs | Up to 13 zone IDs; unused bytes zeroed |

Response: none (ack only).

### `frame_end` (fn 7) [#frame_end-fn-7]

Request: 16-byte long payload — commits all staged zone changes.

| Bytes | Field                               | Notes                                                                           |
| ----- | ----------------------------------- | ------------------------------------------------------------------------------- |
| 0     | `persistence`                       | `FramePersistence` discriminant (`0` = volatile, `1` = volatile + non-volatile) |
| 1     | `current_frame` high byte           | Big-endian `u16`                                                                |
| 2     | `current_frame` low byte            |                                                                                 |
| 3     | `frames_till_next_change` high byte | Big-endian `u16`                                                                |
| 4     | `frames_till_next_change` low byte  |                                                                                 |
| 5–15  | —                                   | Zeroed padding                                                                  |

Response: none (ack only).

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::per_key_lighting::{
    FramePersistence, PerKeyLightingFeature, Rgb, RgbZone, ZonePresencePage,
}};

// mut device: Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<PerKeyLightingFeature>() {
    // Query which zone IDs are present on this device (first 112 zones)
    let presence = feat.get_rgb_zone_presence(ZonePresencePage::Zones0To111).await?;

    // Stage two individual zones: zone 1 = red, zone 2 = blue
    feat.set_individual_rgb_zones(&[
        RgbZone { zone_id: 1, color: Rgb { red: 0xff, green: 0x00, blue: 0x00 } },
        RgbZone { zone_id: 2, color: Rgb { red: 0x00, green: 0x00, blue: 0xff } },
    ]).await?;

    // Commit the staged changes to RAM only, one-shot (no animation)
    feat.frame_end(FramePersistence::Volatile, 0, 0).await?;
}
```


# 0x8090 · modeStatus (/hidpp/features/x8090-mode-status)



Switches a device between **performance** and **endurance** operating modes.
`getModeStatus` (function 0) reads the current `ModeStatus` (primary and secondary
status bytes); `setModeStatus` (function 1) writes them back using an explicit
changed-bit mask so callers update only the bits they intend to touch. The
convenience wrapper `set_performance_mode` drives the single `PERFORMANCE` flag
directly without requiring the caller to manage the mask. `getDeviceConfig`
(function 2) reports which switch mechanisms the device exposes.

* **`ModeStatus`** — the struct returned by `getModeStatus`, containing
  `status0: ModeStatus0` (primary flags) and `status1: u8` (secondary byte,
  reserved in v1 but preserved for callers).
* **`ModeStatus0::PERFORMANCE`** — set = performance mode; unset = endurance mode.
* **`ModeStatusCapabilities::HARDWARE_SWITCH`** — a physical switch on the device
  can change the mode bit.
* **`ModeStatusCapabilities::SOFTWARE_SWITCH`** — software may change the mode bit.
* **`ModeStatusChange`** — pairs desired values (`status0`, `status1`) with
  changed-bit masks (`changed_mask0`, `changed_mask1`); only masked bits are written.

> **Spec:** Logitech HID++ 2.0 — *modeStatus*. &#x2A;*Used by:** Typed wrapper in
> `openlogi-hidpp`.

## Function reference [#function-reference]

The `ModeStatusFeature` wrapper (`0x8090`) exposes:

### Methods [#methods]

| Function               | HID++ fn | Signature                    | Returns                  |
| ---------------------- | -------- | ---------------------------- | ------------------------ |
| `get_mode_status`      | 0        | `()`                         | `ModeStatus`             |
| `set_mode_status`      | 1        | `(change: ModeStatusChange)` | `()`                     |
| `set_performance_mode` | 1        | `(enabled: bool)`            | `()`                     |
| `get_device_config`    | 2        | `()`                         | `ModeStatusCapabilities` |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `ModeStatus` [#modestatus]

Current mode-status bytes returned by `get_mode_status`.

| Field     | Type          | Description                                                      |
| --------- | ------------- | ---------------------------------------------------------------- |
| `status0` | `ModeStatus0` | Primary status bits.                                             |
| `status1` | `u8`          | Secondary status byte, reserved by v1 but preserved for callers. |

#### `ModeStatusChange` [#modestatuschange]

A mode-status update request passed to `set_mode_status`.

| Field           | Type          | Description                    |
| --------------- | ------------- | ------------------------------ |
| `status0`       | `ModeStatus0` | Desired primary status bits.   |
| `status1`       | `u8`          | Desired secondary status byte. |
| `changed_mask0` | `ModeStatus0` | Primary changed-bit mask.      |
| `changed_mask1` | `u8`          | Secondary changed-bit mask.    |

#### `ModeStatus0` [#modestatus0]

The first mode-status byte.

| Flag          | Bit/Value | Description                                                    |
| ------------- | --------- | -------------------------------------------------------------- |
| `PERFORMANCE` | bit 0     | Performance mode. When unset, the device is in endurance mode. |

#### `ModeStatusCapabilities` [#modestatuscapabilities]

Capabilities reported by `ModeStatus`.

| Flag              | Bit/Value | Description                                |
| ----------------- | --------- | ------------------------------------------ |
| `HARDWARE_SWITCH` | bit 0     | A hardware switch can change the mode bit. |
| `SOFTWARE_SWITCH` | bit 1     | Software can change the mode bit.          |

## Wire format [#wire-format]

Getter requests carry a 3-byte zero payload; `set_mode_status` uses a 16-byte long payload. Responses are decoded from the extended 16-byte payload returned by each call.

### `get_mode_status` (fn 0) [#get_mode_status-fn-0]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field     | Notes                                          |
| ---- | --------- | ---------------------------------------------- |
| 0    | `status0` | `ModeStatus0` bitflags — bit 0 = `PERFORMANCE` |
| 1    | `status1` | Secondary status byte (reserved in v1)         |
| 2–15 | —         | Unused                                         |

### `set_mode_status` (fn 1) [#set_mode_status-fn-1]

Request (16-byte long payload):

| Byte | Field           | Notes                                               |
| ---- | --------------- | --------------------------------------------------- |
| 0    | `status0`       | `ModeStatus0::bits()` — desired primary flags       |
| 1    | `status1`       | Desired secondary byte                              |
| 2    | `changed_mask0` | `ModeStatus0::bits()` — which primary bits to write |
| 3    | `changed_mask1` | Which secondary bits to write                       |
| 4–15 | —               | Zero                                                |

Response: no bytes consumed (only error status checked).

### `set_performance_mode` (fn 1) [#set_performance_mode-fn-1]

Convenience wrapper. Sends `set_mode_status` with `status0 = PERFORMANCE` (or empty) and `changed_mask0 = PERFORMANCE`, `status1 = 0`, `changed_mask1 = 0`. Wire layout is identical to `set_mode_status` above.

### `get_device_config` (fn 2) [#get_device_config-fn-2]

Request: `[0x00, 0x00, 0x00]` (no parameters)

Response (byte → field):

| Byte | Field                    | Notes                                                |
| ---- | ------------------------ | ---------------------------------------------------- |
| 0    | `capabilities` high byte | Big-endian `u16` — bits 8–15 (none defined)          |
| 1    | `capabilities` low byte  | bit 0 = `HARDWARE_SWITCH`, bit 1 = `SOFTWARE_SWITCH` |
| 2–15 | —                        | Unused                                               |

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::mode_status::ModeStatusFeature};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<ModeStatusFeature>() {
    // Read current mode
    let status = feat.get_mode_status().await?;
    println!("performance mode: {}", status.status0.contains(hidpp::feature::mode_status::ModeStatus0::PERFORMANCE));

    // Switch to performance mode
    feat.set_performance_mode(true).await?;

    // Check which switch mechanisms the device exposes
    let caps = feat.get_device_config().await?;
    println!("hw switch: {}, sw switch: {}",
        caps.contains(hidpp::feature::mode_status::ModeStatusCapabilities::HARDWARE_SWITCH),
        caps.contains(hidpp::feature::mode_status::ModeStatusCapabilities::SOFTWARE_SWITCH));
}
```


# 0x8100 · onboardProfiles (/hidpp/features/x8100-onboard-profiles)



The `onboardProfiles` feature gives a host application full access to the
profiles stored in a device's onboard flash memory. A profile bundles settings
such as DPI levels, button remappings, lighting effects, and report-rate
preferences into a named, persistent slot that the device can apply
autonomously, even without a host driver running. This feature is most
commonly found on gaming mice, but also appears on some gaming keyboards and
other peripherals that carry onboard non-volatile storage.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Profile enumeration** — how many profile slots the device has and which is
  active.
* **Profile activation** — switch the device to any stored slot, or enter a
  host-controlled mode that applies per-application settings without touching
  the onboard flash.
* **Read / write profile data** — the raw profile payload: DPI presets, button
  assignments, lighting configuration.
* **Profile metadata** — per-profile display information (name, colour, icon
  index) for presenting a labelled profile list.
* **Factory reset** — usually a function to reset one or all profiles to
  defaults.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8100`


# 0x8110 · mouseButtonFilter (/hidpp/features/x8110-mouse-button-filter)



`mouseButtonFilter` controls which physical mouse buttons are forwarded to the
operating system as HID reports. It appears on mice and, in some
configurations, multi-mode pointer devices. Masking a button suppresses its
events at the firmware level, with no OS-level remapping involved. `0x1b04`
specialKeysMSEButtons handles remappable button assignments; this feature sits
lower and decides whether a button's raw input is reported at all.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Filter mask** — read and write a per-button mask deciding which physical
  buttons may generate HID input reports.
* **Selective suppression** — a filtered button is excluded from normal HID
  reporting without being remapped; the device simply emits no report for its
  presses.
* **Software handling** — the host can intercept a filtered button's raw HID++
  events and run the action entirely in software, bypassing the OS input
  stack.
* **Persistence** — depending on the device, the active mask is volatile
  (reset on power cycle) or stored in onboard memory; the feature likely has
  functions to query and set which.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8110`


# 0x8111 · latencyMonitoring (/hidpp/features/x8111-latency-monitoring)



`latencyMonitoring` measures end-to-end input latency on the device itself,
most commonly on gaming mice. Instead of host-side timing alone, the firmware
records the time between a physical input event (a button press, a sensor
sample) and the corresponding USB or wireless report reaching the host, so
latency can be observed under real operating conditions. The active report
rate directly affects what it measures, which ties it to `0x8060`
adjustableReportRate and `0x8061` extendedAdjustableReportRate.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Measurement** — trigger or read firmware-recorded measurements of the time
  from input event to report delivery.
* **Capability discovery** — which measurement modes and statistics the device
  supports: instantaneous, average, worst-case.
* **Session control** — some devices can start, stop, or configure a
  measurement session as a dedicated monitoring state separate from normal
  operation.
* **Results** — read back the collected statistics, possibly in microseconds
  and broken down by phase: sensor sampling, wireless transmission, USB
  delivery.
* **Events** — the device may notify the host asynchronously when a new result
  is ready or latency crosses a configured threshold.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8111`


# 0x8123 · forceFeedback (/hidpp/features/x8123-force-feedback)



The `forceFeedback` feature provides host-side control over force-feedback
actuators built into Logitech gaming peripherals, most commonly racing wheels,
flight sticks, and gamepads whose motors or resistance mechanisms simulate
physical sensations such as road texture, recoil, or spring centering. It is not relevant to mice, keyboards, headsets, or receivers that
lack such actuators. Where a device also exposes onboard profiles via `0x8100`
onboardProfiles, force-feedback effect parameters may be stored as part of those
profiles, but `0x8123` itself handles the real-time command and configuration
path for the actuator hardware.

<Callout type="warn" title="Not implemented in OpenLogi">
  OpenLogi's vendored `hidpp` crate does not provide a typed wrapper for this feature yet. The notes below summarise the HID++ feature in general terms, not OpenLogi behaviour.
</Callout>

## What it does [#what-it-does]

* **Capability discovery** — which effect types the device supports (constant
  force, spring, damper, periodic waveforms) and how many independent actuator
  axes it has.
* **Effect management** — create, modify, and destroy effects, with parameters
  like magnitude, direction, duration, and envelope shape (attack, sustain,
  fade).
* **Playback** — start, stop, and pause single effects or all at once, and set
  a global gain or autocenter strength.
* **Device state** — events or status flags may report actuator readiness,
  safety limits (overcurrent, thermal), or transitions between onboard and
  host-controlled operation.

> **Status:** Not implemented · &#x2A;*Feature ID:** `0x8123`


# 0x8300 · sidetone (/hidpp/features/x8300-sidetone)



Headset sidetone control: routes the microphone signal back into the ear cups so the wearer can hear their own voice while speaking. Two independent axes are exposed: `get_sidetone_level` / `set_sidetone_level` manage the playback level as a value in `0..=100` (the device rejects out-of-range writes), and `get_sidetone_mute` / `set_sidetone_mute` manage per-channel mute state via a bitmask.

* **`SidetoneMuteStatus`** — `statuses: u8` bitmask; a set bit means the corresponding channel is muted.
* **`SidetoneMuteChange`** — `change_mask: u8` selects which channels to update; `statuses: u8` carries the desired mute state for those channels. Bits absent from `change_mask` are left unchanged on the device.

> **Spec:** Logitech HID++ 2.0 — *Sidetone*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `SidetoneFeature` wrapper (`0x8300`) exposes:

### Methods [#methods]

| Function             | HID++ fn | Signature                      | Returns              |
| -------------------- | -------- | ------------------------------ | -------------------- |
| `get_sidetone_level` | 0        | `()`                           | `u8`                 |
| `set_sidetone_level` | 1        | `(level: u8)`                  | `()`                 |
| `get_sidetone_mute`  | 2        | `()`                           | `SidetoneMuteStatus` |
| `set_sidetone_mute`  | 3        | `(change: SidetoneMuteChange)` | `()`                 |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `SidetoneMuteStatus` [#sidetonemutestatus]

Per-channel sidetone mute statuses.

| Field      | Type | Description                                                    |
| ---------- | ---- | -------------------------------------------------------------- |
| `statuses` | `u8` | Raw mute-status bitmask. A set bit means the channel is muted. |

#### `SidetoneMuteChange` [#sidetonemutechange]

Change mask and statuses for sidetone mute settings.

| Field         | Type | Description                                                               |
| ------------- | ---- | ------------------------------------------------------------------------- |
| `change_mask` | `u8` | Channels to update. A set bit means the corresponding status bit applies. |
| `statuses`    | `u8` | Desired mute statuses. A set bit means the channel should be muted.       |

## Wire format [#wire-format]

All four functions use a short 3-byte request payload and a 16-byte long response payload (the crate reads results from `extend_payload()`). The feature index byte is managed by `FeatureEndpoint`; only the function-specific payload bytes are shown below.

### `get_sidetone_level` (fn 0) [#get_sidetone_level-fn-0]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are padding.

Response (byte → field):

| Byte | Field        | Notes                                  |
| ---- | ------------ | -------------------------------------- |
| 0    | level (`u8`) | Sidetone level in the range `0..=100`. |

### `set_sidetone_level` (fn 1) [#set_sidetone_level-fn-1]

Request: `[level, 0x00, 0x00]`

| Byte | Value   | Notes                                                             |
| ---- | ------- | ----------------------------------------------------------------- |
| 0    | `level` | Sidetone level to write. Device rejects values outside `0..=100`. |
| 1–2  | `0x00`  | Padding.                                                          |

Response: acknowledged only; no payload fields are read.

### `get_sidetone_mute` (fn 2) [#get_sidetone_mute-fn-2]

Request: `[0x00, 0x00, 0x00]` — no parameters; all bytes are padding.

Response (byte → field):

| Byte | Field                                 | Notes                                           |
| ---- | ------------------------------------- | ----------------------------------------------- |
| 0    | `SidetoneMuteStatus::statuses` (`u8`) | Bitmask; a set bit means that channel is muted. |

### `set_sidetone_mute` (fn 3) [#set_sidetone_mute-fn-3]

Request: `[change_mask, statuses, 0x00]`

| Byte | Value                             | Notes                                                  |
| ---- | --------------------------------- | ------------------------------------------------------ |
| 0    | `SidetoneMuteChange::change_mask` | Selects which channel bits to overwrite on the device. |
| 1    | `SidetoneMuteChange::statuses`    | Desired mute state for the selected channels.          |
| 2    | `0x00`                            | Padding.                                               |

Response: acknowledged only; no payload fields are read.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::sidetone::{SidetoneMuteChange, SidetoneFeature}};

// device: &mut Device, already created via Device::new(channel, index).await?
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<SidetoneFeature>() {
    // Read the current sidetone level (0–100)
    let level = feat.get_sidetone_level().await?;
    println!("Sidetone level: {level}");

    // Lower it by 10, clamped to the valid range
    feat.set_sidetone_level(level.saturating_sub(10)).await?;

    // Read current per-channel mute state
    let mute = feat.get_sidetone_mute().await?;
    println!("Mute bitmask: 0b{:08b}", mute.statuses);

    // Mute channel 0 (bit 0) without touching other channels
    feat.set_sidetone_mute(SidetoneMuteChange {
        change_mask: 0b0000_0001,
        statuses:    0b0000_0001,
    }).await?;
}
```


# 0x8310 · equalizer (/hidpp/features/x8310-equalizer)



Per-band audio equalizer and microphone noise reduction for HID++ headsets.
`getEqInfo` (function 0) returns the table dimensions: `band_count`, the
gain range in dB, and an `EqCapabilities` flag indicating whether values are
stored as gains or as DSP coefficients. `getFrequencies` (function 1) pages
through the fixed centre frequency (Hz) of every band, up to seven per response.
`getFrequencyGains` / `setFrequencyGains` (functions 2–3) read and write the
signed per-band gains (dB), with `setFrequencyGains` accepting a
`GainPersistence` selector that controls whether the new values land in RAM
only, EEPROM only, or both simultaneously. A separate pair of functions (4–5)
reads and writes hardware **microphone noise reduction** as a single boolean.

* **`EqInfo`** — `band_count` (up to 15 bands), `db_range` / `db_min` /
  `db_max` (gain limits in dB; `effective_range()` resolves the
  "both-zero implies `±db_range`" shorthand), `capabilities`.
* **`GainLocation`** — `Eeprom` (the persistent custom EQ) or `Ram` (the
  currently active EQ).
* **`GainPersistence`** — `Volatile` (RAM only), `VolatileAndNonVolatile`
  (RAM + EEPROM), or `NonVolatileOnly` (EEPROM only).

> **Spec:** Logitech HID++ 2.0 — *x8310 equalizer*. &#x2A;*Used by:** Typed wrapper in `openlogi-hidpp`.

## Function reference [#function-reference]

The `EqualizerFeature` wrapper (`0x8310`) exposes:

### Methods [#methods]

| Function                  | HID++ fn | Signature                                      | Returns    |
| ------------------------- | -------- | ---------------------------------------------- | ---------- |
| `get_eq_info`             | 0        | `()`                                           | `EqInfo`   |
| `get_frequencies`         | 1        | `(band_count: u8)`                             | `Vec<u16>` |
| `get_frequency_gains`     | 2        | `(location: GainLocation, band_count: u8)`     | `Vec<i8>`  |
| `set_frequency_gains`     | 3        | `(persistence: GainPersistence, gains: &[i8])` | `Vec<i8>`  |
| `get_mic_noise_reduction` | 4        | `()`                                           | `bool`     |
| `set_mic_noise_reduction` | 5        | `(enabled: bool)`                              | `()`       |

All methods are `async` and return `Result<…, Hidpp20Error>`.

### Types [#types]

#### `EqInfo` [#eqinfo]

EQ table information returned by `get_eq_info`.

| Field          | Type             | Description                                                                |
| -------------- | ---------------- | -------------------------------------------------------------------------- |
| `band_count`   | `u8`             | Number of frequency bands.                                                 |
| `db_range`     | `u8`             | Gain range in dB; used as `±db_range` when `db_min`/`db_max` are both `0`. |
| `capabilities` | `EqCapabilities` | How EQ values are stored.                                                  |
| `db_min`       | `i8`             | Minimum gain in dB, or `0` to imply `-db_range`.                           |
| `db_max`       | `i8`             | Maximum gain in dB, or `0` to imply `+db_range`.                           |

**Method:** `effective_range(&self) -> (i8, i8)` — resolves the "both-zero implies `±db_range`" shorthand into concrete `(min, max)` bounds.

#### `EqCapabilities` [#eqcapabilities]

Bitflags indicating how a device stores its EQ values, from `get_eq_info`.

| Flag                     | Bit/Value | Description                           |
| ------------------------ | --------- | ------------------------------------- |
| `STORED_AS_GAINS`        | `0x01`    | EQ values are stored as gains.        |
| `STORED_AS_COEFFICIENTS` | `0x02`    | EQ values are stored as coefficients. |

#### `GainLocation` [#gainlocation]

Selects which copy of the EQ gains `get_frequency_gains` reads from.

| Variant  | Value | Description                                             |
| -------- | ----- | ------------------------------------------------------- |
| `Eeprom` | `0`   | The custom EQ stored in EEPROM (the version-0 default). |
| `Ram`    | `1`   | The active EQ in RAM.                                   |

#### `GainPersistence` [#gainpersistence]

Controls how `set_frequency_gains` persists the new gains.

| Variant                  | Value | Description                          |
| ------------------------ | ----- | ------------------------------------ |
| `Volatile`               | `0`   | Applied to RAM only.                 |
| `VolatileAndNonVolatile` | `1`   | Applied to RAM and stored in EEPROM. |
| `NonVolatileOnly`        | `2`   | Stored in EEPROM only.               |

## Wire format [#wire-format]

Simple getters carry a 3-byte request payload (all zeroes except where noted); `set_frequency_gains` uses a 16-byte long request. All responses are read from the 16-byte extended payload.

### `get_eq_info` (fn 0) [#get_eq_info-fn-0]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field          | Notes                                                                                   |
| ---- | -------------- | --------------------------------------------------------------------------------------- |
| 0    | `band_count`   | Number of frequency bands.                                                              |
| 1    | `db_range`     | Symmetric gain range in dB when `db_min`/`db_max` are both `0`.                         |
| 2    | `capabilities` | `EqCapabilities` bitflags: bit 0 = `STORED_AS_GAINS`, bit 1 = `STORED_AS_COEFFICIENTS`. |
| 3    | `db_min`       | Minimum gain (signed `i8`), or `0` to imply `-db_range`.                                |
| 4    | `db_max`       | Maximum gain (signed `i8`), or `0` to imply `+db_range`.                                |

### `get_frequencies` (fn 1) [#get_frequencies-fn-1]

Paged: the wrapper issues one request per page of up to 7 bands until all `band_count` frequencies are collected.

Request: `[start_index, 0x00, 0x00]`

| Byte | Field         | Notes                                 |
| ---- | ------------- | ------------------------------------- |
| 0    | `start_index` | Index of the first band on this page. |

Response (byte → field):

| Byte | Field                | Notes                                                                |
| ---- | -------------------- | -------------------------------------------------------------------- |
| 0    | echoed `start_index` | Validated by the wrapper; returns `UnsupportedResponse` on mismatch. |
| 1–2  | frequency\[0]        | Big-endian `u16`, Hz.                                                |
| 3–4  | frequency\[1]        | Big-endian `u16`, Hz.                                                |
| …    | …                    | Up to 7 frequencies per page (bytes 1–14).                           |

### `get_frequency_gains` (fn 2) [#get_frequency_gains-fn-2]

Request: `[location, 0x00, 0x00]`

| Byte | Field      | Notes                                            |
| ---- | ---------- | ------------------------------------------------ |
| 0    | `location` | `GainLocation` as `u8`: `0` = Eeprom, `1` = Ram. |

Response (byte → field):

| Byte | Field | Notes                                                                    |
| ---- | ----- | ------------------------------------------------------------------------ |
| 0–N  | gains | One signed `i8` per band (cast from raw byte), up to `band_count` bytes. |

### `set_frequency_gains` (fn 3) [#set_frequency_gains-fn-3]

Uses `call_long`, a 16-byte request payload.

Request: `[persistence, gain[0], gain[1], …, gain[14]]`

| Byte | Field         | Notes                                                                                     |
| ---- | ------------- | ----------------------------------------------------------------------------------------- |
| 0    | `persistence` | `GainPersistence` as `u8`: `0`=Volatile, `1`=VolatileAndNonVolatile, `2`=NonVolatileOnly. |
| 1–15 | gains         | Each gain cast from `i8` to `u8`; up to 15 bands.                                         |

Response (byte → field):

| Byte | Field                | Notes                                         |
| ---- | -------------------- | --------------------------------------------- |
| 0    | echoed `persistence` | Skipped by the wrapper.                       |
| 1–N  | echoed gains         | Parsed as signed `i8`; returned as `Vec<i8>`. |

### `get_mic_noise_reduction` (fn 4) [#get_mic_noise_reduction-fn-4]

Request: `[0x00, 0x00, 0x00]`

Response (byte → field):

| Byte | Field   | Notes                                                    |
| ---- | ------- | -------------------------------------------------------- |
| 0    | enabled | Non-zero = enabled; zero = disabled. Returned as `bool`. |

### `set_mic_noise_reduction` (fn 5) [#set_mic_noise_reduction-fn-5]

Request: `[enabled, 0x00, 0x00]`

| Byte | Field     | Notes                        |
| ---- | --------- | ---------------------------- |
| 0    | `enabled` | `1` = enable, `0` = disable. |

Response is ignored.

## Usage (Rust) [#usage-rust]

```rust
use hidpp::{device::Device, feature::equalizer::{EqualizerFeature, GainLocation, GainPersistence}};

// device: &mut Device, already created via Device::new(channel, index)
device.enumerate_features().await?;
if let Some(feat) = device.get_feature::<EqualizerFeature>() {
    // Query EQ table dimensions.
    let info = feat.get_eq_info().await?;
    let (min_db, max_db) = info.effective_range();

    // Read centre frequencies of all bands.
    let freqs = feat.get_frequencies(info.band_count).await?;

    // Read the currently active gains from RAM.
    let gains = feat.get_frequency_gains(GainLocation::Ram, info.band_count).await?;

    // Apply a flat EQ to RAM (volatile, no EEPROM write).
    let flat: Vec<i8> = vec![0i8; usize::from(info.band_count)];
    let echoed = feat.set_frequency_gains(GainPersistence::Volatile, &flat).await?;

    // Toggle microphone noise reduction.
    let nr_on = feat.get_mic_noise_reduction().await?;
    feat.set_mic_noise_reduction(!nr_on).await?;
}
```


# Configuration (/docs/configurations)



OpenLogi stores everything in a single **TOML** file. The GUI writes it for you:
the main window edits button bindings, the Actions Ring, DPI presets, SmartShift,
scrolling, lighting, and camera controls, and the **Settings window** (⌘,) covers
the app-wide preferences, but the file is plain text and safe to hand-edit.
Per-app binding overlays have no dedicated editor yet, so those are authored here
directly.

<Callout type="info">
  The agent reads the config on startup and rewrites it atomically on every
  change. Hand-edit it while OpenLogi is **quit**, or the running agent will
  overwrite your edits the next time it saves. Before its first save in each
  process, OpenLogi copies the previous file to `config.toml.backup.1` and
  rotates up to `config.toml.backup.5`.
</Callout>

## File location [#file-location]

| Platform      | Path                                                                               |
| ------------- | ---------------------------------------------------------------------------------- |
| macOS / Linux | `$XDG_CONFIG_HOME/openlogi/config.toml` (default `~/.config/openlogi/config.toml`) |
| Windows       | `%USERPROFILE%\.config\openlogi\config.toml`                                       |

The file is written atomically (temp file + rename) and, on Unix, with `0600`
permissions.

## Top-level layout [#top-level-layout]

```toml
schema_version = 4                             # required; a newer version is refused
selected_device = "receiver:aabbccdd:slot:1"   # physical key of the device shown in the carousel

[app_settings]                                 # app-wide preferences (omitted entirely when default)
# …

[devices."receiver:aabbccdd:slot:1"]           # one block per physical device
# …

[keyboard.bindings]                            # OS-level function-key remapper
# …
```

* **`schema_version`** — the layout version (currently `4`). Older files are
  migrated on load; a file declaring a **newer** version is refused rather than
  silently misread. v4 dropped the one-gesture-button-per-device owner lock
  (`gesture_owner`), v3 moved the device map from model keys to physical-device
  keys, and v2 merged `button_bindings` / `gesture_bindings` into one `bindings`
  map.
* **`selected_device`** — remembers which device the carousel was on; omitted
  when unset.
* **`[app_settings]`** — see below; the whole block is omitted while every field
  is at its default.
* **`[devices.<key>]`** — per-device settings, keyed by **physical device
  identity** (see below).
* **`[keyboard]`** — device-independent function-key remapping, driven by the OS
  hook rather than HID++.

## Device keys [#device-keys]

Since schema v3 a device block is keyed by the *physical* device, not by model,
so two identical mice never share one entry:

| Form                                                      | Used for                                          |
| --------------------------------------------------------- | ------------------------------------------------- |
| `receiver:<receiver-uid>:slot:<n>`                        | Paired to a Bolt / Unifying / Lightspeed receiver |
| `direct:<vid>:<pid>:serial:<serial>` (or `:unit:<hex>`)   | Bluetooth-direct or USB-wired HID++ device        |
| `raw:<vid>:<pid>:<usage-page>:<usage-id>:serial:<serial>` | Raw-HID device such as a Litra light              |

Keys are lower-case hex. The GUI writes the right key for you; the reliable way
to get one is to configure the device once in the app, then read the key back out
of `config.toml`. A device that reports no serial and no unit id has no stable
identity, so OpenLogi does not persist settings for it.

## `[app_settings]` [#app_settings]

| Key                      | Default           | Meaning                                                                                                                                                                                                   |
| ------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `launch_at_login`        | `false`           | Start the agent on login. A LaunchAgent plist on macOS, a systemd user unit on Linux.                                                                                                                     |
| `check_for_updates`      | `false`           | Opt-in. One `HEAD` request to the GitHub latest-release per launch; logs whether a newer version exists; never downloads on its own.                                                                      |
| `auto_install_updates`   | `false`           | Opt-in, and only acts when `check_for_updates` is on: downloads and stages a newer version in the background, applied on the next restart.                                                                |
| `update_prompt_seen`     | `false`           | Set once the first-run "check for updates?" prompt has been answered, so it is never shown again.                                                                                                         |
| `show_in_menu_bar`       | `true`            | macOS menu-bar status item and Windows tray icon. Ignored on Linux.                                                                                                                                       |
| `capture_mouse_events`   | `true`            | Whether the agent installs the OS mouse hook at all. `false` stops button remapping and grabs no input device; DPI, SmartShift, and the other HID++ features keep working. Takes effect on agent restart. |
| `auto_download_assets`   | `true`            | Fetch device renders when a device appears. `false` makes no asset network requests; **Refresh assets** in Settings still fetches on demand.                                                              |
| `asset_source`           | `automatic`       | Asset mirror: `automatic` (race every built-in mirror), `openlogi`, `cloudflare`, or `fastly`.                                                                                                            |
| `language`               | *(follow system)* | UI locale, one of the 20 bundled locales (`en`, `de`, `pt-BR`, `zh-CN`, …). Unset follows the system locale.                                                                                              |
| `thumbwheel_sensitivity` | `14`              | Thumb-wheel responsiveness on a `1`–`100` scale; the default is 1× native scroll (the wheel is only diverted from native scrolling once this leaves the default).                                         |
| `appearance`             | `system`          | `system`, `light`, or `dark`.                                                                                                                                                                             |
| `theme_light`            | *(brand theme)*   | Theme name used in light mode, e.g. `"OpenLogi Light"`.                                                                                                                                                   |
| `theme_dark`             | *(brand theme)*   | Theme name used in dark mode.                                                                                                                                                                             |
| `ui_radius`              | *(theme default)* | Corner-radius override in pixels; the Appearance page offers `0` / `6` / `12`.                                                                                                                            |

## Per-device blocks [#per-device-blocks]

Each `[devices.<key>]` block holds the settings for one physical device.

| Key                      | Type            | Meaning                                                                                                                                                    |
| ------------------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                | bool            | `false` leaves the device completely native: no HID++ capture session, no settings re-applied on reconnect. Default `true`.                                |
| `bindings`               | table           | Maps a logical button to a binding: a single action, or a per-direction gesture table (see [Buttons](#buttons) and [Gesture bindings](#gesture-bindings)). |
| `per_app_bindings`       | table of tables | Overlays keyed by app id. While that app is frontmost its entries win; everything else falls through to `bindings`.                                        |
| `action_ring`            | table           | [Actions Ring](/docs/features/actions-ring) enable state, haptics, default layout, and per-app layouts.                                                    |
| `dpi_presets`            | array of ints   | Ordered DPI values cycled by `CycleDpiPresets` and indexed by `SetDpiPreset`.                                                                              |
| `dpi`                    | int             | The committed sensor DPI. Lives in device RAM, so the agent re-applies it on reconnect.                                                                    |
| `smartshift`             | table           | `mode` (`ratchet` / `free`), `auto_disengage`, `tunable_torque`; re-applied on reconnect.                                                                  |
| `invert_scroll`          | bool            | Reverse this device's native wheel direction without touching the system trackpad direction.                                                               |
| `scroll_resolution`      | string          | `low` or `high`. Persisted HID++ `0x2121` wheel resolution. Absent leaves the device's own setting alone.                                                  |
| `thumbwheel_sensitivity` | int             | Per-device override of the app-wide value.                                                                                                                 |
| `lighting`               | table           | Static RGB for HID++ keyboards; see below.                                                                                                                 |
| `light`                  | table           | Standalone light (Litra) power, brightness, temperature; see [Litra lights](/docs/features/lights).                                                        |
| `camera_controls`        | table           | [Webcam](/docs/features/webcams) UVC controls, keyed by control name.                                                                                      |
| `camera_profiles`        | table of tables | User-saved camera profiles (name → control snapshot).                                                                                                      |
| `camera_profile`         | string          | The camera profile last applied from the GUI.                                                                                                              |
| `host_switch_targets`    | array of keys   | Device keys of mice that follow this keyboard's Easy-Switch channel.                                                                                       |
| `fn_lock`                | bool            | Keyboards only. `true` makes the F-row send F1–F12 without holding Fn; absent leaves the keyboard's own state alone. Re-applied on reconnect.              |
| `identity`               | table           | Written by the app: last-known name, kind, and capabilities, so a sleeping device still renders its panels. Not meant to be hand-authored.                 |
| `disabled_gestures`      | table           | Written by the app: the direction map of a button whose gesture mode is currently off, so re-enabling restores it.                                         |

### `lighting` [#lighting]

| Key          | Default    | Meaning                                                   |
| ------------ | ---------- | --------------------------------------------------------- |
| `enabled`    | `true`     | Whether the static color is applied.                      |
| `color`      | `"ffffff"` | Static color as six hex digits `RRGGBB` (no leading `#`). |
| `brightness` | `100`      | `0`–`100`; clamped on load.                               |

### `light` [#light]

| Key                  | Default   | Meaning                                                                                |
| -------------------- | --------- | -------------------------------------------------------------------------------------- |
| `enabled`            | `true`    | Whether the light should be on.                                                        |
| `auto_camera`        | `false`   | Turn the light on while any camera is in use, off when camera use stops (macOS).       |
| `brightness_percent` | `100`     | `0`–`100`, mapped to the device's native range (a Litra's 20–250 lumens, for example). |
| `temperature_kelvin` | *(unset)* | Colour temperature, when the device supports it (Litra: 2700–6500 K in 100 K steps).   |

## Buttons [#buttons]

`bindings` and `per_app_bindings` are keyed by a **logical button**.

Mouse controls: `LeftClick`, `RightClick`, `MiddleClick`, `Back`, `Forward`,
`DpiToggle` (the mode-shift button under the wheel), `Thumbwheel` (its click),
`ThumbwheelScrollUp`, `ThumbwheelScrollDown`, `GestureButton`, `HapticPanel`
(the MX Master 4 Haptic Sense Panel).

Keyboard F-row controls, diverted over HID++ only when you bind them:
`KeySearch`, `KeyDictation`, `KeyEmoji`, `KeyScreenCapture`, `KeyMicMute`,
`KeyPlayPause`, `KeyMute`, `KeyVolumeDown`, `KeyVolumeUp`. See
[Keyboards](/docs/features/keyboard).

## Actions [#actions]

Binding values are action names, written verbatim:

* **Suppress** — `None` (capture the input but do nothing)
* **Mouse** — `LeftClick`, `RightClick`, `MiddleClick`, `MouseBack`, `MouseForward` (the real extra-button events most apps treat as native back/forward)
* **Editing** — `Copy`, `Paste`, `Cut`, `Undo`, `Redo`, `SelectAll`, `Find`, `Save`
* **Browser & tabs** — `BrowserBack`, `BrowserForward`, `NewTab`, `CloseTab`, `ReopenTab`, `NextTab`, `PrevTab`, `ReloadPage`
* **Window & desktop (macOS)** — `MissionControl`, `AppExpose`, `PreviousDesktop`, `NextDesktop`, `ShowDesktop`, `LaunchpadShow`
* **System** — `LockScreen`, `Screenshot`, `CaptureRegion`, `Sleep`, `ShowActionsRing`, `OpenApplication`
* **Media** — `PlayPause`, `NextTrack`, `PrevTrack`, `VolumeUp`, `VolumeDown`, `MuteVolume`
* **DPI & wheel** — `CycleDpiPresets`, `SetDpiPreset`, `ToggleSmartShift`
* **Scroll** — `ScrollUp`, `ScrollDown`, `HorizontalScrollLeft`, `HorizontalScrollRight`
* **Power user** — `CustomShortcut`, `TypeText`, `RunAppleScript`, `RunShellCommand`, `Workflow`

The picker lists 44 plain actions. `ShowActionsRing` is written by hand (it isn't
in the picker), and the parameterized actions are written as a single-key table:

```toml
MiddleClick = "MissionControl"                              # plain action
DpiToggle = { SetDpiPreset = 2 }                            # preset index
Back = { CustomShortcut = "Cmd+Shift+P" }                   # key chord
Forward = { OpenApplication = { path = "~/Downloads", display_name = "Downloads" } }
```

`OpenApplication` takes an application, folder, filesystem path, or URL; a
leading `~` is expanded when the action runs. `CustomShortcut` stores a
platform-neutral chord such as `Cmd+Shift+P`, `Ctrl+Alt+Left`, or `F5`. The GUI
can author all of these: custom shortcuts and **Open application** from the
action picker, and `TypeText` / `RunAppleScript` / `RunShellCommand` / `Workflow`
under its **Power User** submenu.

## Gesture bindings [#gesture-bindings]

Any capable button can be in **gesture mode**: its `bindings` entry becomes a
sub-table keyed by `Up`, `Down`, `Left`, `Right`, and `Click` (the plain press,
no swipe) instead of holding a single action. Since schema v4 that is a
per-button fact; several buttons can be in gesture mode at once, and the old
device-wide `gesture_owner` key is gone (a v3 file's owner is migrated to the
equivalent binding shapes on load).

```toml
[devices."receiver:aabbccdd:slot:1".bindings.GestureButton]
Up = "MissionControl"
Down = "ShowDesktop"
Left = "PrevTab"
Right = "NextTab"
Click = "AppExpose"
```

The dedicated Gesture Button and the MX Master 4 Haptic Sense Panel are captured
over HID++ raw-XY; middle / back / forward gestures ride the OS hook.

## `[keyboard]` [#keyboard]

A device-independent remapper for function keys on **any** keyboard, driven by
the OS hook. Keys are triggers of the form `[modifier+]…key`, with modifiers
`shift`, `control` (`ctrl`), `option` (`alt`), `command` (`cmd`), and keys `esc`
and `f1`–`f19`:

```toml
[keyboard.bindings]
f1 = "MissionControl"
"shift+f2" = "ShowDesktop"
"cmd+f5" = { CustomShortcut = "Cmd+Shift+P" }
```

This is separate from a Logitech keyboard's HID++ F-row bindings under
`[devices.<key>.bindings]`; see [Keyboards](/docs/features/keyboard) for when to
use which.

## Example [#example]

```toml
schema_version = 4
selected_device = "receiver:aabbccdd:slot:1"

[app_settings]
launch_at_login = true
language = "zh-CN"
thumbwheel_sensitivity = 14
appearance = "system"

# MX Master 4 in slot 1 of a Bolt receiver.
[devices."receiver:aabbccdd:slot:1"]
dpi_presets = [800, 1600, 3200]
dpi = 1600
invert_scroll = true
scroll_resolution = "high"

[devices."receiver:aabbccdd:slot:1".bindings]
Back = "BrowserBack"
Forward = "BrowserForward"
MiddleClick = "MissionControl"
HapticPanel = "ShowActionsRing"

# The gesture button binds per direction; Click is the plain press.
[devices."receiver:aabbccdd:slot:1".bindings.GestureButton]
Left = "PrevTab"
Right = "NextTab"
Click = "PlayPause"

# Back becomes Undo only while VS Code is frontmost.
[devices."receiver:aabbccdd:slot:1".per_app_bindings."com.microsoft.VSCode"]
Back = "Undo"

[devices."receiver:aabbccdd:slot:1".smartshift]
mode = "ratchet"
auto_disengage = 16
tunable_torque = 0

[devices."receiver:aabbccdd:slot:1".action_ring]
enabled = true
haptics = true

[devices."receiver:aabbccdd:slot:1".action_ring.default.slots]
Top = { action = "Cut" }
TopRight = { action = "Copy" }
Right = { action = "Paste", label = "Paste It" }
BottomRight = { action = "BrowserForward" }
Bottom = { action = "PlayPause" }
BottomLeft = { action = "BrowserBack" }
Left = { action = "Undo" }
TopLeft = { action = "Redo" }

# A Signature-series keyboard: F-row keys diverted over HID++, Fn-lock off.
[devices."receiver:aabbccdd:slot:2"]
fn_lock = false
host_switch_targets = ["receiver:aabbccdd:slot:1"]

[devices."receiver:aabbccdd:slot:2".bindings]
KeySearch = "MissionControl"
KeyScreenCapture = "CaptureRegion"

[devices."receiver:aabbccdd:slot:2".lighting]
enabled = true
color = "ff0000"
brightness = 80

# A Litra Glow, keyed by its raw-HID identity.
[devices."raw:046d:c900:ff43:0202:serial:YOUR-SERIAL".light]
enabled = true
auto_camera = true
brightness_percent = 65
temperature_kelvin = 4600
```

**Source:**
[CONFIGURATION.md](https://github.com/AprilNEA/OpenLogi/blob/master/docs/CONFIGURATION.md)


# MX Master 4 (/docs/devices/mx-master-4)





Logitech's registry lists this mouse as model id `2b042` — the string OpenLogi
resolves its renders and hotspots by, and the one the
[model id index](/hidpp/model-ids) keys it under. What follows describes the
product; what your own unit answers is what `openlogi diag features` prints.

## Controls [#controls]

<DeviceFigure depot="mx_master_4" name="MX Master 4" />

Each control binds under a logical name in `config.toml`, and each is diverted
from its native behaviour only once you bind it:

| # | Control            | Binds as                                                   | Diverted through                                           |
| - | ------------------ | ---------------------------------------------------------- | ---------------------------------------------------------- |
| 1 | Middle click       | `MiddleClick`                                              | [`0x1b04`](/hidpp/features/x1b04-special-keys-mse-buttons) |
| 2 | Mode-shift button  | `DpiToggle`                                                | `0x1b04`                                                   |
| 3 | Thumb wheel        | `Thumbwheel`, `ThumbwheelScrollUp`, `ThumbwheelScrollDown` | [`0x2150`](/hidpp/features/x2150-thumbwheel)               |
| 4 | Gesture button     | `GestureButton`                                            | `0x1b04`                                                   |
| 5 | Forward            | `Forward`                                                  | `0x1b04`                                                   |
| 6 | Back               | `Back`                                                     | `0x1b04`                                                   |
| 7 | Haptic Sense panel | `HapticPanel`                                              | a divertable `0x01a0` control in the `0x1b04` table        |

Any of them can be put in **gesture mode**, where the binding becomes a table of
`Up` / `Down` / `Left` / `Right` / `Click` instead of one action — several at
once, since schema v4. See [Remap buttons](/docs/features/mouse/remap-buttons).

The Haptic Sense panel is the one control this mouse has and its predecessors
don't. It is an ordinary divertable control in the reprogrammable-controls
table, so it binds like any button — `ShowActionsRing` is the binding that makes
it open the [Actions Ring](/docs/features/actions-ring) — and the haptics
themselves come from [`0x19b0`](/hidpp/features/x19b0-haptic-feedback).

## Panels this mouse lights up [#panels-this-mouse-lights-up]

Panels are gated on the feature ids the device reports, never on its marketing
type, so this list is what an MX Master 4 normally answers with:

| Panel                               | Feature                                                                  | Page                                          |
| ----------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------- |
| DPI presets                         | `0x2201` AdjustableDpi, or `0x2202` extended                             | [DPI](/docs/features/mouse/dpi)               |
| SmartShift                          | [`0x2111`](/hidpp/features/x2111-smartshift-enhanced) SmartShiftEnhanced | [SmartShift](/docs/features/mouse/smartshift) |
| Scroll inversion & wheel resolution | [`0x2121`](/hidpp/features/x2121-hires-wheel) HiResWheel                 | [Scrolling](/docs/features/mouse/scrolling)   |
| Thumb wheel                         | [`0x2150`](/hidpp/features/x2150-thumbwheel) Thumbwheel                  | [Scrolling](/docs/features/mouse/scrolling)   |
| Haptic feedback                     | [`0x19b0`](/hidpp/features/x19b0-haptic-feedback) HapticFeedback         | [Actions Ring](/docs/features/actions-ring)   |
| Battery                             | [`0x1004`](/hidpp/features/x1004-unified-battery) UnifiedBattery         | —                                             |
| Host switching                      | [`0x1814`](/hidpp/features/x1814-change-host) ChangeHost                 | —                                             |

A panel that doesn't appear means the unit didn't report its feature; the
authority is `openlogi diag features`, not this table.

## Configure it [#configure-it]

Device blocks are keyed by the **physical device**, not by model — two MX Master
4s on one machine never share an entry. Configure the mouse once in the app,
then read the key back out of `config.toml`; `receiver:…` is the shape for a
Bolt-paired mouse, `direct:…` for one paired over Bluetooth.

```toml
[devices."receiver:aabbccdd:slot:1"]
dpi = 1600
dpi_presets = [800, 1600, 3200]
invert_scroll = false

[devices."receiver:aabbccdd:slot:1".smartshift]
mode = "ratchet"
auto_disengage = 20

[devices."receiver:aabbccdd:slot:1".bindings]
MiddleClick = "MissionControl"
DpiToggle = { SetDpiPreset = 2 }
HapticPanel = "ShowActionsRing"
Back = { CustomShortcut = "Cmd+Shift+P" }

[devices."receiver:aabbccdd:slot:1".bindings.GestureButton]
Up = "MissionControl"
Down = "ShowDesktop"
Left = "PrevTab"
Right = "NextTab"
Click = "AppExpose"
```

Every key above, and the full action list, is in
[Configuration](/docs/configurations).

## Identity [#identity]

The registry's `2b042` is not what the firmware reports: an MX Master 4 answers
`0x0003 deviceInformation` with `ext=01` and PID `b042`, so it is the trailing
four digits that line up, not the prefix — OpenLogi matches on them, and
[Model IDs](/hidpp/model-ids) explains why. `openlogi list` prints the raw pair
under the paired device.


# Architecture (/docs/project/architecture)



OpenLogi separates one authoritative **background agent** from two GPUI clients.
The agent owns HID++ inventory and channels, input capture, binding resolution,
device writes, and action execution. The settings app mirrors agent snapshots
and sends commands; the Actions Ring overlay only renders and reports user
interactions. The CLI is independent and opens hardware directly.

## Components [#components]

* **OpenLogi agent** (`openlogi-agent`) — the background process: HID++ device
  I/O, input hooks and capture sessions, per-app watching, pairing, action
  dispatch, the menu-bar / tray item, and supervision of the overlay helper.
* **OpenLogi desktop** (`openlogi-desktop`) — the GPUI settings app. Its local
  state is a presentation and editing mirror; HID++ reads and writes go through
  the agent. Webcam preview and UVC controls remain direct desktop operations.
* **Actions Ring overlay** (`openlogi-overlay`) — a warm, separately supervised
  GPUI process. It receives presentation-only ring snapshots and returns hover,
  activation, and cancellation events; the agent retains and executes actions.
* **OpenLogi CLI** (`openlogi`) — headless inventory, asset sync, light and
  camera control, and HID++ diagnostics. It deliberately bypasses agent IPC.
* **[assets.openlogi.org](https://assets.openlogi.org)** — a static host serving
  per-device renders and clickable-hotspot metadata, keyed by each device's
  `modelId`; a versioned Cloudflare Pages origin and jsDelivr npm shards are
  raced as mirrors.

## Component relationships [#component-relationships]

### Processes and local state [#processes-and-local-state]

`openlogi-ipc` is a shared contract and transport library, not another process.
The desktop and overlay connect to the same endpoint, while the CLI deliberately
bypasses it.

<Mermaid
  chart="flowchart TB
  subgraph Clients[&#x22;Client processes&#x22;]
    direction LR
    desktop[&#x22;openlogi-desktop<br/>Settings GUI&#x22;]
    overlay[&#x22;openlogi-overlay<br/>Actions Ring renderer&#x22;]
    cli[&#x22;openlogi<br/>CLI&#x22;]
  end

  ipc[&#x22;openlogi-ipc<br/>tarpc contract + local socket transport&#x22;]
  agent[&#x22;openlogi-agent<br/>Authoritative device + action runtime&#x22;]
  mock[&#x22;openlogi-agent-mock<br/>Development substitute&#x22;]
  config[(&#x22;config.toml&#x22;)]
  direct[&#x22;Direct hardware access<br/>CLI only&#x22;]

  desktop <-->|&#x22;snapshots + commands&#x22;| ipc
  overlay <-->|&#x22;ring state + interactions&#x22;| ipc
  ipc --- agent
  mock -.->|&#x22;alternative IPC server&#x22;| ipc
  agent -.->|&#x22;spawns and supervises&#x22;| overlay
  desktop --> config
  config -->|&#x22;load + validated reload&#x22;| agent
  cli -->|&#x22;bypasses IPC&#x22;| direct"
/>

### Runtime and I/O boundaries [#runtime-and-io-boundaries]

The agent is authoritative for HID++ and native input capture. The desktop only
opens UVC cameras directly; both the desktop and CLI can populate the verified
asset cache.

<Mermaid
  chart="flowchart LR
  subgraph Callers[&#x22;Callers&#x22;]
    direction TB
    agent[&#x22;openlogi-agent&#x22;]
    agentCore[&#x22;openlogi-agent-core<br/>orchestration + action dispatch&#x22;]
    desktop[&#x22;openlogi-desktop&#x22;]
    cli[&#x22;openlogi CLI&#x22;]
    agent --> agentCore
  end

  subgraph Libraries[&#x22;I/O libraries&#x22;]
    direction TB
    hid[&#x22;openlogi-hid<br/>discovery, channels, writes&#x22;]
    hidpp[&#x22;openlogi-hidpp<br/>HID++ protocol&#x22;]
    hook[&#x22;openlogi-hook<br/>native input capture&#x22;]
    inject[&#x22;openlogi-inject<br/>host action synthesis&#x22;]
    camera[&#x22;openlogi-camera<br/>UVC discovery + controls&#x22;]
    assets[&#x22;openlogi-assets<br/>registry + verified cache&#x22;]
    hid --> hidpp
  end

  agentCore --> hid
  agentCore --> hook
  agentCore --> inject
  desktop --> camera
  desktop --> assets
  cli --> hid
  cli --> camera
  cli --> assets

  subgraph Boundaries[&#x22;External boundaries&#x22;]
    direction TB
    devices[&#x22;Logitech HID++ devices<br/>Bolt / Unifying / USB / Bluetooth&#x22;]
    webcams[&#x22;UVC webcams&#x22;]
    os[&#x22;macOS / Linux / Windows APIs&#x22;]
    mirrors[&#x22;assets.openlogi.org<br/>Cloudflare Pages / jsDelivr&#x22;]
  end

  hid --> devices
  camera --> webcams
  hook --> os
  inject --> os
  assets --> mirrors"
/>

## Crates [#crates]

| Crate                       | Role                                                                                                      |
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
| `openlogi-core`             | Leaf-level types, TOML config, paths, device models, and the button / action catalog                      |
| `openlogi-hidpp-derive`     | Private proc macros that generate HID++ feature boilerplate                                               |
| `openlogi-hidpp`            | Workspace hard fork of the HID++ protocol crate (lib name `hidpp`): channels, features, receivers         |
| `openlogi-hid`              | Device discovery, HID++ reads / writes, control capture, raw-HID lights                                   |
| `openlogi-hook`             | OS input hook: macOS CGEventTap, Linux evdev/uinput, Windows `WH_MOUSE_LL`                                |
| `openlogi-inject`           | OS event synthesis: CGEvent, uinput / MPRIS, `SendInput`                                                  |
| `openlogi-camera`           | UVC webcam discovery, capture, and image controls                                                         |
| `openlogi-permissions`      | Permission status and remediation for Accessibility, input, camera, `uinput`, and `hidraw`                |
| `openlogi-assets`           | Device-render registry schema and cached mirror fetches                                                   |
| `openlogi-ipc`              | The tarpc service contract and bincode transport over Unix sockets or Windows named pipes                 |
| `openlogi-agent-core`       | Headless orchestration, capture planning, hardware operations, action dispatch, and Actions Ring sessions |
| `openlogi-ui`               | Shared GPUI presentation assets, ring geometry, colors, and locales                                       |
| `openlogi-cli` / `openlogi` | CLI implementation library and its thin binary wrapper                                                    |
| `openlogi-agent`            | The real agent and `openlogi-agent-mock` binary targets                                                   |
| `openlogi-desktop`          | GPUI settings application and agent IPC client                                                            |
| `openlogi-overlay`          | Separate GPUI Actions Ring renderer and agent IPC client                                                  |
| `xtask`                     | CI, packaging, bundling, and release tooling                                                              |

## How a button press becomes an action [#how-a-button-press-becomes-an-action]

1. The control is captured: either by the OS hook (middle / back / forward) or
   diverted over HID++ `0x1b04` (gesture button, haptic panel, mode-shift,
   keyboard F-row) by a per-device capture session the agent rebuilds whenever
   bindings change.
2. The agent resolves the binding: the frontmost app's overlay first, then the
   device's global map.
3. The agent dispatches it: host actions go through `openlogi-inject`; device
   actions use an agent-owned `openlogi-hid` channel.
4. `ShowActionsRing` is different: the agent snapshots a presentation for the
   overlay, accepts hover / activate / cancel RPCs, validates the session and
   slot, then executes the selected action itself.

Configuration is a plain TOML file; there is no cloud or account. The desktop
loads and atomically writes the file, then requests `reload_config`. The agent
independently loads it at startup and validates it again on reload before
replacing live state. Values that live in volatile device RAM (DPI, SmartShift,
Fn-lock, wheel resolution) are re-applied after reconnects and system wake.

## Developing without hardware [#developing-without-hardware]

`openlogi-agent-mock` serves the real IPC contract from a scripted in-memory
inventory without opening HID devices or installing input hooks, so the desktop
and overlay can be developed with no Logitech device attached. See
[DEVELOPMENT.md](https://github.com/AprilNEA/OpenLogi/blob/master/docs/DEVELOPMENT.md).


# Contributing (/docs/project/contributing)



OpenLogi is experimental and testing help is especially valuable; support is
limited by the devices contributors can test against. Device reports are as
useful as patches: the repository has an issue template for them.

* **Source:** [github.com/AprilNEA/OpenLogi](https://github.com/AprilNEA/OpenLogi)
* **Build from source:**
  [DEVELOPMENT.md](https://github.com/AprilNEA/OpenLogi/blob/master/docs/DEVELOPMENT.md)
  — including `openlogi-agent-mock`, which lets you work on the GUI with no
  hardware attached
* **Translations:** [Crowdin](https://crowdin.com/project/openlogi)

## Acknowledgments [#acknowledgments]

* **Windows port** by [@davidbudnick](https://github.com/davidbudnick) — the
  input hook, MSI in-app updates, tray and settings parity
* **Linux port** by [@cserby](https://github.com/cserby) — the evdev/uinput
  hook, D-Bus actions, `.deb` / `.rpm` packaging
* [`hidpp`](https://crates.io/crates/hidpp) by [@lus](https://github.com/lus) —
  vendored as `openlogi-hidpp`
* [Solaar](https://github.com/pwr-Solaar/Solaar) by
  [@pwr](https://github.com/pwr) — the most complete open-source HID++
  implementation, and OpenLogi's protocol reference
* [Mouser](https://github.com/TomBadash/Mouser) by
  [@TomBadash](https://github.com/TomBadash) — prior art for a local,
  account-free Options+ replacement

## License [#license]

Dual-licensed under Apache-2.0 or MIT, at your option. The vendored
`openlogi-hidpp` crate is 0BSD. The OpenLogi name, logo, and app icon are **not**
covered by those licenses; see
[`design/LICENSE`](https://github.com/AprilNEA/OpenLogi/blob/master/design/LICENSE).


# Roadmap (/docs/project/roadmap)



<Callout type="warning">
  OpenLogi is under active development and not yet stable; features and config
  may still change.
</Callout>

| Capability                                                                     | State                                                              |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
| Discover Bolt receivers + list paired devices (CLI + GUI)                      | ✅                                                                  |
| Unifying receivers (older protocol, replaced by Bolt)                          | ✅                                                                  |
| Lightspeed receivers (G-series, including the nano dongles)                    | ✅                                                                  |
| Bluetooth-direct / wired devices (no receiver)                                 | ✅                                                                  |
| Battery percentage / charge state (`0x1004`, `0x1000`, `0x1001`)               | ✅ (online devices)                                                 |
| Interactive GUI: carousel, mouse diagram, action picker                        | ✅ macOS + Linux + Windows                                          |
| Button remapping via the OS input hook                                         | ✅ macOS + Linux + Windows                                          |
| 44-action catalog + custom shortcuts, app launchers, power-user actions        | ✅ macOS + Linux + Windows¹                                         |
| Actions Ring (eight-slot radial launcher, per-app layouts)                     | ✅ macOS + Linux + Windows                                          |
| DPI control + presets + Cycle / Set-preset actions (HID++ `0x2201`)            | ✅                                                                  |
| SmartShift wheel: mode + sensitivity + permanent ratchet (`0x2111` / `0x2110`) | ✅                                                                  |
| Per-device scroll inversion + wheel resolution (HID++ `0x2121`)                | ✅ (supported devices)                                              |
| Keyboard F-row remapping + Fn-lock (`0x1b04`, `0x40a2` / `0x40a3`)             | ✅ (supported keyboards)                                            |
| Function-key remapper via the OS keyboard hook                                 | ✅ macOS + Windows; Linux key capture pending                       |
| Keyboard host switching that drags paired mice along (`0x1814` / `0x1815`)     | ✅ (supported devices)                                              |
| Static RGB keyboard lighting (HID++ `0x8070` / `0x8080`)                       | ✅ (supported devices)                                              |
| MX Keys backlight control (HID++ `0x1982`)                                     | ✅ CLI                                                              |
| Standalone Litra lights (power, brightness, temperature)                       | ✅; auto-on-with-camera on macOS                                    |
| Logitech webcams: live preview + UVC image controls                            | ✅ macOS + Linux + Windows                                          |
| Per-application profile overlays (auto-switch on app focus)                    | ✅ macOS + Windows, ✅ Linux (X11, GNOME, wlroots)                   |
| Settings window: launch-at-login, updates, permissions, language, appearance   | ✅ macOS + Linux + Windows                                          |
| Agent status icon                                                              | ✅ macOS menu bar + Windows tray; N/A on Linux                      |
| Interface localization (20 languages)                                          | ✅                                                                  |
| Linux packaging: udev rules, systemd unit, `.deb` / `.rpm` / `.pkg.tar.zst`    | ✅ Linux                                                            |
| Gesture bindings, per direction and per button + live capture                  | ✅ (device capability dependent)                                    |
| Bolt pairing from the GUI                                                      | ✅; Unifying / Lightspeed pairing not implemented                   |
| Windows (agent, GUI, event hook, installer)                                    | ✅ validated on Windows 11 hardware; newer port with ongoing polish |

¹ Media key actions use D-Bus MPRIS on Linux; a handful of macOS-specific
actions (e.g. Launchpad) have no universal Linux equivalent and are no-ops.
Windows maps platform actions to native equivalents where available.

Help improve the interface translations on
[Crowdin](https://crowdin.com/project/openlogi).

*Mirrored from the [project README](https://github.com/AprilNEA/OpenLogi).*


# Actions Ring (/docs/features/actions-ring)



The **Actions Ring** is a radial launcher: press its trigger and eight actions
fan out around the pointer, ready to pick. It is host-side UI — the agent renders
the overlay and executes the chosen action — so any device with a spare button
can open one, and the layout can change per application.

## Opening and dismissing it [#opening-and-dismissing-it]

The trigger is the &#x2A;*`ShowActionsRing`** action. On an MX Master 4 the **Haptic
Sense Panel** — the touch-sensitive thumb rest — carries it by default. To open
the ring from a different control, bind `ShowActionsRing` to that button in
[`config.toml`](/docs/configurations): the GUI's action picker doesn't
list it yet, so it is a config-file edit today.

The ring opens at the current cursor position, on the display the cursor is on.
Click a slot — or hover it and release the trigger — to run its action. To leave
without picking anything, press the trigger again, hit the &#x2A;*×** in the middle,
or (on macOS) click anywhere outside the ring. An untouched ring expires on its
own after a few seconds.

## Slots [#slots]

The eight positions are named clockwise from twelve o'clock: `Top`, `TopRight`,
`Right`, `BottomRight`, `Bottom`, `BottomLeft`, `Left`, `TopLeft`. Out of the
box they carry Cut, Copy, Paste, Browser Forward, Play/Pause, Browser Back,
Undo, and Redo.

Each populated slot holds:

* **An action** — any binding action except `ShowActionsRing` itself (a ring
  cannot open another ring) and `None` (an empty slot is simply an absent
  entry, so clearing a slot removes it).
* **An optional icon** — pick from the built-in gallery, or leave it to inherit
  the action's own icon.
* **An optional label** — free text shown on hover. Custom labels render exactly
  as written and are never localized, which is what makes several
  `RunShellCommand` slots distinguishable instead of all reading "Run Command".

## From the GUI [#from-the-gui]

Select the device and open its **Actions Ring** tab: it appears for pointer
devices with rebindable buttons, and for any device with a haptic panel. The
editor shows the ring as it will appear, with a row of controls:

* **Enabled** — whether `ShowActionsRing` opens this device's ring at all.
* **Haptic feedback** — play device feedback when hovering and activating a slot,
  on hardware that supports it (HID++ `0x19b0` hapticFeedback). Off leaves the
  ring silent.
* **Slot editor** — click a slot to choose its action, its icon (**Use action
  icon** keeps the derived one), and a custom label; **Clear slot** empties it.

## Per-application rings [#per-application-rings]

A per-app ring is a **complete layout, not a sparse overlay**: when the frontmost
app matches, that layout replaces the default one wholesale, so every slot you
want has to be listed. This differs from
[per-app button bindings](/docs/features/profiles), where unlisted buttons fall
through to the global map.

## In the config [#in-the-config]

The ring lives under `[devices.<key>.action_ring]` in
[`config.toml`](/docs/configurations):

```toml
[devices."receiver:aabbccdd:slot:1".action_ring]
enabled = true
haptics = true

# Omit `icon` to use the action's own icon; omit `label` to use its name;
# omit a slot entirely to leave that position empty.
[devices."receiver:aabbccdd:slot:1".action_ring.default.slots]
Top = { action = "Cut" }
TopRight = { action = "Copy", icon = "Copy" }
Right = { action = "Paste", label = "Paste It" }
BottomRight = { action = "BrowserForward" }
Bottom = { action = "PlayPause" }
BottomLeft = { action = "BrowserBack" }
Left = { action = "Undo" }
TopLeft = { action = { OpenApplication = { path = "/Applications/Safari.app", display_name = "Safari" } } }

# A per-app ring is a complete layout, not an overlay.
[devices."receiver:aabbccdd:slot:1".action_ring.per_app."com.microsoft.VSCode".slots]
Top = { action = "Cut" }
TopRight = { action = "Copy" }
Right = { action = "Redo" }
BottomRight = { action = "NextTab" }
Bottom = { action = "ShowDesktop" }
BottomLeft = { action = "PrevTab" }
Left = { action = "Undo" }
TopLeft = { action = "Paste" }
```

<Callout type="info">
  App identifiers follow the same rules as per-app bindings: a bundle id on
  macOS, the `WM_CLASS` class (X11 / GNOME) or xdg-shell `app_id` (wlroots) on
  Linux, and a lower-cased executable path — or an `exe:<name>.exe` fallback —
  on Windows. See [Per-app profiles](/docs/features/profiles).
</Callout>

**Status:** the ring overlay is rendered by the agent's GPUI helper on macOS,
Linux, and Windows; dismiss-on-outside-click is macOS-only today. Haptic
feedback needs a device that reports HID++ `0x19b0`, such as the MX Master 4;
every other device opens the ring silently. See
[`0x19b0` hapticFeedback](/hidpp/features/x19b0-haptic-feedback) for the
protocol detail.


# Features (/docs/features)



How to drive each OpenLogi feature.

## Buttons & automation [#buttons--automation]

<Cards>
  <Card title="Remap buttons" description="Browse the action catalog, configure gestures, and learn how buttons are captured." href="/docs/features/mouse/remap-buttons" />

  <Card title="Actions Ring" description="Configure the eight-slot radial launcher." href="/docs/features/actions-ring" />

  <Card title="Per-app profiles" description="Switch button bindings automatically with the frontmost app." href="/docs/features/profiles" />
</Cards>

## Mouse [#mouse]

<Cards>
  <Card title="DPI presets" description="Set pointer resolution and cycle through an ordered preset list." href="/docs/features/mouse/dpi" />

  <Card title="Scrolling" description="Configure wheel inversion, resolution, and the thumb wheel." href="/docs/features/mouse/scrolling" />

  <Card title="SmartShift" description="Switch the main wheel between ratchet and free-spin modes." href="/docs/features/mouse/smartshift" />
</Cards>

## Other devices [#other-devices]

<Cards>
  <Card title="Keyboards" description="Remap the F-row and control Fn-lock and host switching." href="/docs/features/keyboard" />

  <Card title="Keyboard lighting" description="Control keyboard RGB and backlighting." href="/docs/features/keyboard/lighting" />

  <Card title="Litra lights" description="Control power, brightness, colour temperature, and camera-linked auto-power." href="/docs/features/lights" />

  <Card title="Webcams" description="Preview a Logitech webcam and adjust its UVC image controls." href="/docs/features/webcams" />
</Cards>

## Reference [#reference]

<Cards>
  <Card title="Configuration" description="Read the complete config.toml reference." href="/docs/configurations" />
</Cards>


# Litra lights (/docs/features/lights)



OpenLogi drives standalone Litra lights over their raw-HID protocol and shows
them in the same device carousel as your mice and keyboards.

Litra **Glow** (`046d:c900`) and Litra **Beam** (`046d:c901`) are recognised by
their raw-HID interface. Selecting one opens a **Lighting** tab with:

* **Power** — on / off.
* **Brightness** — `0`–`100`%, mapped onto the light's native range (20–250
  lumens on a Litra).
* **Colour temperature** — 2700–6500 K in 100 K steps.
* **Auto-on with camera** — turn the light on while any [camera](/docs/features/webcams)
  is in use and off when camera use stops (macOS). The manual power choice and
  the other settings stay independent of this policy.

```toml
[devices."raw:046d:c900:ff43:0202:serial:YOUR-SERIAL".light]
enabled = true
auto_camera = true
brightness_percent = 65
temperature_kelvin = 4600
```

<Callout type="info">
  A light's config key is its raw-HID route plus a **serial number**. If the HID
  backend exposes only a transient OS-node identity for your light, OpenLogi
  won't persist settings for it; `openlogi light list` prints the HID tuple and
  identity it sees, which is the fastest way to tell.
</Callout>

**Status:** Litra control works wherever raw HID does — macOS, Linux, and
Windows — with the camera-linked auto-power policy on macOS.


# Per-app profiles (/docs/features/profiles)



Per-app profiles let OpenLogi swap a device's button bindings automatically based
on the frontmost app. A background watcher reads the foreground app's identifier
once per second; when it changes, the overlay you authored for that app is
layered on top of the device's global
[`bindings`](/docs/features/mouse/remap-buttons), with per-app entries winning and any
unlisted button falling through to the global map. The
[Actions Ring](/docs/features/actions-ring) can switch per app too, as a
complete layout rather than an overlay. DPI presets, scrolling, and lighting
stay device-wide.

## App identifiers [#app-identifiers]

The identifier is whatever the platform reports for the frontmost window, and it
must match a config key **exactly**; there is no wildcard or pattern matching.

| Platform                            | Identifier                                       | Example                |
| ----------------------------------- | ------------------------------------------------ | ---------------------- |
| macOS                               | Bundle id                                        | `com.microsoft.VSCode` |
| Linux (X11 / XWayland, GNOME Shell) | `WM_CLASS` class                                 | `Code`                 |
| Linux (wlroots compositors)         | xdg-shell `app_id`                               | `code`                 |
| Windows                             | Lower-cased executable path, or `exe:<name>.exe` | `exe:sharex.exe`       |

<Callout type="warning">
  The Linux backends do not share a namespace: a profile authored under a
  wlroots compositor (`app_id`) will not match under GNOME or X11 (`WM_CLASS`),
  and vice versa. Author the form your session actually reports.
</Callout>

On Windows an exact path entry wins when both forms exist, so `exe:` is the
stable fallback for Store apps and self-updating installs whose path moves.

## From the GUI [#from-the-gui]

There is no per-app editor for button bindings yet; the action picker edits the
device's global `bindings`, and overlays are hand-authored in
[`config.toml`](/docs/configurations). Two things are surfaced:

* **Active profile** — a read-only row on the configuration card showing the
  frontmost app's identifier as the active profile, or a **Default profile**
  label when no app is frontmost. It is a status display, not an editor.
* **Auto-switch** — fully automatic, with no control. When the watcher reports a
  new identifier, OpenLogi rebuilds the in-memory binding map and hands it to the
  event hook, so the next button press picks up the new app's overlay.

## Authoring overlays [#authoring-overlays]

Add a table under `[devices.<key>.per_app_bindings."<app-id>"]` holding a partial
button → action map. Buttons you list override the global binding while that app
is frontmost; buttons you omit keep their global value.

```toml
[devices."receiver:aabbccdd:slot:1".bindings]
Back = "BrowserBack"
Forward = "BrowserForward"

# While VS Code is frontmost, Back becomes Undo and Forward becomes Redo.
[devices."receiver:aabbccdd:slot:1".per_app_bindings."com.microsoft.VSCode"]
Back = "Undo"
Forward = "Redo"
```

Values are [action names](/docs/configurations#actions) written verbatim.
The overlay only adds or overrides keys; it never removes one, so to make a
button do nothing in a given app set it to `None` rather than omitting it. A
per-app overlay always maps a button to a single action; per-direction gesture
maps stay device-wide.

<Callout type="info">
  Switching is polled at roughly **1 second**, so expect up to a second of
  latency after you change apps.
</Callout>

**Status:** macOS, Linux, and Windows. On Linux the identifier depends on the
session backend (X11 / XWayland, GNOME Shell, or a wlroots compositor); a
session with none of those available reports no app and everything falls back to
the **Default profile**. Authored in
[`config.toml`](/docs/configurations); driven by the OS
foreground-app API, not a HID++ feature.


# Webcams (/docs/features/webcams)



OpenLogi drives Logitech webcams over USB Video Class (UVC) and shows them in
the same device carousel as your mice and keyboards.

Any Logitech USB camera (Brio, StreamCam, C920, C922, C930e, C270, …) is a
standard UVC device, so detection keys off the Logitech vendor id rather than a
model table; plug one in and it appears.

A camera's detail screen leads with a **Camera** tab holding a live preview and
the image controls:

* **Lens** — zoom, focus, exposure, each with an **Auto** toggle where the
  camera has one.
* **Image** — brightness, contrast, saturation, sharpness, white balance, tint.
* **Profiles** — Default, Streaming, and Video call presets, plus profiles you
  save yourself; the last applied one is highlighted when you reopen the tab.

These are **device-level** UVC controls: a change lands in the camera's own
registers, so Zoom, Meet, OBS, and everything else see it too, not just the
preview.

Saved values live under the camera's device entry:

```toml
[devices."<camera-key>".camera_controls]
brightness = 128
contrast = 32

[devices."<camera-key>".camera_profiles."Streaming"]
brightness = 140
saturation = 160
```

<Callout type="warning">
  Live preview and snapshots need **Camera** permission. On macOS, grant it in
  System Settings → Privacy & Security → Camera; reading and writing UVC
  controls does not need it.
</Callout>

Litra lights can follow camera activity automatically; see
[Litra lights](/docs/features/lights).

**Status:** Camera capture and controls have full backends on macOS
(AVFoundation + IOKit UVC), Windows (Media Foundation + DirectShow), and Linux
(V4L2 via `uvcvideo`).


# Keyboards (/docs/features/keyboard)



OpenLogi treats keyboards as first-class devices: it remaps the F-row, controls
Fn-lock and backlighting over HID++, and can drag paired mice along when the
keyboard switches hosts.

There are **two** ways to remap keys, and they are independent:

|           | HID++ F-row keys                 | Function-key remapper                |
| --------- | -------------------------------- | ------------------------------------ |
| Scope     | One Logitech keyboard            | Any keyboard attached to the machine |
| Path      | HID++ `0x1b04` control diversion | OS keyboard hook                     |
| Config    | `[devices.<key>.bindings]`       | `[keyboard.bindings]`                |
| Edited in | `config.toml`                    | the GUI's **Keys** tab               |
| Platforms | macOS, Linux, Windows            | macOS, Windows                       |

## HID++ F-row keys [#hid-f-row-keys]

A Logitech keyboard's media / shortcut row can be diverted key by key over
HID++, so a press dispatches an OpenLogi action instead of its printed function.
OpenLogi models nine Signature-series controls:

| Button name        | Signature F-row position | `0x1b04` control ID |
| ------------------ | ------------------------ | ------------------- |
| `KeySearch`        | F4                       | `0x00d4`            |
| `KeyDictation`     | F5                       | `0x0103`            |
| `KeyEmoji`         | F6                       | `0x0108`            |
| `KeyScreenCapture` | F7                       | `0x010a`            |
| `KeyMicMute`       | F8                       | `0x011c`            |
| `KeyPlayPause`     | F9                       | `0x00e5`            |
| `KeyMute`          | F10                      | `0x00e7`            |
| `KeyVolumeDown`    | F11                      | `0x00e8`            |
| `KeyVolumeUp`      | F12                      | `0x00e9`            |

Only keys that carry a binding are diverted; an unbound key keeps its native
firmware function and OpenLogi never touches it. A key the device doesn't expose
is skipped with a log line, so a partially-supported keyboard degrades key by
key instead of failing as a whole.

```toml
[devices."receiver:aabbccdd:slot:2".bindings]
KeySearch = "MissionControl"
KeyScreenCapture = "CaptureRegion"
KeyMicMute = { CustomShortcut = "Cmd+Shift+M" }
```

<Callout type="info">
  Diversion works on the key's *control* — the printed media function — so it
  fires while Fn-lock is **off**, or via Fn+key while it is on. The plain
  F1–F12 codes of an Fn-locked row travel the ordinary HID keyboard interface
  and never reach `0x1b04`; remap those with the function-key remapper below.
</Callout>

## Function-key remapper [#function-key-remapper]

The **Keys** tab remaps function keys on any keyboard through the OS hook: the
keyboard photo sits beside a row of callout bubbles, and clicking a key selects
it and slides in the action panel (the same catalog as the mouse picker, plus
the Power User section).

Bindings are global rather than per device, and live under `[keyboard.bindings]`
in [`config.toml`](/docs/configurations#keyboard). Triggers are written
`[modifier+]…key`, with modifiers `shift`, `control` (`ctrl`), `option`
(`alt`), `command` (`cmd`) and keys `esc`, `f1`–`f19`:

```toml
[keyboard.bindings]
f1 = "MissionControl"
"shift+f2" = "ShowDesktop"
"cmd+f5" = { CustomShortcut = "Cmd+Shift+P" }
```

## Fn-lock [#fn-lock]

`fn_lock` flips the F-row between its printed media functions and plain
F1–F12, over HID++ fn-inversion (`0x40a2`, or `0x40a3` on multi-host keyboards):

```toml
[devices."receiver:aabbccdd:slot:2"]
fn_lock = true    # F-row sends F1–F12 without holding Fn
```

`true` locks the row to F1–F12, `false` restores the printed functions, and
leaving the key out means "don't touch what the keyboard is already doing". The
state lives in device RAM per host, so the agent re-applies your choice each
time the keyboard reconnects. There is no GUI control for it yet.

## Host switching [#host-switching]

A multi-host (Easy-Switch) keyboard can pull mice along when it changes host.
List the target devices' [physical keys](/docs/configurations#device-keys)
on the **keyboard's** entry:

```toml
[devices."receiver:aabbccdd:slot:2"]
host_switch_targets = ["receiver:aabbccdd:slot:1"]
```

Pressing one of the keyboard's host keys switches every listed target first,
then lets the keyboard leave. Both devices must already be paired on
corresponding channels, and every target has to expose the HID++ host-switch
features (`0x1814` changeHost / `0x1815` hostsInfo). Because the relationship is
keyboard-initiated, configure it on each computer the switch may start from.

## Lighting [#lighting]

Keyboard backlight and RGB are covered in [Keyboard lighting](/docs/features/keyboard/lighting):
static per-key colour over `0x8070` / `0x8081`, and monochrome backlight levels
over `0x1982`.

**Status:** F-row diversion, Fn-lock, and host switching ride HID++ and work on
macOS, Linux, and Windows. The function-key remapper needs OS keyboard events,
which the hook delivers on macOS and Windows; Linux key capture is not wired up
yet.


# Keyboard lighting (/docs/features/keyboard/lighting)



OpenLogi drives two different keyboard lighting systems: the **RGB** engine on
G-series boards, as one static colour rather than per-key effects, and the
white, level-adjustable **backlight** on the MX Keys line. For Litra lights, see
[Litra lights](/docs/features/lights).

## RGB keyboards [#rgb-keyboards]

Select the keyboard in the device carousel and open its **Lighting** tab:

* **Colour swatches** — a small palette of preset accent colours.
* **On / off** — toggle whether the static colour is applied.
* **Brightness** — a `0`–`100` slider. The level is pushed to the keyboard when
  you release the slider, so dragging doesn't stream a burst of updates.

Under the hood OpenLogi prefers the keyboard's effect engine — HID++ `0x8070`
colorLedEffects, writing the *fixed* single-colour effect — because that
replaces a running onboard profile, which a per-key `0x8080` write can't
override on G-series firmware. The colour is applied to RAM only, so nothing is
burned into flash; the agent re-applies it when the keyboard reconnects.

Your choice is saved per device under `[devices.<key>.lighting]` in the
[config](/docs/configurations) (`enabled`, `color`, `brightness`).

## MX Keys backlight [#mx-keys-backlight]

Keyboards with the white proximity backlight expose HID++ `0x1982` backlight
instead of an RGB engine. It is driven from the CLI:

```sh
openlogi backlight            # current state (mode, level, status)
openlogi backlight off        # dark regardless of ambient light or hand proximity
openlogi backlight on         # restore the keyboard's stored mode and level
```

Unlike the RGB path, this writes the keyboard's **non-volatile** configuration,
so it survives reconnects, host switches, and power cycles with nothing
re-applying it.

<Callout type="info">
  RGB lighting targets keyboards, not mice or receivers. As always, quit Logi
  Options+ first so OpenLogi can own the device.
</Callout>

**Status:** macOS, Linux, and Windows. Protocol detail is on
[`0x8070` colorLedEffects](/hidpp/features/x8070-color-led-effects),
[`0x8081` perKeyLighting2](/hidpp/features/x8081-per-key-lighting), and
[`0x1982` backlight](/hidpp/features/x1982-backlight).


# DPI presets (/docs/features/mouse/dpi)





Set a Logitech mouse's pointer resolution and keep an ordered list of DPI
presets, backed by the HID++ **`0x2201` adjustableDpi** feature. The slider
snaps to the values your device actually supports, and the presets can be cycled
from a hardware button so you never have to open the window to change DPI.

## From the GUI [#from-the-gui]

Select the mouse in the device carousel and open its **Pointer** tab; the DPI
card holds:

* **DPI slider** — its range is the device's own supported DPI list (`min`–`max`,
  with the step set to the smallest gap between adjacent stops). Dragging only
  updates the on-screen value; the new DPI is written to the mouse when you
  **release** the slider, and the value snaps to the nearest stop the device
  reports. A single-DPI mouse shows a fixed value instead of a slider.
* **Range label** — reads `{min}–{max} · step {n}` once the device's capabilities
  are known, or a status line while OpenLogi is offline, still reading, or if the
  read failed.
* **Preset chips*&#x2A; — each preset is a clickable chip. Clicking one snaps its value
  to the device grid, applies it, and writes it to the mouse. The chip matching
  the current DPI is highlighted as active. The &#x2A;*×*&#x2A; on a chip removes that
  preset, and **+ Add** appends the current DPI to the list.

<Figure caption="The MX Master 4 Pointer tab with DPI set to 2000 and an empty preset list.">
    <img alt="OpenLogi's Pointer tab for an MX Master 4, showing the DPI slider set to 2000 and an empty preset list" src="__img0" />
</Figure>

Capabilities are discovered lazily the first time the DPI panel renders. If that
discovery fails, the panel shows a clickable **retry** line that re-arms it for
the selected device.

The DPI you commit lives in the mouse's RAM and is lost on a power cycle, so
OpenLogi also records it as `dpi` under the device's config block and the agent
re-applies it whenever the device reconnects.

<Callout type="info">
  There is no reorder control. Presets are append-only (each **+ Add** drops the
  current DPI at the end) and removed by index, and that `Vec` order **is** the
  cycle order. To change the order, remove and re-add, or edit the
  [config](/docs/configurations) directly. Duplicate values are allowed on
  purpose, so the same DPI can sit at more than one cycle position.
</Callout>

## Cycling from a button [#cycling-from-a-button]

Two actions drive the preset list from a hardware button:

* **`CycleDpiPresets`** — steps to the next preset, wrapping from the last back to
  the first. This is the default action for the mode-shift (`DpiToggle`) button,
  and it is the only DPI-cycle action offered in the GUI's action picker.
* **`SetDpiPreset`** — jumps straight to a preset index. It carries that index as
  data, so it is **not** in the picker; bind it by hand-editing
  [`config.toml`](/docs/configurations).

An empty preset list makes both actions a no-op. Editing the presets or switching
devices resets the cycle position to the first entry, since the list changed.

## In the config [#in-the-config]

The list persists per device as `dpi_presets` under
`[devices.<key>]` in the [config](/docs/configurations):

```toml
[devices."receiver:aabbccdd:slot:1"]
dpi_presets = [800, 1600, 3200]
dpi = 1600                        # last committed value, re-applied on reconnect
```

It is an ordered array of integers, omitted entirely while empty. See
[Configuration](/docs/configurations) for the full action vocabulary and
[Per-app profiles](/docs/features/profiles) for swapping presets per application.

<Callout type="info">
  Quit Logi Options+ first so OpenLogi can own the device. DPI writes target
  sensor 0; multi-sensor mice aren't addressed by the GUI. The panel drives
  `0x2201` adjustableDpi only; a mouse that exposes `0x2202`
  extendedAdjustableDpi instead is enumerated, and the HID++ crate has a typed
  wrapper for it, but the DPI panel doesn't use that path yet.
</Callout>

**Status:** macOS, Linux, and Windows; the device-side DPI write works wherever
HID++ does, and firing it from a button rides the OS hook (CGEventTap on macOS,
evdev on Linux, `WH_MOUSE_LL` on Windows). See
[`0x2201` adjustableDpi](/hidpp/features/x2201-adjustable-dpi) for the protocol
detail and the [roadmap](/docs/project/roadmap) for what's wired up.


# Remap buttons (/docs/features/mouse/remap-buttons)



Reassign your Logitech mouse's reprogrammable buttons from the GUI's interactive
diagram: click a button hotspot — or its label card — and pick from **44
built-in actions**, from `Copy`/`Paste` to `MissionControl`, media keys, real
mouse button 4/5 (`MouseBack`/`MouseForward`), and `CycleDpiPresets`. Left and
right clicks always pass through; everything else
can be remapped, scoped per app, and saved to your TOML
[config](/docs/configurations).

## From the GUI [#from-the-gui]

Select the mouse in the device carousel and open its **Buttons** tab. It shows
an interactive diagram of the mouse — the device's own artwork when OpenLogi has
it, or a synthetic silhouette otherwise — with a **hotspot dot** over each
reprogrammable button and a leader line out to a **label card** on the side.

* **Hotspot dot or label card** — two entry points into the same flow. Hovering
  highlights the button; the label card reads the current binding, or **Unbound**
  if there's none. Clicking either opens a small popover.
* **Action picker** — a scrollable list of the catalog grouped by category
  (Mouse, Editing, Browser, Navigation, System, Media, DPI, Scroll). The current
  binding is checked. Clicking a row commits it and closes the popover. Picking
  **Do Nothing** captures the input but does nothing.
* **Custom bindings** — the picker also authors a **Custom shortcut** (type a
  chord such as `Cmd+Shift+P`) and **Open application** (an app, folder, path,
  or URL). A **Power User** section adds `TypeText`, `RunAppleScript`,
  `RunShellCommand`, and multi-step `Workflow` bindings.
* **Gestures** — a gesture-capable button's picker leads with a pinned
  **Gestures** row. Clicking it promotes that button into gesture mode: its
  single action becomes the **Click** arm and the four swipe arms seed from the
  defaults. Reopening the popover then lands on the gesture menu: a
  plus-shaped navigator listing **Up**, **Down**, **Left**, **Right**, and
  **Click**, each opening the full action catalog, with a footer row that demotes
  the button back to a single action.

Any number of buttons can be in gesture mode at once: the middle, back, and
forward buttons as readily as the dedicated Gesture Button or the MX Master 4's
Haptic Sense Panel. This is something Options+ doesn't offer: it pins the
gesture role to the dedicated thumb pad.

## How buttons are captured [#how-buttons-are-captured]

Capture is split across two paths, both ending in the same action dispatcher:

* The **OS hook** — a CGEventTap on macOS, evdev/uinput on Linux, a
  `WH_MOUSE_LL` hook on Windows — owns the side buttons: **Back**, **Forward**,
  and the **middle** click. On macOS it needs Accessibility permission; on
  Linux, the udev rules from the package.
* HID++ **`0x1b04` reprogControlsV4** diverts the **Gesture Button** and the
  **Haptic Sense Panel** (read as raw swipe travel), the **DpiToggle*&#x2A;
  (mode-shift) button, and any keyboard F-row key you bind. The thumb wheel
  diverts over **`0x2150` thumbwheel** when its
  [sensitivity](/docs/features/mouse/scrolling) leaves the default, or when its click
  or a rotation direction is rebound.

A gesture is told apart from a click by hold and travel: held past **160 ms**
with at least **50 raw-XY units** of travel on the dominant axis commits a
directional swipe; a quick tap fires the **Click** arm on release.

<Callout type="info">
  Capture runs per device from a plan the agent rebuilds whenever your bindings
  change, so only the controls you actually bound are diverted; everything else
  keeps its native firmware behaviour.
</Callout>

## In the config [#in-the-config]

Bindings persist per device as plain TOML keyed by the device's
[physical key](/docs/configurations#device-keys). Action names serialize
**verbatim**, so they read the same as the picker:

```toml
[devices."receiver:aabbccdd:slot:1".bindings]
Back = "BrowserBack"
Forward = "BrowserForward"
MiddleClick = "MissionControl"
HapticPanel = "ShowActionsRing"

# A gesture-mode button binds per direction; Click is the plain press.
[devices."receiver:aabbccdd:slot:1".bindings.GestureButton]
Left = "PrevTab"
Right = "NextTab"
Click = "PlayPause"
```

The same keys can live under `per_app_bindings."<app-id>"` to win only while a
given app is frontmost. See [Configuration](/docs/configurations) for the
full button and action vocabulary, and
[Per-app profiles](/docs/features/profiles) for the overlay rules.

<Callout type="info">
  Quit Logi Options+ first so OpenLogi can own the device. On macOS the event
  tap needs **Accessibility** permission to remap the side buttons; the HID++
  paths (gesture button, haptic panel, DpiToggle, thumb wheel) don't. Bindings
  are saved per device, so with no device selected they're held in memory only
  until one is.
</Callout>

**Status:** macOS, Linux, and Windows. See the
[roadmap](/docs/project/roadmap) for per-platform detail and
[`0x1b04` reprogControls5](/hidpp/features/x1b04-special-keys-mse-buttons)
for the protocol.


# Scrolling (/docs/features/mouse/scrolling)



Scroll behaviour is per device, written straight to the mouse over HID++ and
persisted so it survives a power cycle. For the ratchet / free-spin feel of the
main wheel, see [SmartShift](/docs/features/mouse/smartshift).

## From the GUI [#from-the-gui]

Select the mouse and open its **Pointer** tab; the **Scrolling** card sits beside
DPI:

* **Invert scroll direction** — reverses this mouse's wheel without touching the
  system-wide setting, so a trackpad can keep natural scrolling while the mouse
  scrolls the traditional way. It is applied through the device's native HID++
  wheel inversion, so the card greys out for devices that don't report support
  for it.
* **Wheel resolution** — **Device default** (OpenLogi doesn't change it),
  **Standard** (one scroll report per physical ratchet step), or **High
  resolution** (finer reports between ratchet steps), backed by HID++ `0x2121`
  hiResWheel.

Both are pure configuration: the card writes the value and the agent re-applies
it whenever the device reconnects.

## Thumb wheel [#thumb-wheel]

On MX-line mice the horizontal thumb wheel is its own control:

* `ThumbwheelScrollUp` / `ThumbwheelScrollDown` bind each rotation direction, and
  `Thumbwheel` binds its click. Out of the box, rotating it scrolls horizontally
  (up → right, down → left) and the click is App Exposé.
* **Thumb Wheel Sensitivity** scales that scroll on a `1`–`100` scale, app-wide
  in Settings and per device from the SmartShift panel.

The wheel keeps scrolling natively until you give OpenLogi a reason to take it
over: it is diverted over HID++ `0x2150` only once the sensitivity leaves its
default (`14`) or one of its bindings diverges from the default.

## In the config [#in-the-config]

```toml
[devices."receiver:aabbccdd:slot:1"]
invert_scroll = true
scroll_resolution = "high"        # "low", "high", or omit for the device default
thumbwheel_sensitivity = 30       # per-device override of the app-wide value

[devices."receiver:aabbccdd:slot:1".bindings]
ThumbwheelScrollUp = "HorizontalScrollRight"
ThumbwheelScrollDown = "HorizontalScrollLeft"
```

**Status:** macOS + Linux + Windows, on devices that expose the matching HID++
features. See
[`0x2121` hiResWheel](/hidpp/features/x2121-hires-wheel) and
[`0x2150` thumbwheel](/hidpp/features/x2150-thumbwheel) for the protocol
detail.


# SmartShift (/docs/features/mouse/smartshift)



Switch the scroll wheel between a clicky **ratchet** feel and frictionless
**free-spin**, set how hard you have to flick before it releases, and pin a
**permanent ratchet*&#x2A; that never lets go, backed by the HID++ **`0x2111`
smartShiftEnhanced** feature (the variant on MX Master 3 / 3S / 4 and current
MX-line mice). Everything lives in the GUI's SmartShift panel, and the
mode toggle is also a bindable action.

## From the GUI [#from-the-gui]

Select the mouse in the device carousel; its detail panel has a SmartShift
section with:

* **Wheel mode** — a two-pill segmented control, **Free spin** / **Ratchet**.
  Free spin lets the wheel coast for long-page scrolling; Ratchet gives the
  stepped, tactile clicks. The selected pill is highlighted.
* **Sensitivity threshold** — an `8`–`50` slider (default `16`) that sets how
  fast you have to spin before the wheel auto-releases into free-spin. Higher
  keeps the ratchet engaged longer. The numeric label tracks your drag, but the
  value is pushed to the device only when you release the slider. It's greyed
  out unless you're in ratchet mode with permanent ratchet off. The floor is
  deliberate: below it the ratchet releases at everyday scroll speeds and the
  wheel feels stuck spinning.
* **Permanent ratchet** — an **On / Off** toggle. **On** disables the
  auto-release entirely, so the wheel stays ratcheted no matter how fast you
  spin; **Off** restores your last threshold. It has no meaning under free spin
  and is disabled there.
* **Thumb wheel sensitivity** — a per-device override of the app-wide thumb-wheel
  setting; see [Scrolling](/docs/features/mouse/scrolling).

The panel reads the wheel's current state the first time it's shown. If the
device is asleep or offline you'll see an *offline* message with a line to
click and retry; mice that don't expose the feature show &#x2A;*This device does not
support SmartShift.**

## As an action [#as-an-action]

`ToggleSmartShift` flips the wheel between free-spin and ratchet on a button
press, without opening the panel. Bind it from the GUI's action picker or by
hand in the [config](/docs/configurations), and scope it per app with a
[profile](/docs/features/profiles).

## In the config [#in-the-config]

The mouse keeps SmartShift in volatile memory, so a power cycle would otherwise
forget it. OpenLogi records your choice per device and the agent re-applies it on
every reconnect:

```toml
[devices."receiver:aabbccdd:slot:1".smartshift]
mode = "ratchet"        # or "free"
auto_disengage = 16     # 8–254, or 255 for a permanent ratchet
tunable_torque = 0      # 1–100 where the device supports tunable torque
```

A persisted threshold below the supported floor (including the firmware's `0`
"don't change" sentinel) is healed back to the default on load.

<Callout type="info">
  Quit Logi Options+ first so OpenLogi can own the device. Mice that expose the
  older `0x2110` smartShiftWheel variant are supported through the same panel.
</Callout>

**Status:** macOS, Linux, and Windows. See the
[roadmap](/docs/project/roadmap) for what's wired up. Protocol detail is on
[`0x2111` smartShiftEnhanced](/hidpp/features/x2111-smartshift-enhanced) and
[`0x2110` smartShiftWheel](/hidpp/features/x2110-smartshift).
