feat: onboard profiles (0x8100) support for G-series mice - #459
feat: onboard profiles (0x8100) support for G-series mice#459Stanley5249 wants to merge 6 commits into
Conversation
Greptile SummaryThis PR adds read-and-mode-control support for HID++ 2.0 Onboard Profiles (
Confidence Score: 5/5
|
| Filename | Overview |
|---|---|
| crates/openlogi-hidpp/src/feature/onboard_profiles.rs | New feature module implementing HID++ 0x8100 protocol (get description, get/set mode, get/set profile, directory read). The read_profile_directory loop correctly bounds on total_profile_count() (user + OOB), exits early on terminator, and delegates parsing cleanly to parse_directory. Terminator detection and memory_read calls are correct. |
| crates/openlogi-hidpp/src/feature/onboard_profiles/types.rs | Protocol types for 0x8100. parse_directory correctly handles the terminator (0xffff sector), unknown enabled bytes (strict 0/1 only), erased flash, and the max_entries bound. The ROM sector flag (0x0100) is clearly separated from user sectors. All field offsets match libratbag/Solaar references. |
| crates/openlogi-hid/src/write/onboard_profiles.rs | I/O verbs: get_onboard_profiles, set_profiles_mode, set_active_profile, apply_profiles_config. The apply path correctly skips writes the device already matches, validates ROM sectors before touching firmware, and logs (rather than failing) a post-write readback mismatch. Mode and profile writes are ordered correctly — mode first so the device is in onboard mode before a profile sector is selected. |
| crates/openlogi-agent-core/src/orchestrator.rs | configured_onboard_profiles correctly gates on both capability presence and explicit config, so unconfigured devices are never silently mode-switched. Onboard profiles are applied before other volatile settings in reapply_mouse_volatile_in_background, which matters because onboard mode activation affects which device settings take effect. |
| crates/openlogi-desktop/src/features/profiles.rs | GUI panel for mode toggle and profile selection. selectable_profiles correctly excludes ROM and disabled entries; keep_profile_for correctly falls back to the first enabled user profile and never selects a ROM sector. Optimistic writes are followed by a confirming read. The lazy-load / offline / error / retry state machine matches the DPI and SmartShift panels. |
| crates/openlogi-desktop/src/state/profiles.rs | GUI state for onboard profiles: lazy-load cache, optimistic writes, confirming-read flag, and reconnect persistence. store_profiles_info guards stale reads by matching both device key and route. The offline path (no route) correctly skips IPC but still saves config and applies optimistically. |
| crates/openlogi-cli/src/cmd/diag/profiles.rs | diag profiles command: reads state, enters onboard mode for the profile round-trip, restores original mode, and correctly handles the case where enter_onboard fails (skips the round-trip, still attempts mode restore). finish_with_restore surfaces both errors when both the operation and restore fail. round_trip_target excludes ROM sectors and the zero active-profile from the round-trip target selection. |
| crates/openlogi-ipc/src/ipc.rs | PROTOCOL_VERSION correctly bumped to 22 (from 21); two new agent methods appended at the end of the trait to preserve existing variant order. Wire-format golden tests in wire_format.rs cover all new DTOs including ProfilesMode, ProfileEntry, OnboardProfilesInfo, and both new AgentRequest variants. |
| crates/openlogi-core/src/config/settings.rs | OnboardProfiles enum uses serde tagged representation with deny_unknown_fields, which correctly prevents host-mode configs from carrying a profile field. Both TOML shapes (host, onboard with/without profile) are tested and round-trip correctly. |
Sequence Diagram
sequenceDiagram
participant GUI as GUI (ProfilesPanel)
participant AppState
participant IPC as IPC Service
participant Agent as Agent Server
participant HID as HID++ Layer
participant Device as G-series Mouse
Note over GUI,Device: Read flow (lazy, on panel render)
GUI->>AppState: current_profiles_unqueried()?
AppState-->>GUI: true
GUI->>IPC: ReadOnboardProfiles(route)
IPC->>Agent: read_onboard_profiles(route)
Agent->>HID: get_onboard_profiles_on(shared)
HID->>Device: getProfilesDescription (fn 0)
Device-->>HID: memory_model, profile_count, oob_count, sector_size
HID->>Device: getOnboardMode (fn 2)
Device-->>HID: "mode (Onboard=1 / Host=2)"
HID->>Device: getCurrentProfile (fn 4)
Device-->>HID: active sector
HID->>Device: memoryRead sector 0x0000 (fn 5, repeated)
Device-->>HID: directory bytes (4 bytes/entry)
HID-->>Agent: OnboardProfilesInfo
Agent-->>IPC: Ok(info)
IPC->>AppState: store_profiles_info(key, route, Ok(info))
AppState-->>GUI: ProfilesLoad::Ready(info)
Note over GUI,Device: Write flow (user clicks mode/profile button)
GUI->>AppState: commit_onboard_profiles(mode, profile)
AppState->>AppState: persist_and_reload config
AppState->>IPC: SetOnboardProfiles(route, mode, profile)
AppState->>AppState: optimistic update + set profiles_pending_confirm
IPC->>Agent: set_onboard_profiles(route, mode, profile)
Agent->>HID: apply_profiles_config_on(shared, mode, profile)
HID->>Device: getOnboardMode → compare
HID->>Device: setOnboardMode (fn 1) [if changed]
HID->>Device: getCurrentProfile → compare
HID->>Device: setCurrentProfile (fn 3) [if changed]
Agent-->>IPC: Ok(())
Note over GUI,Device: Confirming read (next render cycle)
GUI->>AppState: take_active_profiles_confirm()
AppState-->>GUI: Some((key, route))
GUI->>IPC: ReadOnboardProfiles(route)
IPC->>Agent: read_onboard_profiles(route)
Agent-->>IPC: Ok(confirmed_info)
IPC->>AppState: store_profiles_info(key, route, Ok(confirmed_info))
Note over GUI,Device: Reconnect re-apply (agent side only)
Agent->>HID: apply_profiles_config_on(shared, configured_mode, configured_profile)
HID->>Device: mode/profile writes if needed
Reviews (11): Last reviewed commit: "docs: document onboard profile configura..." | Re-trigger Greptile
|
@Stanley5249 Looks as you have issues, with your pipeline running for the following change, can you fix issues and re-push? |
bd4daa2 to
b57c81b
Compare
b57c81b to
be81dd5
Compare
be81dd5 to
10f99f6
Compare
|
@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. |
6817f7b to
0b6da4b
Compare
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.
0b6da4b to
e42a2c1
Compare
|
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. |
|
@davidbudnick Done. Maybe close #388 as well, its commit is carried here with kiwimaker credited. |
|
@Stanley5249 Can you please rebase and address comments? |
d71740c to
bc22358
Compare
|
wow, this would be great! |
|
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! |
|
@davidbudnick, mind taking a look? Thanks. |
Looking forward on remapping the keys, let me know if there is a list of missing tasks so maybe I can support this |
|
@albertorm95 The main missing tasks are:
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 |
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 now0x8100was 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:
Worth reviewing as the foundation, not as the finished surface.
Changes
feature/onboard_profileswithgetDescription, 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 andenabledbytes surface asUnsupportedResponserather than silent fallbacks. The directory read is bounded byprofile_count + profile_count_oob, so a device that lists its read-only profiles is not truncated. No flash-write session.ProfilesMode/ProfileEntry/OnboardProfilesInfoand the read/apply verbs;apply_profiles_configskips writes the device already matches. Recognises Lightspeed receivers (0xc53f,0xc547) so a G502 X LIGHTSPEED is reachable at all. Adds theexchange()lock described below.[devices."…".onboard_profiles](mode,profile), config-file only and now documented indocs/CONFIGURATION.md;Capabilities::onboard_profilesfrom a0x8100feature probe.set_onboard_profiles/read_onboard_profilesagent methods and the reconnect re-apply.PROTOCOL_VERSION10 → 11, wire-format goldens regenerated.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 rejectssetCurrentProfileon one, so offering them would be an action that cannot succeed.diag profilesstill prints them, since showing raw device state is its job.openlogi diag profilesprints the state and runs a mode/profile round-trip (--read-only,--leave-onboard).Things worth a careful look
PROTOCOL_VERSION10 → 11.ProfilesModeandProfileEntrycross the agent↔GUI IPC, so their variant and field order are wire format.The global lock is a stopgap, not the fix.
send_v20matches a reply to its request by comparing HID++ headers, and the channel is built withrotate_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 failsInvalidArgument3/3 with the DPI payload landing on0x8100's feature index, andget_dpiracing a mode write returnsOk(0), silently wrong with no error.write::exchange()serializes verbs to stop it, but the root fix isHidppChannel::set_rotating_sw_id(true). The lock is global rather than per-device and is held acrossopen_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 thatgesture.rs::run_capture_sessionopens 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 onlyreceiver_display_nametells 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.Three new UI strings are English placeholders in all 19 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.
Verified on hardware:
diag profilesreads 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/1enabledparse holds on real flash, and the unit fixture carries this captured payload.mode = onboard, profile = 2.setCurrentProfileis onboard-mode only. Host mode answersInvalidArgumentand reports the active profile as0x0000, since it parks the flash profile.0x0002active, after a power cycle.1600 → 1650 → 1600round-trips while onboard, while in host mode, and immediately after a mode write.Not verified:
setCurrentProfilerejects0x0101to0x0103, so nothing here exercises them. A device that does list them would be the first real test.Notes
Depends on #388 (Lightspeed receiver recognition). Its commit is carried here so the branch builds and the hardware test runs; the patch-ids match, so it drops out on rebase once #388 merges.
Screenshots
Fixes #462