# How OpenLogi's Short/Long Press Binding Mechanism Works with the 500ms Threshold

> Discover how OpenLogi's short long press binding mechanism uses the 500ms threshold. Learn about the device reported short long timeout and its conversion to milliseconds for press detection.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-08-31

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-hidpp/src/feature/crown.rs) (lines 116-150), the `Mode` struct captures this raw value:

```rust
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()`:

```rust
/// 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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-core/src/binding.rs) cleanly separates the two action types through an enum:

```rust
#[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`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-device/src/session/gesture.rs) implements the actual threshold comparison. It tracks when each button enters the pressed state:

```rust
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:

```rust
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:

```toml
[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`](https://github.com/AprilNEA/OpenLogi/blob/main/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_timeout` byte (×25ms) from HID++ Crown feature, parsed in [`crown.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/crown.rs)
- **500ms default**: Results from raw value `20` commonly reported by Logitech devices
- **Dual action model**: `Action::Press` and `Action::PressLong` variants in [`binding.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/binding.rs) with `_long` suffix mapping
- **Precise timing**: `Instant`-based duration tracking in [`gesture.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/gesture.rs) with strict threshold comparison
- **Graceful degradation**: `0` or `0xFF` values disable long-press path via `Option<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`](https://github.com/AprilNEA/OpenLogi/blob/main/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).