Skip to content

feat: onboard profiles (0x8100) support for G-series mice - #459

Open
Stanley5249 wants to merge 11 commits into
AprilNEA:masterfrom
Stanley5249:feat/onboard-profiles
Open

feat: onboard profiles (0x8100) support for G-series mice#459
Stanley5249 wants to merge 11 commits into
AprilNEA:masterfrom
Stanley5249:feat/onboard-profiles

Conversation

@Stanley5249

@Stanley5249 Stanley5249 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for HID++ 2.0 Onboard Profiles (feature 0x8100), the flash profile memory G-series gaming mice use to decide whether they run host software settings or their own stored profiles. Until now 0x8100 was a name-only row in the feature registry, so OpenLogi could neither report which mode a mouse was in nor let the user choose one.

This is a read and mode-control slice: read the memory description, mode, active profile and profile directory; switch between host and onboard mode; and select the active onboard profile. It deliberately does not write flash, so profile editing (functions 6 to 8) is out of scope.

The mode lives in device RAM, so a mouse left in host mode is back in onboard mode after a power cycle. The agent therefore re-applies a configured mode on every reconnect. A device with no configured mode is left in whatever mode it powered on in, and OpenLogi never switches it on the user's behalf.

Scope, and what still needs work

This PR is the wiring: the protocol wrapper, the I/O verbs, the config key, a panel to toggle the mode and pick a profile, and enough receiver support to reach these mice at all. It is not a finished onboard-profiles feature:

  • Profile contents are untouched. Key bindings, DPI stages and report rate stored inside a profile are neither read nor written. Selecting a profile activates whatever the device already has in that slot.
  • The UI is functional, not designed. The Profiles tab is a source toggle and a row of pills. It needs real design work before it is something to ship proudly.
  • Flash writes are out of scope, so OpenLogi cannot create, edit or reset a profile.

Worth reviewing as the foundation, not as the finished surface.

Changes

  • hidpp: new feature/onboard_profiles with getDescription, get/set onboard mode, get/set current profile, memoryRead, and directory parsing over sector 0. Offsets are reverse-engineered against Solaar and libratbag and marked as such; unknown mode and enabled bytes surface as UnsupportedResponse rather than silent fallbacks. The directory read is bounded by profile_count + profile_count_oob, so a device that lists its read-only profiles is not truncated. No flash-write session.
  • device/hid: IPC-facing ProfilesMode / ProfileEntry / OnboardProfilesInfo and the read/apply verbs; apply_profiles_config skips writes the device already matches. Recognises Lightspeed receivers (0xc53f, 0xc547) so a G502 X LIGHTSPEED is reachable at all. Adds the exchange() lock described below.
  • core: per-device [devices."…".onboard_profiles] (mode, profile), config-file only and now documented in docs/CONFIGURATION.md; Capabilities::onboard_profiles from a 0x8100 feature probe.
  • ipc: set_onboard_profiles / read_onboard_profiles agent methods and the reconnect re-apply. PROTOCOL_VERSION 28 → 29, wire-format goldens regenerated.
  • gui: a Profiles tab gated on Capabilities::onboard_profiles, with a settings source (OpenLogi settings / onboard memory) and the active-profile selector. The selector offers user slots only: a device's read-only profiles are factory templates a slot is reset from, and the firmware rejects setCurrentProfile on one, so offering them would be an action that cannot succeed. diag profiles still prints them, since showing raw device state is its job.
  • cli: openlogi diag profiles prints the state and runs a mode/profile round-trip (--read-only, --leave-onboard).

Things worth a careful look

PROTOCOL_VERSION 10 → 11. ProfilesMode and ProfileEntry cross the agent↔GUI IPC, so their variant and field order are wire format.

The global lock is a stopgap, not the fix. send_v20 matches a reply to its request by comparing HID++ headers, and the channel is built with rotate_software_id: false, so two concurrent requests to one device carry byte-identical headers and can take each other's replies. Bench-observed on a G502 X: a DPI write racing a mode write fails InvalidArgument 3/3 with the DPI payload landing on 0x8100's feature index, and get_dpi racing a mode write returns Ok(0), silently wrong with no error. write::exchange() serializes verbs to stop it, but the root fix is HidppChannel::set_rotating_sw_id(true). The lock is global rather than per-device and is held across open_route_channel(), so it serializes enumeration for unrelated devices too, which is heavier than the problem warrants. Rotating the software id is left out of this PR deliberately: it changes the vendored channel for every device and every feature, so it wants the bench cases re-run on hardware rather than riding along here. Note also that gesture.rs::run_capture_session opens a channel outside the lock.

Lightspeed receivers route as DeviceRoute::Unifying. They speak the same HID++ 1.0 register protocol, so they are enumerated, routed and paired through the Unifying path, and only receiver_display_name tells them apart. A dedicated route variant would fork every match arm for what is currently a cosmetic difference, so it seems worth revisiting only if their pairing register semantics turn out to diverge.

The new UI strings are English placeholders in all 21 non-English locales, for Crowdin to fill.

The device renders as a silhouette. The pinned asset catalog has no G502 X or X-generation entry (#461), so the panel shows generic art and hotspots.

Testing

Hardware: G502 X LIGHTSPEED over a Lightspeed receiver, on Windows.

cargo fmt --all -- --check
$env:RUSTFLAGS='-D warnings'; cargo clippy --workspace --all-targets -- -D warnings
$env:RUSTFLAGS='-D warnings'; cargo test --workspace
$env:RUSTFLAGS='-D warnings'; $env:RUSTDOCFLAGS='-D warnings'; cargo doc --workspace --no-deps --document-private-items --exclude openlogi-ui --exclude openlogi-desktop --exclude openlogi-overlay --exclude openlogi-agent
cargo xtask ci
cargo xtask release check-publish
cargo deny --all-features --manifest-path crates/openlogi/Cargo.toml check --config .cargo/deny.toml

Verified on hardware:

  • diag profiles reads the description (5 user + 2 read-only profiles, 11 buttons, 16 × 255 B sectors), the mode, the active profile and the directory. The strict 0/1 enabled parse holds on real flash, and the unit fixture carries this captured payload.
  • Mode and active-profile round-trips succeed, including across an agent restart honouring a GUI-set mode = onboard, profile = 2.
  • setCurrentProfile is onboard-mode only. Host mode answers InvalidArgument and reports the active profile as 0x0000, since it parks the flash profile.
  • The mode is volatile: a device left in host mode came back onboard, with sector 0x0002 active, after a power cycle.
  • DPI writes are accepted in both modes. 1600 → 1650 → 1600 round-trips while onboard, while in host mode, and immediately after a mode write.
  • The concurrency pairs above, before and after the lock.

Not verified:

  • Any device other than a G502 X LIGHTSPEED. Everything above generalises from one mouse.
  • The read-only-profile paths. The G502 X reports 2 of them alongside its 5 user slots, yet the sector-0 directory terminates after the 5 user entries and setCurrentProfile rejects 0x0101 to 0x0103, so nothing here exercises them. A device that does list them would be the first real test.
  • macOS and Linux GUI builds were not run locally. Locale-key parity passed in the workspace tests.

Screenshots

Profiles tab: settings source Profiles tab: active profile

Fixes #462

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds full read and mode-control support for HID++ 2.0 Onboard Profiles (0x8100) on G-series gaming mice, enabling OpenLogi to report and switch between host and onboard modes, select an active profile, and re-apply the configured mode on every reconnect. The implementation spans the full stack from a new hidpp feature wrapper through IPC, agent, and GUI layers, with a PROTOCOL_VERSION bump from 28 to 29.

  • Protocol layer (openlogi-hidpp): new OnboardProfilesFeature implementing getDescription, get/set mode, get/set current profile, memoryRead, and directory parsing with strict UnsupportedResponse rejection for unknown wire values.
  • Write/IPC layer: apply_profiles_config_on_channel skips writes the device already matches; Lightspeed receivers (0xc53f, 0xc547) are recognised so a G502 X LIGHTSPEED is routable; a global exchange() lock serialises concurrent verbs as a stopgap for the rotate_software_id root fix.
  • Reconnect path: profiles mode is applied first in reapply_mouse_volatile_in_background before DPI/SmartShift so a mode switch settles before shadowed writes arrive; a device with no configured mode is never touched.

Confidence Score: 5/5

  • The change is safe to merge as a foundational layer for onboard-profiles support. Protocol parsing is strict, the reconnect re-apply ordering is correct, and the IPC version bump is properly guarded by wire-format golden tests.
  • The implementation is thorough, well-tested, and the two findings are minor: one is a missing validation for the edge-case sector 0x0000 that requires deliberate misconfiguration to trigger, and the other is a profile-label numbering gap in a UI the author explicitly marks as functional-not-designed. Neither affects correctness on normal hardware or the primary reconnect path.
  • crates/openlogi-device/src/write/onboard_profiles.rs — validate_user_profile does not reject sector 0; crates/openlogi-desktop/src/features/profiles.rs — profile button label numbering when slots are disabled.

Important Files Changed

Filename Overview
crates/openlogi-hidpp/src/feature/onboard_profiles.rs New HID++ 0x8100 feature implementation covering description, mode, profile, and directory reads. The read_profile_directory loop correctly terminates on both the needed byte count and the DIRECTORY_END terminator. Chunk-aligned memory_read calls at 16-byte strides cannot split a 4-byte directory entry across reads.
crates/openlogi-hidpp/src/feature/onboard_profiles/types.rs Well-structured domain types. parse_directory correctly maps enabled byte 0→false/1→true and rejects any other value with UnsupportedResponse. The total_profile_count helper correctly bounds both user and OOB profiles in the read loop.
crates/openlogi-device/src/write/onboard_profiles.rs Solid read/apply logic with good skip-if-matching optimization. One minor gap: validate_user_profile rejects ROM sectors but passes sector 0 (the directory sector), which firmware would reject — a manually configured profile = 0 causes a warn on every reconnect.
crates/openlogi-agent-core/src/hardware.rs Profiles mode applied first in reapply_mouse_volatile_in_background (before DPI/SmartShift), which is the correct ordering — a mode switch must settle before writes that a mode transition would shadow or reject.
crates/openlogi-agent-core/src/orchestrator.rs Reconnect re-apply correctly gates on capabilities.onboard_profiles and only fires when the device has a configured mode; an unconfigured device is never switched.
crates/openlogi-ipc/src/ipc.rs PROTOCOL_VERSION correctly bumped to 29 with the new onboard-profile DTOs appended. New set_onboard_profiles and read_onboard_profiles methods are placed at the end of the trait, preserving wire-format compatibility.
crates/openlogi-desktop/src/features/profiles.rs Functional panel with correct ROM-profile filtering in selectable_profiles. enumerate() before filter() means profile button labels skip numbers for disabled slots (e.g., "Profile 1" and "Profile 3" instead of "Profile 1" and "Profile 2"), noted for the design pass.
crates/openlogi-desktop/src/state/profiles.rs Correct optimistic-update + immediate retry_profiles pattern. Config persisted to disk (via persist_and_reload) before the IPC write, ensuring reconnect re-apply picks up the new mode even if the immediate write fails.
crates/openlogi-cli/src/cmd/diag/profiles.rs Well-written diagnostic with correct mode-windowing (enter onboard → run profile round-trip → restore mode) and careful restore semantics. restore_profile correctly skips when original sector is 0 or ROM. Includes unit tests for target selection edge cases.
crates/openlogi-core/src/config/settings.rs The OnboardProfiles enum correctly uses serde(tag = "mode") and deny_unknown_fields so a host-mode config cannot carry an ignored profile sector.

Sequence Diagram

sequenceDiagram
    participant GUI as GUI (ProfilesPanel)
    participant Agent as Agent (IPC server)
    participant HID as openlogi-hid
    participant Device as G502 X (HID++)

    GUI->>Agent: set_onboard_profiles(route, mode, profile)
    Agent->>HID: apply_profiles_config_on(shared, mode, profile)
    HID->>Device: get_onboard_mode() [0x8100 fn 2]
    Device-->>HID: current mode
    alt mode mismatch
        HID->>Device: set_onboard_mode(mode) [0x8100 fn 1]
        HID->>Device: get_onboard_mode() [read-back verify]
    end
    alt Onboard mode AND profile configured
        HID->>Device: get_current_profile() [0x8100 fn 4]
        Device-->>HID: active sector
        alt sector mismatch
            HID->>Device: set_current_profile(sector) [0x8100 fn 3]
        end
    end
    Agent-->>GUI: Ok(())
    GUI->>GUI: optimistic UI update + retry_profiles
    GUI->>Agent: read_onboard_profiles(route)
    Agent->>HID: get_onboard_profiles_on(shared)
    HID->>Device: getProfilesDescription() [0x8100 fn 0]
    HID->>Device: get_onboard_mode() [fn 2]
    HID->>Device: get_current_profile() [fn 4]
    HID->>Device: "memory_read(sector=0, offset=N×) [fn 5, directory]"
    Agent-->>GUI: OnboardProfilesInfo

    Note over Agent,Device: On reconnect — reapply_mouse_volatile_in_background
    Agent->>HID: apply_profiles_config_on(shared, configured_mode, configured_profile)
    HID->>Device: [same mode+profile apply sequence]
Loading

Reviews (15): Last reviewed commit: "fix(gui): adapt onboard profiles to curr..." | Re-trigger Greptile

Comment thread crates/openlogi-device/src/write/onboard_profiles.rs
Comment thread crates/openlogi-hid/src/write/onboard_profiles.rs Outdated
@davidbudnick

Copy link
Copy Markdown
Collaborator

@Stanley5249 Looks as you have issues, with your pipeline running for the following change, can you fix issues and re-push?

@davidbudnick
davidbudnick force-pushed the feat/onboard-profiles branch from bd4daa2 to b57c81b Compare August 2, 2026 00:53
@Stanley5249
Stanley5249 force-pushed the feat/onboard-profiles branch from b57c81b to be81dd5 Compare August 3, 2026 05:03
@Stanley5249
Stanley5249 force-pushed the feat/onboard-profiles branch from be81dd5 to 10f99f6 Compare August 3, 2026 05:17
@Stanley5249

Copy link
Copy Markdown
Contributor Author

@davidbudnick CI is green now, and I've updated the PR body. A few things could use a closer look: the protocol version bump, the translations, and the temporary global lock. Details on each are at the top. Thanks.

@davidbudnick

Copy link
Copy Markdown
Collaborator

@davidbudnick CI is green now, and I've updated the PR body. A few things could use a closer look: the protocol version bump, the translations, and the temporary global lock. Details on each are at the top. Thanks.

Perfect thanks for updating it, looks as you have a few merge conflicts which need to be resolved before a merge can take place.

@Stanley5249
Stanley5249 force-pushed the feat/onboard-profiles branch from 6817f7b to 0b6da4b Compare August 3, 2026 15:15
Stanley5249 added a commit to Stanley5249/OpenLogi that referenced this pull request Aug 3, 2026
parse_directory stops at max_entries even when the terminator has not
been reached, so passing profile_count alone truncates the directory on
any device that lists its ROM profiles after the user ones.

Bounding by profile_count + profile_count_oob costs nothing -- the read
loop already stops early at the terminator -- and the terminator stays
the real end of the directory.

Reported by Greptile on AprilNEA#459. Its worked example does not reproduce: a
G502 X terminates the directory right after the 5 user entries, and
rejects set_current_profile for 0x0101/0x0102/0x0103 with
InvalidArgument, so ROM profiles there are counted in the description but
neither listed nor selectable. The bound is still wrong in principle, and
a device that does list them would lose entries.
@Stanley5249
Stanley5249 force-pushed the feat/onboard-profiles branch from 0b6da4b to e42a2c1 Compare August 3, 2026 15:20
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Start a greploop in Codex and it will work through the open comments and keep going until this PR reviews clean.

@Stanley5249

Copy link
Copy Markdown
Contributor Author

@davidbudnick Done. Maybe close #388 as well, its commit is carried here with kiwimaker credited.

Comment thread crates/openlogi-cli/src/cmd/diag/profiles.rs Outdated
@Stanley5249

Copy link
Copy Markdown
Contributor Author

@albertorm95

Thanks! For now, it only lets you switch profile modes and pick an onboard profile. It’s still far from everything G Hub supports, but G Hub’s UX sucks!

@Stanley5249

Copy link
Copy Markdown
Contributor Author

@davidbudnick, mind taking a look? Thanks.

@albertorm95

Copy link
Copy Markdown

@albertorm95

Thanks! For now, it only lets you switch profile modes and pick an onboard profile. It’s still far from everything G Hub supports, but G Hub’s UX sucks!

Looking forward on remapping the keys, let me know if there is a list of missing tasks so maybe I can support this

@Stanley5249

Copy link
Copy Markdown
Contributor Author

@albertorm95 The main missing tasks are:

  • Profile writing
  • Key remapping
  • DPI steps
  • G-Shift
  • Live state updates

Key remapping may be the easiest place to start. DPI steps and G-Shift need more design. There is also a sync issue: the app does not update after pressing DPI Shift or switching profiles until you reopen it.

The G502 X guide may help: https://www.logitech.com/assets/66193/3/g502-x-artanis-web-qsg.pdf

@davidbudnick

Copy link
Copy Markdown
Collaborator

@Stanley5249 Looks as you have quite a few actions failing, can you please update them?

@Stanley5249
Stanley5249 force-pushed the feat/onboard-profiles branch from 2ca9ecd to 1516fe4 Compare August 22, 2026 03:24
@Stanley5249

Copy link
Copy Markdown
Contributor Author

@davidbudnick Done.

@davidbudnick davidbudnick added this to the v0.7.5 milestone Aug 22, 2026
davidbudnick
davidbudnick previously approved these changes Aug 22, 2026
@Stanley5249

Copy link
Copy Markdown
Contributor Author

Thanks.

yuzi-co added a commit to yuzi-co/OpenLogi that referenced this pull request Aug 24, 2026
`hook_only_buttons` was documented as "the OS input hook is the only thing
that can remap this device's buttons". That is true today and stops being
true the moment onboard-profile support lands (AprilNEA#459): writing the device's
own button map over `0x8100` is a second remapping path, so a G502 would
carry both `hook_only_buttons: true` and `onboard_profiles: true`, which
contradict each other.

What the flag actually records is narrower and durable — the absence of
`0x1b04`, so controls cannot be *diverted* over HID++. Rename it to
`no_button_diversion` and say so, including why the name deliberately makes
no claim about which paths remain.

Polarity, position and semantics are unchanged, so the wire encoding is
byte-identical and `PROTOCOL_VERSION` stays at 28 — bincode encodes field
order, not field names. Renaming now is cheap; after the flag is in users'
`config.toml` it would need a migration.

The `diag` line drops its "OS hook only" phrasing for the same reason.
yuzi-co added a commit to yuzi-co/OpenLogi that referenced this pull request Aug 25, 2026
`hook_only_buttons` was documented as "the OS input hook is the only thing
that can remap this device's buttons". That is true today and stops being
true the moment onboard-profile support lands (AprilNEA#459): writing the device's
own button map over `0x8100` is a second remapping path, so a G502 would
carry both `hook_only_buttons: true` and `onboard_profiles: true`, which
contradict each other.

What the flag actually records is narrower and durable — the absence of
`0x1b04`, so controls cannot be *diverted* over HID++. Rename it to
`no_button_diversion` and say so, including why the name deliberately makes
no claim about which paths remain.

Polarity, position and semantics are unchanged, so the wire encoding is
byte-identical and `PROTOCOL_VERSION` stays at 28 — bincode encodes field
order, not field names. Renaming now is cheap; after the flag is in users'
`config.toml` it would need a migration.

The `diag` line drops its "OS hook only" phrasing for the same reason.
@Stanley5249

Copy link
Copy Markdown
Contributor Author

Hi @davidbudnick and @AprilNEA, this PR touches many HID settings paths, so changes on main often create merge conflicts. I’ve just rebased it again. Could you let me know what is still needed before it can be merged? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform: all Cross-platform issue type: feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Device]: Cannot detect Logi Pro Wireless Gen 1 Onboard Mode.

3 participants