Repository navigation
Conversation
The MX Creative Keypad drops a HID++ request that is in flight when a key image report arrives. On Windows a capture session's root getFeature went out 0.1 ms before the startup image burst, never got its reply, and timed out after 5 s. In one trace the keypad also restarted 4.4 s later. The same overlap succeeded at another replug, so it is a firmware race. The HID++ channel gains a request pause. It is taken once the requests in flight are answered or abandoned. While it is held, or while a pause waits for it, new requests park as they do behind a busy header. Each key image write holds a pause from its first packet until the device acknowledges it, so images and HID++ requests no longer overlap on the wire. Refs AprilNEA#2281
…ives up The agent runs each key image write inside a 5 s deadline. If the pause took most of that time, the deadline could cancel the write after its packets went out. Dropping the pause then let parked HID++ requests reach the keypad before it acknowledged the image. The packets and the ack wait now run in a spawned task that owns the pause and the ack listener. The caller still waits for the pause itself, so a caller that gives up before any packet is written writes nothing. Refs AprilNEA#2281
# Conflicts: # crates/openlogi-hidpp/src/channel/pending.rs
Identify 046d:bc00 as a dialpad instead of a headset: a registry entry with its 0x4610 roller ids and key positions, DeviceKind::Dialpad in the keyboard family, DeviceRoute::dialpad() as the one owner of the lookup (guarded by core-dialpad-route-owner), and the DialLeft/DialRight/RollerUp/RollerDown rotation names with the Rotary pairing. A direct Bluetooth LE device is named after itself on Windows. Wire additions join the unreleased v46.
The hardware ledger for the keypad and dialpad: identities, the dialpad's 0x4610 MultiRoller layout, its 0x1b04 keys, the keypad's Windows vendor collections, 0x8040 brightness and LCD face findings, plus the mx_dialpad_probe used to capture them.
Capabilities, per-roller resolution, reporting mode and the diverted rotation event of the MX Creative Dialpad's dial and roller. Function names come from Options+ descriptors; byte layouts are hardware-observed and marked so.
A keyboard session on a known dialpad resolves its 0x4610 rollers, diverts exactly the rotary controls with a stored direction binding, reports their turns as CapturedInput::Rotation, re-diverts them after a wake and hands them back to native reporting on teardown through the single PendingCaptureRestore constructor. The keyboard watcher accumulates turns per route (24 steps per revolution, idle decay) and fires the bound action, or posts scroll directly; dial speed reuses the thumb-wheel sensitivity. A strict replay test arms a dialpad, receives a dial turn, ignores the undiverted roller and checks the native hand-back.
A per-device key_display_brightness (1-100 %) is scaled onto the range the keypad's 0x8040 BrightnessControl reports, never its bottom value, and written on reconnect and when a reload changes it. `openlogi api key-brightness --save` stages it through DeviceEdit like the other agent settings. HidppOperation::WriteKeyBrightness joins the unreleased v46, and the persisted field bumps the config schema to v12.
Lucide 1.43.0 (ISC, the set gpui-kit 0.8.1 ships) behind an optional lucide feature, so the agent can draw a chosen icon on a keypad's LCD key without linking GPUI and the overlay never carries the catalog.
…'s LCD keys A per-device key_faces map styles each grid slot of each binding page: a one- or two-line label, background and text colours or a gradient, a picture, a Lucide icon or an emoji, and whether the action's icon stays. The agent renders the styled face with the system fonts and lowers the JPEG quality until a tile fits one image packet (KEY_IMAGE_SINGLE_PACKET_MAX), the only hardware-verified write. A key without a style keeps its action icon. Shrinking the page count drops the faces of removed pages with their bindings; the field joins schema v12.
shell_output runs a command in the same platform shell a bound RunShellCommand uses, so cmd.exe quoting keeps one owner, and answers at most a byte budget of its standard output within a timeout, killing a shell that outlives it.
A per-device live_keys map turns an LCD key into one that shows a value - a command's first line or JSON label and bar, the output volume, a device's battery or the clock - read by a one-second watcher off the async threads, each key at its own interval. Pressing the key activates it and runs its press action; while it is active the keys under the grid run its decrement and increment (the volume keys for a volume source) instead of turning the page, until a second press or its timeout. Action::ActivateLiveKey joins the unreleased v46 and live_keys schema v12; commands run through openlogi_inject::shell_output.
The Keys tab shows the dialpad: on its measured product render the picture itself is the selector, else a drawing from its registry model stands in. The dial binds a counter-clockwise and a clockwise half, the roller an up and a down half, and the four keys are named by their place. Its controls bind per app profile like the keypad's, and a 'Dial & roller speed' slider sets the device's thumb-wheel sensitivity, which the agent applies to both rotary controls. The key being configured now stands apart from the hovered one on every surface: keyboard callouts, keypad keycaps and dialpad parts share one selected look, and the selected key on a device picture carries a heavier ring with a halo.
The keypad's setting bar gains a 'Key brightness' slider (1-100 %) beside its page bar. Until the user moves it the slider rests on the keypad's factory level and nothing is saved; a release saves the level per device and the agent writes it on reload.
A keypad grid key's panel gains an Appearance tab beside its action: a two-line label, the background as one colour or a gradient, the text and icon colours, a picture (PNG or JPEG) and whether the action icon shows. Every chooser offers one shared palette and a custom hex field. The face belongs to the key on its page and device, is saved through AppState, and the agent repaints the key; the Keys tab previews it on the keypad render.
The Appearance tab gains a Symbol section: keep the action icon, search the Lucide catalog the shared UI crate embeds, or choose one of 379 emojis in eleven themed groups (or type one). A chosen symbol replaces the action icon on the key and in the Keys tab preview. The app's asset source resolves lucide/<name>.svg through openlogi_ui::lucide, which the desktop now enables.
The key panel's Action tab opens on a Custom shortcut section for every key the Keys tab shows - keypad and dialpad keys, the dial's and the roller's directions, and a keyboard's keys. A chord is recorded from the keyboard or typed, shown as key caps, and fired tapped, held, or held at its modifiers while its key is tapped; switching the mode rebinds the bound chord. ShortcutMode and ShortcutError move into binding_editor, so the mouse inspector shares them and gains the third mode too.
A keypad grid key's panel gains a Live tab: make the key show the clock, the volume, a device's battery or a shell command's output, refreshed every 1, 5, 30 or 60 seconds, and set the commands a press and the two keys under the grid run while it is active. The live key is saved per device through AppState and the agent paints and runs it.
The nine LCD keys of the MX Creative Keypad were still named in English in the German catalog; they are now 'LCD-Taste (oben links)' and so on.
The dialpad's rotary bindings and speed, and the keypad's key_display_brightness, key_faces and live_keys, with schema v12 in the version history.
The dialpad registry's 0x0053/0x0056 keys are plain keys that share the Back/Forward CIDs, so the CID family guard ignores that file as it does the registry's own record. The configured key's resting fill gets its own theme token instead of reusing the hover token the press-feedback guard reserves, and two doc links that the new ButtonId import made redundant are shortened.
shell_output killed only the shell on a timeout; a pipeline stage or a background child kept running and held the output pipe, so each refresh of a hanging live-key command leaked a process tree and a reader thread. The command now runs in its own process group (Unix) or a kill-on-close job object (Windows), ended when the answer is in or the timeout passes.
The agent added a live key's activation and the paging keys only on its own dispatch path, so the capture gate and the settings app, which read button_bindings_for, never saw them: a keypad with nothing but a live key got no capture session, and the paging-key policy lived in two places. button_bindings_for now binds a live key's grid slot and seeds the paging keys for a device with live keys; entries under non-grid buttons are ignored. The settings app names the activation instead of panicking on it. Keypad brightness changes skip a disabled device like reconnect writes do.
A live key's actions can no longer turn the page or activate a live key, which looped forever; the rendered-tile cache is capped; a key picture is read only from a regular local file within size, dimension and allocation limits (never a network or device path); labels and command values are cut before text layout. Also restores a CLI doc comment the key-brightness helper had displaced.
The asset depot serves config_core.png (1057x1227) as the keypad's buttons image, but only front_core.png (935x1105) was measured, so the Keys tab fell back to drawn keycaps beside the picture and its keys could not be picked on it. Each measured render now carries its own key centres and outlines (KeypadRender); the photo pane outlines its keys against the render it shows. A re-staged render of any other size still falls back to the keycap map.
Two picture-bound assertions use expect/expect_err, and public docs no longer link to private helpers.
Bring in the Easy-Switch channel actions (AprilNEA#2068). Both sides appended Action variants after HoldModifier in protocol v46; upstream's EasySwitchChannel1-3 keep indices 0x3e-0x40 and ActivateLiveKey moves to 0x41.
[Medium impact] The changes affect device controls and LCD-key workflows for supported Logitech Creative Console devices. Do not merge yet: image shutdown can release its request pause too early, and several other blocking defects remain. Findings
|
A keyboard capture session installed its report listener only after arming, so a dial turned while its diversion's reply was on the way, or while the second roller was being diverted, reached a channel with nobody listening and was lost. The session now installs its listeners before the first diversion write (`capture::arm_listening`, which also owns the rollback on a failed arm) and runs with them (`run_listening`). A roller's reporting mode was written through the timeout-cancellable call: a diversion whose native write stalled past the response deadline let teardown send the hand-back first, and the late diversion then left the roller captured. `MultiRollerFeature::set_mode` now finishes its write on the wire before returning, as the G-key row's ownership writes do. The dialpad replay test now turns the dial before the diversion reply and is bounded by a deadline that names the missing rotation; `roller_restore_waits_for_stalled_take` holds the diversion's native write past the HID++ deadline. Counter-proofs: without the listener move the turn is never reported; with `set_mode` back on `call` the session rolls back while the diversion is still on the wire; without rotation forwarding the replay test fails instead of hanging.
Every brightness save started its own worker with no write ticket and no transport turn, so two quick saves could land in reverse and leave the keypad at the older level while the settings showed the newer one. The writes now take a ticket from a per-keypad `WriteOrder` when the change is accepted and run through `within_turn`: setting a level is idempotent, so the newest request is the one that lands. Writes, and the app's brightness slider, were gated on the keypad's identity rather than on a measured feature. `Capabilities` gains `key_brightness`, set from HID++ `0x8040` in the feature table, and the agent writes a saved level (`configured_key_brightness`, `changed_key_brightness`, as for press force) and the app offers the slider only on a device that reported it. The field is appended; the wire golden is regenerated (protocol v47, see the ipc commit). Tests: `key_brightness_latest_save_wins` holds the first worker while a newer save arrives (without the ticket the keypad ends at the older level); `key_brightness_requires_measured_support` in the agent and the app.
A live read carried only its key, so a slow read of a source the key no longer showed overwrote the new source's value, and the old read time kept the new source waiting for up to an hour. Each read now carries a `LiveReadTicket`; a key holds at most one read in flight, and a value is taken in only while its ticket is that read. A changed source starts the key over and is read at once. An active key's time ran out only after the watcher finished every due read, so a paging key pressed after the timeout still adjusted it and renewed it. `press` and `paging` now expire the key themselves, the orchestrator repaints the frame away then, and the watcher expires keys on every tick whatever is being read. Reads ran one after another and were published together, so a command at its five-second limit held back every other key. `LiveReader` runs them concurrently — commands at most four at a time, the clock and battery never behind one — and the watcher draws each value as it arrives. Tests: `live_value_rejects_previous_source`, `expired_live_key_does_not_capture_paging`, `slow_live_source_does_not_delay_clock`, each failing with its fix reverted.
A styled face carried only its picture's path, and the tile cache is keyed by the face, so a file replaced at the same path — or one that was unreadable when chosen and readable later — kept the old tile until another field changed or the agent restarted. Choosing the same path again saves nothing, so nothing repainted it either. A face now carries a `KeyPicture`: the path plus the file's size and modification time, read when the face is made. The live-key watcher's tick republishes the key displays, which publishes only when a face changed, so a replaced picture is drawn again within a second. Test: `replacing_key_picture_invalidates_tile` (fails without the file revision).
The key panel offered Hold and Hold modifiers for the dial's and the roller's directions. A rotation has no press and no release: dispatch runs the action once per detent, and both hold actions become an ordinary tap on every platform, so the saved mode did not do what its label promised. `offered_modes` gives a rotary target Tap only, and a chord saved with a hold mode before shows as tapped. Test: `rotary_shortcuts_offer_only_supported_modes`.
The Live tab had neither the Action tab's height bound nor a scrolling body; its source list grows with every battery device, and with the command and action fields it ran past the inspector column in a short window. The panel is now bounded by the column and its fields scroll in the same list the Action tab uses. Test: `the_live_tab_scrolls_to_its_last_field_in_a_short_window` — a command live key in a 520 px window at 125% scale; without the fix the panel is 798 px tall.
The Appearance editor tracked its draft, a refused picture and an
invalid colour in three independent fields, and Reset cleared only the
draft: the fresh draft then showed the old colour error. The editor is
now one `AppearanceState` — `Closed` or `Editing { draft, feedback }` —
whose methods are its only writers, so feedback cannot outlive its
draft.
The file chooser answered asynchronously and saved through whatever
device was selected by then, so a device change while it was open put
the picture on the other device. A pick now carries the device and key
it was asked for (`FacePick`) and is dropped unless the editor still
edits them. Which device a face edit saves to is decided once, by
`AppState::key_face_device`, for both the save and the check.
Tests: `key_image_pick_rejects_changed_device`,
`resetting_a_face_drops_its_input_feedback` (fails when Reset does not
close the editor).
README's feature cards and platform table did not mention the console; both now name the keypad's LCD keys, live keys and brightness and the dialpad's dial and roller, with the Windows live-volume gap. CONFIGURATION states what the fixes changed: brightness is written only to a keypad that reported 0x8040 and the newest change wins, live commands run concurrently and a changed source is read at once, a replaced picture is redrawn, and a rotary shortcut is always tapped.
The vendored assets/lucide/astroid.svg is Lucide's icon name, not a misspelling of asteroid; the typos job failed on the file name.
The brightness slider saved the level and left the write to the agent's config reload, whose outcome only reached the log: a refused or timed-out write looked applied, and saving the same level again wrote nothing. The slider now follows the press-force pattern end to end: - `read_key_brightness` / `set_key_brightness` are appended to the agent protocol (v46, with `HidppOperation::ReadKeyBrightness`). The agent writes through the keypad's `WriteOrder`, shared with its reconnect and reload writes, and answers with the level the keypad reads back; a write superseded before its turn answers with a plain read. - The device layer reads `0x8040` back as a percentage through `KeyDisplayBrightness::from_level`, the inverse of `scale`. - The app reads the keypad's level for the slider, writes a release directly, and saves it only when the newest write's read-back is the level asked for. A refusal marks the keypad, shows a note beside the slider, and reads the keypad again so the slider returns to its own level; a reconnect or a landed write clears the note. Tests: `key_brightness_failure_reaches_settings` (fails when the error path neither marks nor re-reads), `only_the_newest_write_is_shown_or_saved`, `a_refused_key_brightness_shows_a_note`, `a_gui_brightness_write_answers_with_the_keypads_read_back`, the `from_level` round trip, and the new wire goldens.
| pub fn current(path: PathBuf) -> Self { | ||
| let revision = std::fs::metadata(&path).ok().map(|meta| FileRevision { | ||
| modified: meta.modified().ok(), | ||
| len: meta.len(), | ||
| }); |
There was a problem hiding this comment.
Picture reads block device actions
Blocking. KeyPicture::current calls std::fs::metadata while the live watcher holds orchestrator.lock(). If a picture is on a slow or unavailable network share, this can stall page presses, config reloads and device updates. The call also runs before load_picture rejects Windows network/device paths.
Check the path before touching it, and read file revisions outside the shared lock. Add picture_revision_reads_do_not_block_device_updates.
| HidppOperation::WriteKeyBrightness, | ||
| &ticket, | ||
| |c| async move { openlogi_hid::set_key_display_brightness_on(&c, brightness).await }, | ||
| |c| async move { openlogi_hid::get_key_display_brightness_on(&c).await }, |
There was a problem hiding this comment.
Blocking. The fallback passed to run_in_turn returns a successful brightness read even when a newer request caused this write to be skipped. If that reading equals the requested level, apply_key_brightness_written saves it as an accepted write.
The write-order rule requires skipped writes to return WriteSuperseded and update no saved value. Return that error here and add superseded_brightness_does_not_save_matching_readback.
Rule Used: Severity: high. Every HID++ write to one device takes a ticket from that device's WriteOrder and runs under WriteTicket::in_order (read-modify-write, strict request order) or WriteTicket::turn (idempotent set-value writes only, newest wins). Th... (source)
| .find(|dev| dev.config_key == *device) | ||
| .and_then(|dev| dev.battery.clone()), |
There was a problem hiding this comment.
Battery comes from sleeping route
Blocking. This find takes the first matching config_key, even when that record is offline and the same device has an online direct connection. build_devices keeps both records and sorts receiver routes before direct routes. A live key can therefore show an old battery reading or — instead of the online device's value.
The device-identity rule requires the online, addressable record to win. Prefer that record and add live_battery_prefers_online_route.
Rule Used: Severity: medium. Per-device configuration is matched through Config::resolve_device_key (crates/openlogi-core/src/config/identity.rs) and the configured links: identity keys are stored lowercase, a route key (receiver:…:slot:…) is rejected at ... (source)
| tokio::select! { | ||
| outcome = &mut capture => panic!( | ||
| "the session ended while its diversion was still on the wire: {:?}", | ||
| outcome.map(|_| ()).map_err(|_| "arming failed") | ||
| ), | ||
| () = stall => {} | ||
| } |
There was a problem hiding this comment.
Blocking. The first select! has no deadline. stall waits on write_accepted(), while the other branch waits for capture to end. If the expected write never arrives and capture stays pending, the test hangs before reaching the timeout below.
The async-test rule requires every wait to be bounded. Wrap this phase, or the whole test run, in DIALPAD_DEADLINE.
Rule Used: Severity: medium. Every bug fix ships a regression test that fails without the fix, and the PR states the counter-proof (the reverted line or the killed mutant). A test that passes with the fix reverted is not a regression test: ask for inputs where ... (source)
| /// Keep the live keys current until the process exits. | ||
| pub(crate) async fn watch(orchestrator: Arc<Mutex<Orchestrator>>) { | ||
| let mut reader = LiveReader::new(read_source); | ||
| let mut tick = tokio::time::interval(TICK); |
There was a problem hiding this comment.
Watcher starts outside required schedule
Blocking. This process-lifetime watcher uses interval, which starts with an immediate tick. The periodic-timer rule requires interval_at with the first tick one period later, unless a reconnect or fast-retry exception is documented. Neither exception is stated here.
Use interval_at(now + TICK, TICK) and add live_watcher_waits_one_period. This repository requirement must be met before merging.
Rule Used: Severity: low. Periodic timers use tokio::time::interval_at with the first tick one period out; an immediate first tick is allowed only on a reconnect or fast-retry path and is commented as deliberate. A cross-thread flag that orders a payload uses... (source)
| value: Option<LiveValue>, | ||
| /// When the last read of `source` was issued; `None`: due now. | ||
| read_at: Option<Instant>, | ||
| /// The read of `source` not answered yet. A key has at most one, so its | ||
| /// values arrive in the order they were read. | ||
| in_flight: Option<LiveReadTicket>, |
There was a problem hiding this comment.
Read phases use independent fields
Non-blocking. Tracked::value, read_at and in_flight encode the read lifecycle, with transitions split across due, set_value and paging. They allow a recorded read time with neither a value nor a pending read. Current transitions avoid that combination, but future changes must preserve those rules by hand.
The owner's state-machine instruction calls for explicit states:
- Replace these fields with
ReadState::{Due, Reading, Ready}, carrying only each phase's valid data. - Replace
PointerState::pending_key_brightness_writesandrefused_key_brightnesswith one per-deviceBrightnessWriteState::{Idle, Pending, Refused}.
Make each enum's impl the only writer. Test stale completions and attempts to mark a pending write refused without ending it.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| pub fn current_key_brightness_supported(&self) -> bool { | ||
| self.current_record().is_some_and(has_key_brightness) |
There was a problem hiding this comment.
Offline brightness changes disappear
Blocking. current_key_brightness_supported checks only saved capabilities, so an unplugged keypad still gets an active slider through its saved device links. But commit_key_brightness now requires a live route and silently returns when none exists. Moving the slider offline saves nothing and reports no refusal, unlike the previous config-only save.
The control rule requires unavailable hardware controls to be hidden or disabled with a reason. Meet that requirement before merging and add offline_keypad_brightness_cannot_accept_release.
Rule Used: Severity: low. Every focusable control calls the shared focus_ring(pal) from crates/openlogi-desktop/src/ui/theme.rs in all its states, and text painted over accent_tint stays text_primary. A control whose change cannot be applied (device off... (source)
The brightness write state lived in three fields — a refused set, a pending-write map and a counter — kept in step by hand. It is now one `KeyBrightnessWrites`, whose per-keypad `WriteStage` is `Pending(write)` or `Refused` (absent: idle), and whose methods are the only writers: a keypad cannot be both pending and refused, and a refusal never outlives the next write. A live-key read whose reader is gone now logs instead of dropping the send result silently. Test: `writes_move_only_on_the_newest_answer` walks every transition (fails when a reconnect also drops a pending write).
`v0.9.1-beta.144` was published from master at 9ca7ef0 with `PROTOCOL_VERSION` 46, so v46 is released and the wire changes this branch appended to it — `DeviceKind::Dialpad`, the dial and roller `ButtonId`s, `HidppOperation::{WriteKeyBrightness, ReadKeyBrightness}`, `Action::ActivateLiveKey`, `Capabilities::key_brightness` and the brightness calls — would let a beta and this build agree on a version they do not share. They now form v47, and the pin follows.
9775e08 to
7e9a06e
Compare
| let picture = style | ||
| .and_then(|style| style.image.clone()) | ||
| .filter(|path| path.exists()); |
There was a problem hiding this comment.
Pictures freeze the settings window
Blocking. PhotoKey::render calls lcd_face, which runs path.exists() for every picture on every repaint. The chooser checks only the file extension. If a saved picture is on an unavailable network share, this file check can freeze the window before GPUI starts loading it.
Check permitted paths and file availability outside rendering. Add key_picture_checks_do_not_block_render.
| device.key_faces.retain(|button, _| kept(button)); | ||
| device.live_keys.retain(|button, _| kept(button)); |
There was a problem hiding this comment.
Page removal skips the warning
Blocking. set_keypad_pages now deletes key_faces and live_keys, but keypad_page_binds still checks only ordinary bindings. A page containing only a clock key or custom faces therefore takes the “empty page” branch and is saved away without the confirmation used for other configured pages.
Include both new maps in the removal check. Add removing_live_or_styled_page_requires_confirmation.
| let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) }; | ||
| if job.is_null() { | ||
| return Ok((child, None)); | ||
| } |
There was a problem hiding this comment.
Failed jobs leave commands running
Blocking. If Windows job setup fails, both failure paths return a running shell with tree = None. shell_output then has no owner for the shell’s children. Its timeout kills only the shell, while a background child can keep running and keep the detached output reader blocked. Repeated live-key reads can accumulate those processes and threads.
Stop the child and return an error when job setup fails. Add shell_output_job_failure_leaves_no_child.
| let transaction = tokio::spawn(async move { | ||
| let _pause = pause; | ||
| let _ack_listener = ack_listener; | ||
| for packet in &packets { | ||
| write_whole_raw_report(&channel, packet, HidppOperation::WriteKeyImage).await?; | ||
| } | ||
| tokio::time::timeout(ACK_TIMEOUT, ack_rx) |
There was a problem hiding this comment.
Shutdown releases image pause early
Blocking. The new task keeps the request pause after its caller is dropped, but the key-display manager returns immediately on shutdown and its worker then drops the runtime. That cancels the image task and releases the pause before its acknowledgement or deadline. Keyboard restoration runs concurrently, so its HID++ requests can reach a keypad still taking an image—the overlap this change is meant to prevent.
The deadline rule requires a pause held until acknowledgement to survive caller cancellation. Await the active image task before acknowledging shutdown. Add key_display_shutdown_keeps_pause_until_ack.
Rule Used: Severity: high. Every nested I/O step has its own timeout that fits inside the caller's deadline, and a test asserts the arithmetic. An advisory read the device may ignore (LED, hosts info) gets a budget of hundreds of milliseconds and reads as unkno... (source)
| .bindings | ||
| .get(&turn.button) | ||
| .map_or(Action::None, Binding::click_action); | ||
| match rotary.advance(turn, &action, bindings.rotary_sensitivity, Instant::now()) { |
There was a problem hiding this comment.
Queued turns lose their pauses
Blocking. CapturedInput::Rotation drops the device timestamp, and dispatch passes Instant::now() to the accumulator instead. Queued turns more than STEP_DECAY apart can therefore look adjacent and complete a step that should have expired.
This violates the input-timing rule: use the kernel or arrival timestamp, not the time an event is read. Carry an arrival Instant through the queue. Add queued_rotations_keep_their_idle_gap.
Rule Used: Severity: medium. Input events are timed by their kernel or arrival timestamp, not the time of the read; queued batches keep their pauses; a backwards clock step reads as zero elapsed; deadlines stay on Instant. Values that can change at runtime (d... (source)
| self.config.edit(|config| { | ||
| let live_keys = &mut config.devices.entry(key).or_default().live_keys; | ||
| match live { | ||
| Some(live) => { | ||
| live_keys.insert(ButtonId::Grid(slot), live); | ||
| } | ||
| None => { | ||
| live_keys.remove(&ButtonId::Grid(slot)); | ||
| } | ||
| } | ||
| }); | ||
| self.persist_and_reload("live key"); |
There was a problem hiding this comment.
Non-blocking. commit_live_key changes live_keys but leaves the cached button_bindings unchanged. The Action tab reads that cache, while the LCD preview rebuilds its actions from the current config. Enabling or disabling live mode can therefore leave the panel showing the old action until selection or inventory changes.
Refresh the bindings and announce BindingsChanged after the edit, as commit_keypad_pages does. Add live_toggle_refreshes_key_action_panel.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Adds full MX Creative Console support. The Dialpad is now recognised (it was identified as a headset) and its dial, roller and keys can be bound. The Keypad's nine LCD keys get custom faces, live values and adjustable brightness. Everything is configurable from the Keys tab.
Builds on #2322 (HID++ requests are held while a key image is in flight). Its two commits are included here and drop out once it merges.
Changes
0x4610MultiRoller feature (capabilities, resolution, reporting mode, diverted rotation events).046d:bc00modelled asDeviceKind::Dialpadwith its roller IDs and key positions;DialLeft/DialRight/RollerUp/RollerDownrotation names. New per-device configkey_display_brightness,key_facesandlive_keys(schema v12). Live keys are bound inbutton_bindings_for, the one binding owner.0x8040. Renders styled key faces (label, colours/gradient, picture, Lucide icon or emoji) as single-packet JPEGs. Shows live values (command output, volume, battery, clock); an active live key takes over the keys below the grid. Picture size, tile cache and live-key loops are bounded.shell_outputruns through the same shell owner asRunShellCommand, in its own process group / kill-on-close job, so a timeout ends the whole process tree.lucidefeature (agent and desktop only; the overlay does not get it).DeviceKind::Dialpad,HidppOperation::WriteKeyBrightness,Action::ActivateLiveKey(index0x41, after the Easy-Switch channels from feat: software-initiated easy-switch host switching #2068).CONFIGURATION.md, German strings, and a Dialpad fixture with a strict replay test.Screenshots
MX Creative Keypad: styled keys (solid colour, gradient, Lucide icons, emoji, live keys) and the brightness slider
Action tab with custom shortcut recording
Key appearance: Lucide icon picker · emoji picker
Live key
MX Creative Dialpad: dial and roller bindings, speed slider · selected dial half
Testing
cargo check --all-targets,cargo clippy --all-targets -- -D warningsandcargo teston core, ipc, agent-core and desktop;cargo fmt --check. Run on Windows after merging upstreammaster.cargo test -p openlogi-desktop: 890 pass.openlogi-agent-mock --fixturewith the recorded Keypad and Dialpad fixtures.Fixes #2240