Skip to content

Timer Model

anpeaco edited this page May 16, 2026 · 1 revision

Timer Model

FreeJoyX has two kinds of timers that affect button behaviour: global gesture timers (one set per device, in the Shifts & Timers tab) and per-slot timers (one per logical button slot, in the Button Config tab). This page explains which kind drives which slot type.

Global gesture timers

Located in the Shifts & Timers tab. Two values:

Name Default Effect
tap_cutoff_ms 500 ms Maximum press duration that counts as a "tap". Press held longer is reclassified as a hold.
double_tap_window_ms 300 ms After the first tap's release, how long the firmware waits for a second rising edge before deciding the gesture is a single tap.

These run the per-physical gesture state machine and apply only to physicals that have at least one TAP or DOUBLE_TAP slot bound to them.

Per-slot timers

Located in the Button Config tab as the DelayTimer and PressTimer columns. Each column picks one of BUTTON_TIMER_1, BUTTON_TIMER_2, BUTTON_TIMER_3 (whose millisecond values are configured globally in the Shifts & Timers tab) or BUTTON_TIMER_NONE.

Effect depends on the slot's type:

Slot type DelayTimer PressTimer
Standalone NORMAL (no gesture sister) Delay before rise after press Minimum duration in PRESS phase
NORMAL with TAP or DT sister Disabled — cleared, no effect Minimum-hold floor on logical output
TAP Disabled — cleared, no effect Minimum-hold floor on the TAP pulse
DOUBLE_TAP Disabled — cleared, no effect Minimum-hold floor on second-tap release
TOGGLE, RADIO, SEQUENTIAL, etc. Slot-specific (see Button Types) Slot-specific

Minimum-hold floor (gesture-managed slots)

When a slot's logical output would normally drop to 0, the floor holds it high for at least press_timer ms past that drop event. Two guarantees:

  • A press_timer = 0 slot still produces a 20 ms minimum pulse (MIN_PULSE_MS), enough for any HID host polling at 125 Hz to sample it at least twice.
  • The floor only extends output — it never blocks the gesture state machine from detecting the next physical edge. The state machine reads physical edges, not logical outputs.

This means a slot can be configured to guarantee, e.g., a 100 ms visible TAP pulse regardless of how briefly the user actually pressed the button.

Why no DelayTimer on gesture slots

The gesture state machine already imposes implicit delays:

  • TAP fires either immediately on release (no DT sister) or after double_tap_window_ms (with a DT sister, to give the second tap a chance to arrive).
  • DT fires only on the second rising edge.
  • NORMAL fires tap_cutoff_ms after rising (the decision window).

A user-tunable delay_timer on top of these implicit delays would compose strangely and isn't useful — so the configurator clears it to None and disables the column for gesture-managed slots.

Default per-slot timer values

New slots default to BUTTON_TIMER_NONE for both DelayTimer and PressTimer. Setting them is opt-in.

BUTTON_TIMER_1, _2, _3 have configurable millisecond values in the Shifts & Timers tab (defaults: 50 / 100 / 200 ms).

Standalone NORMAL — the classic FreeJoy timer model

When a NORMAL slot's physical has no TAP or DT sister, both timers behave exactly as in upstream FreeJoy:

rising edge -> DELAY phase (lasts delay_timer ms)
                |
                v
              PRESS phase (lasts press_timer ms, output = on_state)
                |
                v
              IDLE (output = physical passthrough)

This is the model used by upstream FreeJoy for all button types and preserved here for backward compatibility on plain NORMAL slots.

See also

  • Button Types — what each Function dropdown entry does
  • Gesture Coexistence — how the gesture state machine resolves between coexisting NORMAL / TAP / DOUBLE_TAP slots
  • anpeaco/FreeJoyX#22 — the issue that introduced the minimum-hold floor semantics

Clone this wiki locally