feat(routing): bass shaker support and generalised effect routing#66
Draft
walmis wants to merge 102 commits into
Draft
feat(routing): bass shaker support and generalised effect routing#66walmis wants to merge 102 commits into
walmis wants to merge 102 commits into
Conversation
…e before retrieving gains. Fixes startup error.
Adds docs/shaker-mvp/ with PLAN.md, ARCHITECTURE.md, and one STEP_NN_*.md per phase of the bass-shaker MVP. No source-code changes. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
Adds telemffb/hw/shaker_synth.py — phase-continuous sine oscillators with linear amplitude ramping, mixed into a sounddevice OutputStream. Module is self-contained (no telemffb.* imports) and runnable via ``python -m telemffb.hw.shaker_synth --list-devices|--selftest``. Adds sounddevice to requirements.txt. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
Adds telemffb/hw/ffb_shaker.py mirroring the ffb_rhino HapticEffect surface. .periodic / .constant configure parameters; .start creates/updates the named oscillator on the module-level ShakerSynth; .stop ramps to zero; .destroy removes the oscillator. Force-only methods (.spring/.damper/.friction/ .inertia/.setCondition/.spring_adjuster/._conditional_effect) are chainable no-ops with debug logs. Effect-type constants mirror ffb_rhino exactly. Whitelist filtering of effect names is deferred to STEP_04. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
- CmdLineArgs --type help: include shaker - main.py mapping/index_dict: add shaker -> 5 - _determine_master_instance_status: refuse to launch shaker as master - _initialize_device_connection: shaker branch initialises ShakerSynth and rebinds aircraft_base.effects.cls via use_shaker_backend(), in lieu of HapticEffect.open(). Rhino path is byte-for-byte unchanged. - aircraft_base: add is_shaker() predicate and use_shaker_backend() rebind - globals: add shaker_synth slot The runtime rebind is used in place of a conditional import at the top of aircraft_base.py because aircraft_base is imported transitively from main.py's top-level imports (via MainWindow), before _setup_device_configuration runs. Effects are created lazily via Dispenser, so swapping effects.cls before telemetry processing begins is sufficient. The brief explicitly anticipated this contingency. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
Adds a hardcoded set of effect names known to translate usefully to a bass shaker (~46 entries spanning runway / weapons / buffeting / afterburner / prop+rotor / surface movements / overspeed+aoa / wind). HapticEffect.start() drops effect names not in the set with a debug log and no synth side effects. Force-only methods (.spring/.damper/...) remain chainable no-ops as before; this whitelist provides defence in depth so unrelated effects added upstream don't accidentally produce audio. The set is module-level and public, so adding a new effect name is a one-line edit with no other changes required. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
A new top-level "Shaker" tab is added programmatically in
SystemSettingsDialog.__init__ so the Qt-Designer-generated Ui_SystemDialog.py
is left untouched. The tab exposes three controls:
- Output device combobox: populated from ShakerSynth.list_output_devices(),
items show "{idx}: {name} ({sr:.0f} Hz)" with the device name as userData
so settings survive index reshuffles between reboots. Item 0 is
"(System default)" with userData="".
- Master gain: QDoubleSpinBox, range 0.0-2.0, step 0.05, default 1.0.
- Test button: plays 2 s of 35 Hz @ 0.5 on a daemon thread; the UI thread
re-enables the button via QTimer.singleShot after the worker is done.
Persistence: 'shakerDevice' (str) and 'shakerGain' (float) added to the
global settings dict, with defaults '' and 1.0 in SystemSettings.globl_sys_dict.
main.py's shaker branch normalises an empty-string device to None.
If the audio backend (sounddevice/PortAudio) is unavailable at dialog open
time, the tab still renders but combo+test are disabled with an
explanatory tooltip.
https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
A non-modal Effect Tester dialog reachable from MainWindow > Utilities lets the user play a single tunable effect on the locally-bound device. Sliders for frequency / magnitude / direction; combobox for effect type (sine, square, triangle, sawtooth up/down, constant); continuous-vs-fixed-duration toggle; a "Live update while playing" checkbox that re-tunes the running effect on slider drag. Backed by aircraft_base.effects['__effect_tester__'], so the dialog dispatches to whichever HapticEffect is bound: the Rhino HID on joystick / pedals / collective / trimwheel instances; the shaker facade on a shaker child instance. The reserved name is added to SHAKER_EFFECT_WHITELIST so the tester is allowed to drive the shaker. Cleanup on dialog close stops the effect, calls destroy, and disposes the dispenser entry so re-opening produces a fresh effect. This is a developer / tuning tool, not a persisted setting. https://claude.ai/code/session_019KUXPrunPNuZLNEcVBaKCJ
Add bass shaker audio synthesis support with device-type integration
Closes the gaps left in STEP_03 / STEP_05 that prevented the shaker child
from launching, registering as an active device, and producing audio in
response to MSFS telemetry.
Launch / IPC plumbing
- main.py: _launch_children now spawns the shaker child too. Bootstrap
pysimconnect from site-packages so the local ./simconnect/ resource
folder doesn't shadow it as a namespace package. Skip the
configurator-gains async init when no Rhino is bound (dev=None).
- IPCNetworkThread._child_active: add 'shaker' so master tracks keepalives.
- MainWindow: tray Instances + Log child menus iterate 'shaker';
geometry match in __init__, do_reset_window_size, set_default_geometry
add a 'shaker' case (plus 'case _' fallback for forward compat);
change_config_scope handles the 'shaker' device id.
- DevicePanel.DEVICE_ICONS: add 'shaker' (placeholder reuses collective
icon — drop in image/icon_shaker.png + regenerate resources to replace).
- utils.SystemSettings.globl_sys_dict: defaults for pidShaker (2059),
autolaunchShaker, startMinShaker, startHeadlessShaker.
System Settings UI
- Ui_SystemDialog.py: add 6th row to master launch options grid
(lab_shaker / tb_pid_s / cb_al_enable_s / cb_min_enable_s /
cb_headless_s); shift label_10 footer from row 6 to 7.
- SystemSettingsDialog.py: validator + signals on the new widgets;
extend change_master_widgets (shaker row stays visible — never master),
toggle_al_widgets, toggle_launchmode_cbs, validate_settings,
save_settings, load_settings, current_al_dict.
- Fix STEP_05 ordering bug: _setup_shaker_tab now runs before
load_settings so _load_shaker_settings finds shaker_device_combo.
Aircraft handler routing
- aircraft_base.use_shaker_backend(): also rebind HapticEffect /
FFBReport_SetCondition in the already-imported sibling modules
(aircrafts_msfs_xp / aircrafts_dcs / aircrafts_il2). The static
'from ... import HapticEffect' in those modules captures a reference
before this function runs, so the global rebind here didn't reach
them — and per-frame HapticEffect() construction in the shaker child
was instantiating the Rhino class, crashing on .device.create_effect.
- ffb_shaker.HapticEffect: add a _StubDevice with get_input() returning
a _ZeroInput that supports forceXY / axisXY / CP_XY / CP_scaled_axisXY
/ isButtonPressed / getPressedButtons; plus set_deadzone, reset_effects,
and a get_gains() returning a zeroed stub (ConfiguratorDialog reads
master_gain / periodic_gain / spring_gain / damper_gain / inertia_gain
/ friction_gain / constant_gain). Add an id property returning None
so MainWindow.on_update_telemetry's "ID:{}" formatting doesn't crash.
- SimConnectSock.emit_event: guard reset_effects on the Open event
when HapticEffect.device is the shaker stub (no HID to reset).
Effect Tester
- EffectTestDialog: when the user switches effect type while playing
(Constant <-> Sine, or between two periodic types), destroy the
underlying _h_effect and recreate it. Previously the cached
_h_effect was reused with the wrong type, tripping
setPeriodic's `assert self.type in PERIODIC_EFFECTS`.
SimConnectManager
- Drop the hardcoded DATATYPE_* constants that were patched in as a
workaround for the broken `from simconnect import *` import. Their
values were wrong (e.g. DATATYPE_FLOAT64=1 vs. the real 4) and would
have silently corrupted SimConnect data parsing once the bootstrap
in main.py made the wildcard import work.
Verified: master + shaker child both connect to MSFS, shaker registers
in Active Devices, and whitelisted effects (prop_rpm*, buffeting,
flapsmovement, etc.) drive audible bass-shaker output in a C172 flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…al pan
Three coordinated polish changes for the bass-shaker output, all inside
the existing whitelist-routing model:
1. Transient envelope on Oscillator
- New Oscillator.trigger(freq, amp, attack_ms, decay_ms): linear
attack + exponential decay (k = ln(256)/decay_samples), envelope
ends itself, re-trigger restarts from sample 0.
- render() picks envelope path when active; set()/stop() cancel any
active envelope so continuous effects keep their old behaviour.
- is_silent now considers envelope state.
2. Per-effect profile table in ffb_shaker.py
- SHAKER_EFFECT_PROFILES: tuning per effect-name (kind transient |
continuous, freq override, gain, attack/decay/ramp ms).
- HapticEffect.start() routes through the profile: gearclunk,
touchdown, runway_bumps, gunfire, cm, payload_rel become crisp
transients with envelope-driven attack/decay; buffeting variants
get a snappier 15 ms ramp and slight gain bump.
- Heuristic fallback: a SQUARE call with duration ≤ 80 ms also takes
the transient path so EffectTestDialog feels right out of the box.
- gearclunk added to SHAKER_EFFECT_WHITELIST. aircraft_base.py:1233
guard widened from is_joystick() to (is_joystick() or is_shaker())
so the existing call reaches the shaker too.
3. Spatial channel routing in ShakerSynth
- New ctor kwargs channel_mode ∈ {mono, left, right, pan} and pan
∈ [-1, +1]; runtime setters set_channel_mode / set_pan.
- start() forces stereo when channel_mode != mono and falls back to
mono with a warning if the device refuses 2-channel.
- _callback() uses equal-power pan (cos/sin) so loudness stays
constant across the sweep — gives stick-front / shaker-rear feel.
- System Settings → Shaker tab gains an Output channel combo and a
-1..+1 pan slider with a live "Center / L 0.30 / R 0.60" label;
slider only enabled in Stereo (pan) mode. Test button now also
fires a couple of thomps so the new transient path is audible.
- Settings persisted as shakerChannelMode / shakerPan in
globl_sys_dict; main.py forwards both to ShakerSynth on init.
Verification: shaker_synth standalone smoke test confirms envelope
attack/decay sample counts, retrigger from 0, set() cancellation, and
equal-power pan (gl² + gr² = 1.0). ffb_shaker routing test confirms
gearclunk and touchdown go transient with profile freq override,
buffeting stays continuous with gain bump, the square+short heuristic
picks up the effect tester, and unwhitelisted names stay dropped. New
CLI mode python -m telemffb.hw.shaker_synth --selftest-transient
plays a thomp sequence with L / center / R panning.
STEP_00 of the layered-routing iteration: planning docs only, no code. - PLAN_LAYERS.md: phase checklist + codebase reality-check table reconciling the brief's assumptions against the actual current code (Oscillator.trigger() not a separate ImpulseOscillator class; public attributes on HapticEffect not underscore-prefixed; SHAKER_EFFECT_ PROFILES coexists with new EFFECT_LAYERS; config dir is G.userconfig_rootpath). - STEP_00..STEP_05 markdown files: substantive working specs for each phase. Layer dataclass / dispatch (STEP_01), JSON load + atomic save + reload (STEP_02), System Settings layer-editor tab (STEP_03), bundled default pack at telemffb/data/shaker_effects_default.json plus on-first-start copy (STEP_04), MSFS smoke-test matrix and KNOWN_ISSUES.md template (STEP_05). - Each step doc captures acceptance criteria, out-of-scope items, and ends with "Stop here / request review". No source files modified. STEP_01 is the next step; awaiting human sign-off on this batch before implementation begins.
Add `Layer` frozen dataclass (freq_factor, gain, route, osc_type) and `DEFAULT_LAYER` at module scope in `ffb_shaker.py`. Add routing predicate `_layer_is_for_shaker`. Populate `EFFECT_LAYERS` with a hardcoded test dict for je_rumble_1_1, gunfire, and touchdown. Refactor `HapticEffect.start()`: whitelist check stays first; if the effect name is in EFFECT_LAYERS, delegate to the new `_start_layered()` helper and return; otherwise the existing SHAKER_EFFECT_PROFILES / heuristic / default path runs unchanged. `_start_layered()` creates one Oscillator per shaker-routed layer (keyed `<name>__layer<idx>`), calls `osc.set()` for sine layers and `osc.trigger()` for impulse layers. The duration timer fires only when at least one sine layer is present (impulse envelopes self-terminate). Add `_stop_layer_names()` helper. Refactor `HapticEffect.stop()` to take the layer path first when the effect is in EFFECT_LAYERS, then fall through to the existing single-oscillator path. Add `--selftest-layered` CLI entry point under `python -m telemffb.hw.ffb_shaker --selftest-layered`. `shaker_synth.py` required no changes; the single `Oscillator` class with `set()`/`trigger()` covers both osc_type paths. Smoke tests (run without audio hardware) confirm: - je_rumble_1_1 creates layer0 and layer2; stick layer1 is absent. - gunfire impulse layer has _env_active=True after start(). - sine layer0 has non-zero _target_amp after start(). - runway0 still gets a single oscillator named "runway0" (legacy path). - gearclunk still triggers the profile transient path (_env_active=True). - stop() zeroes all layer oscillators for je_rumble_1_1. Tick STEP_01 done in PLAN_LAYERS.md and add implementation notes. https://claude.ai/code/session_01LXqfeeYWaDvToN8TvogKHE
- New telemffb/hw/shaker_layers_io.py: load(), save(), get_shaker_effects_path(),
CURRENT_VERSION=1, GPL v3 header. load() handles missing file (info log, {}),
malformed JSON (exception log, {}), version mismatch (warn + best-effort), and
bad layer entries (skip individual effect, log, continue). save() writes atomically
via path+".tmp" then os.replace(). get_shaker_effects_path() lazy-imports globals
to stay importable in standalone contexts.
- ffb_shaker.py: replace hardcoded STEP_01 test dict with empty EFFECT_LAYERS={};
add _BUILTIN_DEFAULT_LAYERS={} stub (STEP_04 will populate); add reload_layers()
that clears EFFECT_LAYERS, applies _BUILTIN_DEFAULT_LAYERS, then overlays the
user JSON. Fix two STEP_01 review gaps: HapticEffect.destroy() now iterates
__layerN oscillators for layered effects (was leaking them); started property
now checks shaker-routed __layerN oscillators (was always False for layered).
- main.py: call _ffb_shaker.reload_layers() after aircraft_base.use_shaker_backend()
in the shaker init branch; log count of loaded effects.
- PLAN_LAYERS.md: tick STEP_02 checkbox; add implementation notes.
All 10 STEP_02 smoke checks pass (load/save round-trip, atomic write, malformed
JSON, version mismatch, bad entry skip, reload_layers with/without file, layer-
aware destroy, layer-aware started property).
https://claude.ai/code/session_01LXqfeeYWaDvToN8TvogKHE
Adds a new "Effect layers" subsection inside the existing Shaker tab in
SystemSettingsDialog, built programmatically via _setup_shaker_layers_section()
called from _setup_shaker_tab().
Widgets added:
- Effect dropdown (shaker_layer_effect_combo): sorted SHAKER_EFFECT_WHITELIST,
shows ● prefix next to names whose working copy diverges from saved-to-disk
state.
- Layer table (shaker_layer_table): 6 columns (#, Freq×, Gain, Route, OscType,
Remove). Cell widgets are real controls: QDoubleSpinBox for freq/gain,
QComboBox for route/osctype, QPushButton("−") per row. Last-remaining-row
remove button is disabled.
- Per-effect buttons: + Add layer, Reset effect to default, Test effect.
- Bottom buttons: Save all effects, Reload from disk, Reset all effects to
defaults.
Behaviour:
- Working copy (dict[str, list[Layer]]) is mutated by edits; flushed to
working copy on dropdown switch and save. Diverges from saved state until
explicit Save.
- Save all effects merges working copy over on-disk state (preserving effects
not opened this session), writes via shaker_layers_io.save(), then calls
ffb_shaker.reload_layers() so the running shaker child picks up the new
spec without restart. Button disabled when nothing is modified.
- Reload from disk: confirmation dialog if any effect is modified, then
reloads saved state and rebuilds table.
- Reset effect to default: reads _BUILTIN_DEFAULT_LAYERS (empty until STEP_04);
falls back to single Layer() row. Confirmation dialog if unsaved changes.
- Reset all effects to defaults: writes _BUILTIN_DEFAULT_LAYERS to disk,
reloads. After STEP_04 ships the bundle, this becomes the real factory reset.
- Test effect: plays current (unsaved) layer spec on a short-lived ShakerSynth
at 40 Hz × freq_factor per layer for ~2 s on a worker thread (mirrors
_shaker_test_clicked pattern). Uses dialog's current device/gain/channel/pan.
PLAN_LAYERS.md: ticked STEP_03; appended Notes/Deferred entry with layout
decisions, STEP_04 integration notes, and environment constraints.
https://claude.ai/code/session_01LXqfeeYWaDvToN8TvogKHE
- Add telemffb/data/shaker_effects_default.json: 17 curated two-layer
splits for jet/prop/rotor/runway/impulse/buffet/surface effects.
Low-frequency energy (≤25 Hz band) routes to shaker; mids/highs route
to stick. Impulse effects split body-thump on shaker from haptic crack
on stick.
- shaker_layers_io.get_default_pack_path(): resolves the bundled JSON
without importing telemffb.utils (which has a Windows-only top-level
`import winreg`). Replicates the frozen/source path logic inline.
- ffb_shaker._load_builtin_defaults(): parses the bundle at module-import
time; logs a warning and returns {} if the file is missing/malformed so
the module always loads cleanly.
- ffb_shaker._BUILTIN_DEFAULT_LAYERS: populated dict at import time (17
entries), used by reload_layers() as the base that user JSON overlays,
and by SystemSettingsDialog for "Reset effect/all to defaults".
- ffb_shaker.get_builtin_default_for(name): public helper returning a
fresh list copy so callers can mutate without leaking into the module
dict. SystemSettingsDialog continues to read _BUILTIN_DEFAULT_LAYERS
directly (no refactor needed in STEP_04).
- main.py on-first-start copy: if shaker_effects.json is absent, seeds it
from the bundle before reload_layers() runs so the user's first launch
starts with the curated defaults rather than an empty file.
- VPforce-TelemFFB.spec: added ('telemffb/data/shaker_effects_default.json',
'telemffb/data') to datas so the file ships in the frozen executable.
All six STEP_04 smoke checks pass.
https://claude.ai/code/session_01LXqfeeYWaDvToN8TvogKHE
Two new template docs: - MSFS_LAYER_TEST.md: pre-populated with the 5-effect minimum test matrix (engine rumble, touchdown, gunfire/gearmovement, buffeting, runway rumble) plus 4 extras (gear-clunk, payload release, AB rumble, ETL); each effect has the spec per-effect note skeleton with placeholder fill-ins. - KNOWN_ISSUES.md: baseline "no known issues" form with the three-bucket template (defaults / latency / audible interference) inlined below for the user to promote after testing. PLAN_LAYERS.md changes: - STEP_05 marked [~] (in progress, scaffold ready) rather than [x] — the MSFS run has not happened yet; the user ticks it to [x] after their run. - Notes/Deferred entry added for STEP_05 scaffold. - "Iteration summary" subsection appended: one sentence per code step (STEP_01 layer runtime, STEP_02 config I/O, STEP_03 editor UI, STEP_04 default pack) for future reference. https://claude.ai/code/session_01LXqfeeYWaDvToN8TvogKHE
Single-entry overview document for the shaker integration after the MVP and polish iterations have shipped. Covers: - Purpose, hardware assumptions, process model, IPC routing - Audio synthesis pipeline (Oscillator with set/trigger, ShakerSynth with channel modes / pan / equal-power routing) - The four-stage routing chain in HapticEffect.start(): whitelist -> EFFECT_LAYERS -> SHAKER_EFFECT_PROFILES -> heuristic - Layer model: Layer dataclass, EFFECT_LAYERS, _BUILTIN_DEFAULT_LAYERS, shaker_effects.json, on-first-start copy - System Settings UI structure and behaviour - Full default-pack table (17 effects) - Code-path quick reference table - Limitations: synth restrictions (sine + linear/exp envelope only), layer model (stick not yet layer-aware), routing dead-code (PROFILES vs LAYERS overlap), spatial constraints (mono-only DACs), UI duplication, sim coverage gaps - Carry-over issues from the STEP reviews - Extension hooks (stick-side layers, per-aircraft packs, schema v2 with per-layer envelopes, bandpass noise generator, untapped telemetry sources) - Quick-reference for common tasks (deploying defaults, adding new shaker-routable effects, standalone CLI debugging)
Establishes the docs/shaker-cleanups-noise/ folder for the carry-over cleanups (Stream A) and the new bandpass_noise primitive (Stream B). Each STEP_NN file expands the corresponding section from the human's brief with verified file:line references against the current tree. https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
…ECT_LAYERS (STEP_01) The runtime priority chain in start() is Whitelist -> EFFECT_LAYERS -> PROFILES -> Heuristic, so any effect with a layered default never reaches its PROFILES entry. Removes the six dead entries (touchdown, gunfire, cm, buffeting, vrs_buffet, gearbuffet) that are already in the bundled default layer pack, and adds a comment block above the surviving six explaining the precedence semantics for future readers. Behaviour for currently-shipped effects is identical. https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
Both _shaker_layer_rebuild_table and _on_shaker_layer_add duplicated ~40
lines of QTable cell-widget creation. Extracts a private
_make_layer_row_widgets(row_index, layer) helper that builds widgets only
(no signal wiring) and returns them as a dict. Both call sites now
delegate widget construction to the helper and keep their own signal
wiring + working-copy update logic. The dict carries an extras={} field
that STEP_07 will populate for bandpass_noise rows.
Side benefit: _on_shaker_layer_add now uses the same findData -1 guard
as _shaker_layer_rebuild_table, eliminating a silent index-0 fallback
when route/osc_type data lookup fails.
https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
…r CLI path Running ffb_shaker as the entry point with -m made runpy register the module only as __main__, so the top-level "from .ffb_shaker import Layer" in shaker_layers_io.py triggered a second module load mid-init: while the first copy was at line 158 calling _load_builtin_defaults(), the second copy re-ran the body and tried to import get_default_pack_path from a shaker_layers_io that hadn't yet defined it. ImportError fired, all three CLI selftests broke. Production import path (via "from telemffb.hw.ffb_shaker import ...") was unaffected because by the time _load_builtin_defaults runs, Layer is already defined in the partial module so the import succeeds. Fix: defer the Layer import to function-local inside load() — the only place it is actually used at runtime. Type-hint references are stringified and evaluated lazily, so they keep working without the top-level import. A comment documents the reason. Verified: python -m telemffb.hw.ffb_shaker --selftest-layered now reaches synth.start() (only fails further with a sandbox PortAudioError, which is unrelated environmental). https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
…sor API (STEP_03)
Replaces 17 direct accesses to ShakerSynth._oscillators / ._lock from
external callers (HapticEffect methods, the layered selftest, and the
layer-editor test worker) with three public accessors:
- add_oscillator(name, osc): inject a pre-built oscillator under a name,
replacing any existing one. Used by callers that need a custom subclass
(the layer-editor test worker, and STEP_06's BandpassNoiseGenerator path).
- peek_oscillator(name): read-only get; returns None if absent. Used where
the caller needs to inspect or stop an existing oscillator without
auto-creating one.
- list_oscillator_names(): snapshot of currently-registered names; used by
diagnostics and selftests.
HapticEffect.{started, _stop_layer_names, _start_layered, start, stop} now
go through the public API. The layered selftest no longer reaches into
private state. SystemSettingsDialog._on_shaker_layer_test._run uses
add_oscillator to inject test oscillators and stops them via local
references it retained at construction time, instead of looking them up
back through the synth.
Lock-granularity change: _start_layered previously held _synth._lock
across all per-layer oscillator constructions. The new code relies on
get_oscillator's per-oscillator atomicity. Safe because (a) the audio
callback sees an oscillator only after get_oscillator has fully inserted
it under the lock; (b) freshly-created oscillators start silent until the
immediately-following set/trigger call; (c) is_silent oscillators are
skipped by the callback.
git grep -nE "_oscillators|_lock" -- 'telemffb/hw/ffb_shaker.py'
'telemffb/SystemSettingsDialog.py' produces zero matches as required by
the STEP_03 verification.
https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
…STEP_04) Routes the six selftest progress messages through the module logger so that all of ffb_shaker's diagnostic output is captured by whatever logging configuration is active. main() already calls logging.basicConfig at DEBUG level for the CLI selftest paths, so the visible output is unchanged in practice (just decorated with timestamp/level/logger-name prefix from the existing format string). F-string interpolations are converted to lazy %-formatting so the values are only formatted if INFO is actually being emitted. https://claude.ai/code/session_01T6NNUzQayWHZm9gVJxoEqt
The Setup Wizard had two papercuts: 1. On Windows the default QWizard `AeroStyle` forces a white page background that ignores the application palette. Combined with the dark-mode palette's light-grey `WindowText` (#dddddd), QLabel text on every page was effectively invisible. Switching to `ModernStyle` makes the wizard chrome render through Qt and respect the palette. A small stylesheet pins QLabel text and page backgrounds to palette roles as a belt-and-braces against any leftover style quirks. 2. The Device Types page only listed devices reported by `FFBRhino.enumerate()`. Users with a shaker / transducer driven outside the Rhino enumeration (separate amp/controller, or simply not connected at scan time) had no way to declare it from inside the wizard. Added an "Add device" button (defaults to type=shaker, the common case in this fork) plus per-row "Remove" buttons. Re-entering the Welcome page now preserves manually-added rows instead of wiping them on re-enumeration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(wizard): readable text in dark mode + manual device entry
… browse dir to userconfig_rootpath After saving a shaker calibration profile from System Settings, the confirmation dialog now includes the full path to shaker_profiles.json so users can locate the file. This applies to Save, Save As, and the Calibration Wizard flow. The VPConf startup/exit browse dialog now opens G.userconfig_rootpath (the same directory that holds shaker_profiles.json) as its default starting directory instead of os.getcwd(), making it easier to find configuration files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(shaker): show saved profile path in confirmation dialogs; default…
Adds live telemetry forwarding to WinWing SimAppPro so F-15EX / F-16 handles vibrate alongside VPForce FFB devices. Key changes: telemffb/hw/ffb_winwing.py (new) - WinWingSink class: UDP JSON bridge to SimAppPro on 127.0.0.1:16536 - Implements net/ready → mission/start → mod handshake + 3s heartbeat - Sends addCommon telemetry at 20 Hz: trueAirSpeed, angleOfAttack, gearValue (0-1 raw), speedbrakesValue (0-1 raw), accelerationX/Y/Z, verticalVelocity, gearNoseRod/LeftRod/RightRod, cannonShellsCount, payloadStations - Keys use SimAppPro's internal directParam names (no Direct_ prefix — SimAppPro strips that prefix during effectConfig load; sending the prefixed form causes all values to be silently dropped) - Gear rods derive from WeightOnWheels so 0→1 transition fires isGearTouchGround (touchdown impulse) - cannonShellsCount decrements while Gun=1, triggering isFireCannonShells - Module-level singleton: init_winwing() / shutdown_winwing() hid_probe.py (new) - Standalone HID diagnostic tool: enumerate all WinWing interfaces, probe feature reports, read input, attempt buzz test - Used during development to confirm HID output reports are inaccessible (validates SimAppPro UDP bridge as the correct approach) main.py - Initialise WinWing bridge in _initialize_device_connection() when system setting winwingSimAppPro=true telemffb/sim/aircraft_base.py - Forward telem_data to WinWingSink.update() on every on_telemetry() call telemffb/SystemSettingsDialog.py - New "WinWing" tab: enable checkbox, SimAppPro status probe (netstat), 3-phase test button (touchdown → cannon → speedbrakes) telemffb/SetupWizard.py - New page 5 _WinWingPage: detects SimAppPro on port 16536, checkbox to enable bridge, saves winwingSimAppPro system setting immediately - Summary page shows WinWing bridge state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(winwing): SimAppPro UDP bridge for WinWing handle vibration motors
…unway separation Engine voices (prop_phys / cyl_phys) on the shaker were masking runway rumble: continuous high-amplitude impulse trains saturate the device, leaving no headroom for the runway0_delayed constant force. Solving this with cross-effect ducking would couple engine intensity to ground state and break muscle memory. Instead, split the runway rumble into perceptually distinct streams that each are a pure function of their own telemetry: - runway_carrier (live, stick) / runway_carrier_delayed (rear, shaker): EMA-smoothed |HPF| as a steady low-frequency rumble texture. Sits well below the engine blade-pass band. - runway_impulse (live, stick) / runway_impulse_delayed (rear, shaker): per-bump one-shots fired by an edge-triggered peak detector. Sharp 28 Hz transients punch through any continuous tone, so a runway joint reads as a clear thump even at full engine power. Both sub-streams carry the existing wheelbase/ground-speed front->rear delay and the roughness-based dimmer. No cross-coupling: same RPM = same engine feel, same joint = same thump. Engine voice gains rebalanced (prop_phys 0.9 -> 0.55, cyl_phys 0.7 -> 0.4) so the shaker shares headroom between continuous engine tone and transient bumps rather than letting engine voices dominate. Replaces effect names runway0 / runway0_delayed / runway_bump0 / runway_bump1 across whitelist, profiles, default routing, legacy shaker pack, setup presets, IL-2 dispose call, and migration script categories. BMS taxi-bump path fires both impulse variants simultaneously since BMS lacks wheelbase telemetry.
feat(shaker): split runway rumble into carrier + impulse for engine/runway separation
In ``engine_rumble_mode = "physics"`` (the default), the FFB stick was silent during taxi, takeoff and cruise on prop and rotor aircraft because ``prop_phys_*``, ``cyl_phys_*`` and ``rotor_phys_*`` only declared ``type:shaker`` layers in ``effect_routes_default.json``. The EffectRouter resolves layers per device, and a missing ``type:joystick`` layer makes ``HapticEffect.physics().start()`` a no-op on the stick — the ``ffb_rhino.physics()`` periodic-fallback never gets a chance to run. The legacy ``synthesis`` voices (``prop_rpm0-1``, ``rotor_rpm0-1``, …) already routed to both devices, which masked the gap behind a mode switch. This change brings the physics path in line so the default mode delivers stick haptics too. Joystick gains mirror the legacy ratios: - ``rotor_phys_main`` 1.0 → stick 0.5 - ``rotor_phys_tail`` 0.6 → stick 0.3 - ``prop_phys_*`` 0.55 → stick 0.4 - ``cyl_phys_*`` 0.4 → stick 0.25 ``runway_carrier_delayed`` / ``runway_impulse_delayed`` deliberately stay shaker-only — they're the rear copy of the front-stick effects and the wheelbase-delay illusion depends on them not being mirrored back. The migration script's ``_synthesise_layers`` heuristic for physics voices is updated alongside so a re-run preserves the new layout. ``build()`` still preserves hand-tuned routes, so existing custom packs are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(routing): route physics-mode engine/rotor voices to joystick
The bass-shaker child process never received its aircraft config because
``defaults.xml`` carries no ``<shaker>true</shaker>`` markers. The XML
reader filtered by ``[shaker="true"]`` matched zero entries, leaving the
shaker's ``Helicopter`` instance with the Python class defaults
(``engine_rotor_rumble_enabled = False`` etc). The handler's update
functions early-returned and disposed all rumble voices before they could
play -- the user reported full silence on the shaker for any helicopter.
Add a ``_read_device_chain`` helper that, for the shaker, expands to
``[joystick, shaker]`` (joystick as base layer, shaker overrides last)
and rewrite the eight device-filtered XML readers to consume that chain:
- ``read_xml_file`` -- per-setting ``<defaults>`` blocks
- ``read_default_class_data`` -- class defaults + removal markers
- ``read_models_data`` -- model-specific defaults / userconfig
- ``read_user_sim_data`` -- user sim-level overrides
- ``read_user_class_data`` -- user class-level overrides
- ``read_models`` -- UI model list
- ``get_craft_attributes`` -- UI craft attribute set
- ``apply_validvalue_overrides_from_root`` -- UI slider validvalues
Joystick / pedals / collective / trimwheel reads are unchanged
(``_read_device_chain('joystick') -> ['joystick']`` etc.). Write paths
are untouched -- the shaker still does not write its own userconfig
entries.
The two-layer architecture is now clean: the config layer (defaults.xml
+ userconfig) decides WHAT effects the aircraft handler triggers
(device-agnostic), and the routing pack
(``effect_routes_default.json``) is the single source of truth for
WHICH device actually plays each one. Per-device gain / freq / osc_type
all live in the routing pack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three log calls used Unicode arrows / em-dashes (``→``, ``—``)
that crash the stdout handler on a default-locale Windows console
(``cp1252``):
UnicodeEncodeError: 'charmap' codec can't encode character '→'
The WinWing bridge startup line was the visible offender on every run;
the two ``ffb_shaker`` lines only fire on rare paths (unknown osc_type
warning + the layered-start self-test) but would crash the same way.
Replace ``->`` for the WinWing line and ``-`` for the em-dashes. Pure
cosmetic, no behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… icons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude/interesting solomon c6b57b
fix(icon): convert icon_shaker to outline style matching other device…
The router's HapticEffect subclass overrode start/stop/destroy/started/id to only operate on routed sub_handles, never delegating to super(). Force- only effects (spring/damper/inertia/friction) populate the inherited _h_effect via setCondition() but never go through periodic/constant — their start() became a silent no-op, so force trim and stick centering were dead. Lifecycle methods now also call super() when _h_effect is populated, so the inherited slot fires for force-only effects while routed-only no- match cases stay silent (gated on _h_effect being None for that path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(routing): wire force-only effects through inherited _h_effect slot
Three changes to fix 97 test failures from the huey/wip merge: 1. aircraft_base.py: Initialize _telem_data/_last_telem_data as BaseTelemetryData() instead of plain dicts. MixIn methods like _sim_is_msfs() access .src via attribute access which fails on dicts but works on BaseTelemetryData. 2. ffb_rhino.py: Guard __del__ against double-destroy by checking _h_effect directly and inlining destroy logic. Prevents GC-triggered __del__ from calling destroy() again after explicit destroy() already cleared _h_effect. 3. ffb_router.py: Null out sub._h_effect after explicit destroy to prevent __del__ from re-entering destroy on garbage collection. Reduces test failures from 99 to 2 (both pre-existing on refactor branch).
Three lines of effects.dispose() were at module level instead of inside the else block, causing an IndentationError on import.
…debase Walks all .py files in the repo and compiles them to catch SyntaxError/ IndentationError that slip through because Python only checks at import time. Modules never imported by existing tests can hide these issues.
…c_initialization The if G.system_settings.enableVPConfStartup block (lines 942-947) had zero indentation, placing it outside init_async() instead of inside it. Added 8 spaces to match the function body.
…read.run() Line 54 (if self._raw_packet_hook is not None) had zero indentation, placing it outside the try block. Indented to 12 spaces so it executes within the UDP receive loop's try/except.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges @89Huey89's bass shaker integration and generalised multi-device effect routing onto the
refactorbranch.Originally developed across iterative PRs on https://github.com/89Huey89/vpforce-telemffb-Shaker — now rebased and merged clean onto our
refactorbase.Status: Untested end-to-end. All code compiles, tests pass locally, but hardware-in-the-loop validation with an actual shaker setup has not been performed.
What it does
Bass Shaker as first-class device type
--type shakerdevice type alongside joystick/pedals/collective/trimwheel--type shaker --child), receives telemetry via IPCShakerSynth) replaces Rhino HID — outputs to user-selected soundcard viasounddevice/PortAudioattack_ms/decay_ms) for transient shapingGeneralised effect routing (device-agnostic)
telemffb/routing/— new module:EffectRouter,EffectRoute,RouteLayer,DirectionPolicyeffect_routes_default.json, user-overridableeffect_routes_user.json)type:shaker,type:joystick,id:<dev>,pos:<tag>— any device can be addressedWinWing handle vibration bridge
ffb_winwing.py— SimAppPro UDP protocol for WinWing handle motorsUI & onboarding
SetupWizard— first-run onboarding with bundled presetsShakerCalibrationWizard— guided calibration flowDeviceInventoryTab/EffectRoutingDialog— visual device inventory and routing matrixEffectTestDialog— interactive per-effect testerShakerWaveformWidget— waveform visualization in calibration UISystemSettingsDialog— shaker tab, layer editor, profile managementAircraft-base changes
Data files
data/effect_routes_default.json— curated default routing (1148 lines, full aircraft whitelist)data/shaker_effects_default.json— default shaker effect profilesdata/shaker_profiles_default.json— default shaker calibration profilesdata/device_inventory_default.json— default device inventorydata/setup_presets.json— SetupWizard preset configurationsTests
test_ffb_router.py,test_routing.py,test_phase_locked_impulses.py,test_routes_coverage.py,test_shaker_whitelist.pyDocumentation
docs/SHAKER.md— comprehensive overview (functionality, architecture, limitations)docs/ROUTING.md— routing system designdocs/shaker-mvp/,docs/shaker-polish-layers/,docs/shaker-cleanups-noise/,docs/shaker-envelope-override/Key new dependencies
sounddevice— audio I/O for shaker output (added torequirements.txt)Files changed
48 Python/XML/JSON files, ~14,600 lines added, ~115 removed. Major new modules:
ffb_shaker.py,ffb_winwing.py,shaker_synth.py,routing/,SetupWizard.py,ShakerCalibrationWizard.py,EffectRoutingDialog.py,DeviceInventoryTab.py,EffectTestDialog.py.This has not been tested with real hardware. Before merging to main: