From 622475d585755afb62c223358b3eb850a9ed7d33 Mon Sep 17 00:00:00 2001 From: digitalfox Date: Sun, 24 Mar 2024 21:37:33 -0400 Subject: [PATCH] [Update] Support Tundra Tracker with timing quirks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support the Tundra Tracker using microseconds instead of milliseconds for the legacy, deprecated "triggerHapticPulse()" method. This also comes with an upper limit of around 4000 µs (4 ms), so the update loop must run more frequently to stay within that maximum duration for a single pulse. Set Tundra Trackers back to a multiplier of 1.0, matching the behavior of the Vive Trackers. This might need replaced with support for the IVRInput system in place of the deprecated triggerHapticPulse() function. Warn if attempting to set a strength exceeding 100% (1.0). It should not happen as that should have no effect - the pulse duration lasts longer than the delay before the next pulse. However, multipliers resulting in excess of 100% offer a way to adust for odd trackers without needing to rebuild the released app on Windows. Haptic Tundrakes, anyone? ...no? Tested setup: * 4x pancake vibration motors Thinner than LRAs shipped with Tundra IO Boards, fits in stock base * 4x Tundra Trackers Left/right foot, waist, chest * Default multiplier, default strengths Also tested Velocity strength set to 60% - 100% * Proximity and Velocity set to "Linear" pattern "Throb" works too, but feels a bit weak --- BridgeApp/app_config.py | 7 ++-- BridgeApp/app_gui.py | 2 +- BridgeApp/app_runner.py | 93 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/BridgeApp/app_config.py b/BridgeApp/app_config.py index 47337db..a1c445e 100644 --- a/BridgeApp/app_config.py +++ b/BridgeApp/app_config.py @@ -21,12 +21,11 @@ def __init__(self, index: int, model: str, serial: str): @staticmethod def get_multiplier(model: str): - if model.startswith("Tundra"): - return 100.0 if model.startswith("VIVE Controller"): return 100.0 - else: - return 1.0 + + # Vive Tracker, Tundra Tracker, etc + return 1.0 # This is a definition class for storing user settings per tracker diff --git a/BridgeApp/app_gui.py b/BridgeApp/app_gui.py index 593eb14..41e4101 100644 --- a/BridgeApp/app_gui.py +++ b/BridgeApp/app_gui.py @@ -107,7 +107,7 @@ def tracker_row(self, tracker_id, tracker_serial, tracker_model): vib_multiplier = dev_config.multiplier_override battery_threshold = dev_config.battery_threshold - multiplier_tooltip = "1.0 for Vive trackers\n150 for Tundra trackers\n200 for Vive Wand\n400 for Index c." + multiplier_tooltip = "Additional strength multiplier\nCompensates for different trackers\n1.0 for default (Vive/Tundra Tracker)\n200 for Vive Wand\n400 for Index c." print(f"[GUI] Adding tracker: {string}") layout = [[sg.Text(string, pad=(0, 0))], diff --git a/BridgeApp/app_runner.py b/BridgeApp/app_runner.py index ff3925e..87396ff 100644 --- a/BridgeApp/app_runner.py +++ b/BridgeApp/app_runner.py @@ -21,8 +21,53 @@ def __init__(self, config: AppConfig, tracker: VRTracker, pulse_function, batter self.strength: float = 0.0 # Should be treated as a value between 0 and 1 self.strength_delta: float = 0.0 self.last_str_set_time = time.time() - + # If true, the warning for strength exceeding 1.0 (100%) has been shown + # Resets on changing strength + self.shown_strength_exceed_max = False + + # Some devices (e.g. Tundra Trackers) use microseconds instead of + # milliseconds for the legacy triggerHapticPulse() function, but then + # limit you to around 3999µs per pulse. + # + # TODO: That number is probably not exact; check with an oscilloscope. + # + # See https://steamcommunity.com/app/358720/discussions/0/405693392914144440/ + # + # In these situations, the update loop needs to run much faster to + # avoid exceeding the maximum duration of one haptic pulse. Any manual + # pulses also need to be spread out over time to not exceed the limit. + # + # TODO: Try switching from "triggerHapticPulse()" to IVRInput + # triggerHapticPulse() is deprecated as per Valve: + # * https://github.com/ValveSoftware/openvr/blob/v2.2.3/headers/openvr.h#L2431-L2433 + # * https://github.com/ValveSoftware/openvr/blob/v2.2.3/headers/openvr.h#L5216-L5218 + # + # This might be specific to Tundra Trackers vs. Vive Trackers, as + # Tundra ships their IO Expansion board with a LRA/haptic actuator + + # Assume trackers that behave as expected self.interval_ms = 50 # millis + + # Hack: multiplier to convert from milliseconds to the actual unit + self.hack_pulse_mult_to_ms = 0 + # Hack: maximum allowed pulse duration in milliseconds + self.hack_pulse_limit_ms = 0 + # Hack: when to stop a queued force pulse + self.hack_pulse_force_stop_time = 0 + + # Special-case as needed + if self.tracker.model.startswith("Tundra"): + # Tundra Tracker + # Works in microseconds + self.hack_pulse_mult_to_ms = 1 / 1000 + # Has a limit of roughly 4000 microseconds + self.hack_pulse_limit_ms = 4000 * self.hack_pulse_mult_to_ms + + if self.hack_pulse_limit_ms > 0: + # Apply pulse duration hack/workaround + self.interval_ms = self.hack_pulse_limit_ms + print(f"[VibrationManager] Using {self.interval_ms} ms pulse limit workaround for {self.tracker.serial}") + self.interval_s = self.interval_ms / 1000 # seconds self.vp = VibrationPattern(self.config) @@ -36,6 +81,7 @@ def set_strength(self, strength): self.strength_delta += abs(strength - self.strength) self.strength = strength self.last_str_set_time = time.time() + self.shown_strength_exceed_max = False def run(self): print(f"[VibrationManager] Thread started for {self.tracker.serial}") @@ -43,10 +89,40 @@ def run(self): while True: start_time = time.time() + pulse_length = 0 + strength = self.calculate_strength(start_time) - # So we pulse every 50 ms that means a 50 ms pulse would be 100% + # So we pulse every self.interval_ms (e.g. 50) ms. That means a + # self.interval_ms (50/etc) ms pulse would be 100%. if strength > 0: - self.pulse_function(self.tracker.index, int(strength * self.interval_ms)) + # Warn if strength exceeds 100% + if strength > 1 and not self.shown_strength_exceed_max: + print(f"[VibrationManager] Strength >100% ({round(strength * 100)}) for {self.tracker.serial}, multiplier too high?") + self.shown_strength_exceed_max = True + # Don't cap strength to 1.0 as some may rely on overriding + # the multiplier to adjust for different timescales, etc. + + pulse_length = strength * self.interval_ms + + # Check if there's a queued force pulse + if start_time < self.hack_pulse_force_stop_time: + # Convert to milliseconds + force_pulse_duration = (self.hack_pulse_force_stop_time - start_time) * 1000 + # Cap to self.interval_ms duration + force_pulse_capped = min(force_pulse_duration, self.interval_ms) + # Pick the biggest number for pulse_length + pulse_length = max(pulse_length, force_pulse_capped) + + # Convert to target unit of time if necessary + if self.hack_pulse_mult_to_ms: + pulse_length = pulse_length / self.hack_pulse_mult_to_ms + + # Convert to integer (after unit adjustment, for max precision) + pulse_length = int(pulse_length) + + # Trigger pulse if nonzero length requested + if pulse_length > 0: + self.pulse_function(self.tracker.index, pulse_length) sleep = max(self.interval_s - (time.time() - start_time), 0.0) time.sleep(sleep) @@ -77,4 +153,13 @@ def apply_multiplier(self, strength): * self.config.get_tracker_config(self.tracker.serial).multiplier_override) def force_pulse(self, length): - self.pulse_function(self.tracker.index, int(length * self.tracker.pulse_multiplier)) + if self.hack_pulse_limit_ms > 0: + # Add the pulse length in milliseconds to the current time in + # seconds, determining the new target time to stop + self.hack_pulse_force_stop_time = time.time() + (length / 1000) + else: + # Convert to target unit of time if necessary + if self.hack_pulse_mult_to_ms: + length = length / self.hack_pulse_mult_to_ms + # Trigger haptic pulse + self.pulse_function(self.tracker.index, int(length * self.tracker.pulse_multiplier))