How OpenLogi's Short/Long Press Binding Mechanism Works with the 500ms Threshold
OpenLogi uses a device-reported short_long_timeout value converted to milliseconds (×25) to distinguish short presses from long presses, defaulting to a ~500ms threshold when the firmware reports 20 (0x14).
The open-source OpenLogi project implements sophisticated gesture recognition for Logitech HID++ devices. Its short/long press binding system — internally nicknamed "SOOMs" (Short-Only Or Mixed-press) — relies on firmware-provided timing values rather than hardcoded constants. This article examines how the 500ms threshold emerges from device communication, how the core binding model represents dual actions, and how the session layer dispatches the correct action based on measured press duration.
Reading the Threshold from HID++ Device Firmware
Logitech's HID++ protocol exposes button timing through the Crown feature. The short_long_timeout byte at payload[3] defines how long a press must last before the firmware itself considers it "long."
In crates/openlogi-hidpp/src/feature/crown.rs (lines 116-150), the Mode struct captures this raw value:
pub struct Mode {
pub short_long_timeout: u8, // raw byte from HID++ Crown feature
// additional fields...
}
impl Mode {
fn from_payload(payload: &[u8]) -> Self {
Self {
short_long_timeout: payload[3],
// ...
}
}
}
The conversion to milliseconds happens in short_long_threshold_ms():
/// Convert the raw byte into a usable millisecond threshold.
fn short_long_threshold_ms(&self) -> Option<u32> {
// 0 means "no long-press support" on this device
NonZeroU8::new(self.short_long_timeout)
.map(|nz| nz.get() as u32 * 25)
}
A raw value of 20 (0x14) yields the 500ms threshold referenced in common configurations. The NonZeroU8 wrapper ensures that a reported value of 0 becomes None, disabling long-press functionality entirely. The special value 0xFF (255) indicates "permanent ratchet" mode — also mapped to None — where no long-press detection should occur.
Core Binding Model: Press vs. PressLong
OpenLogi's binding system in crates/openlogi-core/src/binding.rs cleanly separates the two action types through an enum:
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Action {
/// Regular (short) click.
Press,
/// Hold-then-release – long press. The boxed action executes after hold completes.
PressLong(Box<Action>),
// ...Shortcut, Scroll, DPICycle, etc.
}
The PressLong variant carries a boxed Action representing what executes after the long press is confirmed. This recursive design allows chaining — though in practice, Most configurations use simple action types inside the box.
Configuration parsing in crates/openlogi-core/src/bindings.rs recognizes the _long suffix on button names. A TOML entry like Back_long = "Redo" generates two distinct bindings:
ButtonId::Back→Action::Press(implicit default)ButtonId::BackLong(generated) →Action::PressLong(Box::new(Action::Shortcut("Redo")))
Session-Level Duration Tracking and Dispatch
The gesture session in crates/openlogi-device/src/session/gesture.rs implements the actual threshold comparison. It tracks when each button enters the pressed state:
fn on_button_down(&mut self, btn: ButtonId) {
self.press_times.insert(btn, Instant::now());
}
On button release, elapsed time determines which binding to execute:
fn on_button_up(&mut self, btn: ButtonId) {
if let Some(start) = self.press_times.remove(&btn) {
let elapsed = start.elapsed().as_millis() as u32;
let threshold = self.device.short_long_threshold_ms().unwrap_or(0);
if elapsed <= threshold {
// Short press — use normal binding
if let Some(action) = self.effective_bindings.get(&btn) {
self.dispatch(action.clone());
}
} else {
// Long press — look for *_long variant
let long_key = ButtonId::long_variant(btn);
if let Some(long_action) = self.effective_bindings.get(&long_key) {
self.dispatch(long_action.clone());
}
// No long binding present: press is ignored (no fallback to short)
}
}
}
Key implementation details from the source:
- Strict threshold comparison:
elapsed <= threshold— releases at exactly the threshold count as short presses - No automatic fallback: Missing long-press bindings result in ignored input, not demotion to short-press
- Microsecond precision:
Instant::now()provides monotonic timing immune to system clock changes
TOML Configuration and UI Representation
Users configure dual bindings through simple suffix conventions:
[bindings]
Back = "Undo" # short-press: released within threshold
Back_long = "Redo" # long-press: held beyond threshold (~500ms default)
The Actions Ring UI in crates/openlogi-desktop/src/ui/action.rs renders distinct icons when both variants exist for a button, providing visual feedback about available gesture options.
Why 500ms? The Device-Driven Design
OpenLogi deliberately avoids overriding the device's own timing preference. The 500ms value emerges from:
- Logitech firmware default of
20× 25ms units = 500ms - Hardcoded device profiles reporting different values (some use 375ms, others 625ms)
- User-customizable values stored in onboard device memory
This design ensures consistent behavior between OpenLogi and Logitech's official software — the same physical timing that triggers "long press" in Options+ triggers it in OpenLogi.
Summary
- Firmware-provided threshold:
short_long_timeoutbyte (×25ms) from HID++ Crown feature, parsed incrown.rs - 500ms default: Results from raw value
20commonly reported by Logitech devices - Dual action model:
Action::PressandAction::PressLongvariants inbinding.rswith_longsuffix mapping - Precise timing:
Instant-based duration tracking ingesture.rswith strict threshold comparison - Graceful degradation:
0or0xFFvalues disable long-press path viaOption<NonZeroU8>
Frequently Asked Questions
How do I find my device's actual short/long threshold?
The threshold appears in OpenLogi logs at debug level when the device initializes. Look for short_long_threshold_ms in output from the openlogi-device crate. Alternatively, check your device's HID++ register dump — byte 3 of the Crown feature response.
Can I override the 500ms threshold if my device reports 20?
Not through OpenLogi configuration. The project intentionally respects the firmware-reported value for consistency with official Logitech software. You would need to modify the device firmware or patch crown.rs to replace the parsed value before Mode construction.
What happens if I only define a long-press binding without a short-press?
Short presses become no-ops. The dispatch logic has no fallback — it strictly routes to short_action for ≤threshold releases and long_action for >threshold releases. Define both bindings for complete functionality.
Why does my long press sometimes trigger the short action?
This typically indicates threshold boundary effects. The comparison elapsed <= threshold uses millisecond truncation; nanosecond precision within the millisecond can shift classification. For reliable long presses, hold approximately 50ms beyond the threshold (550ms for 500ms devices).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →