Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Update] Support Tundra Tracker quirks #9

Merged
merged 2 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions BridgeApp/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions BridgeApp/app_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ def build_pattern_setting_layout(key: str, pattern_list: [str], pattern_config:
k=key + KEY_VIB_PATTERN, size=15, readonly=True, enable_events=True)],
[sg.Text("Strength:"),
sg.Text("Min:", pad=0),
sg.Spin([num for num in range(0, 100)], pattern_config.str_min, pad=0,
sg.Spin([num for num in range(0, 101)], pattern_config.str_min, pad=0,
key=key + KEY_VIB_STR_MIN, enable_events=True),
sg.Text("Max:", pad=0),
sg.Spin([num for num in range(0, 100)], pattern_config.str_max, pad=0,
sg.Spin([num for num in range(0, 101)], pattern_config.str_max, pad=0,
key=key + KEY_VIB_STR_MAX, enable_events=True)],
[sg.Text("Speed:", size=6, tooltip=speed_tooltip),
sg.Slider(range=(1, 32), size=(13, 10), default_value=pattern_config.speed, tooltip=speed_tooltip,
Expand All @@ -108,7 +108,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))],
Expand All @@ -118,7 +118,7 @@ def tracker_row(self, tracker_id, tracker_serial, tracker_model):
sg.Button("Identify", k=(KEY_BTN_TEST, tracker_serial), tooltip="Send a 500ms pulse to the tracker")],
[sg.Text(" "),
sg.Text("Battery threshold:", tooltip="Disables vibration bellow this battery level"),
sg.Spin([num for num in range(0, 90)], battery_threshold, pad=0,
sg.Spin([num for num in range(0, 91)], battery_threshold, pad=0,
key=(KEY_BATTERY_THRESHOLD, tracker_serial), enable_events=True),
sg.Text("%", pad=0),
sg.VSeparator(),
Expand Down
107 changes: 104 additions & 3 deletions BridgeApp/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,56 @@ def __init__(self, config: AppConfig, tracker: VRTracker, pulse_function, batter
self.strength_delta: float = 0.0
self.last_str_set_time = time.time()

# 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
# try to avoid exceeding the maximum duration of one haptic pulse.
# Exceeding 100% strength requires extending a pulse into the next loop
# iteration. 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 device time unit
self.hack_pulse_mult_to_ms = 0
# HACK: maximum allowed pulse duration in milliseconds
# Exceeding the limit likely results in sporadic or glitchy vibration
# instead of a max strength (as continuous as possible) pulse.
self.hack_pulse_limit_ms = 0
# HACK: if true, the warning for a pulse exceeding the pulse limit has
# been shown. Only shown once to avoid console spam.
self.hack_pulse_limit_exceeded = False
# 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)
Expand All @@ -43,10 +92,51 @@ 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%
if strength > 0:
self.pulse_function(self.tracker.index, int(strength * self.interval_ms))
# So we pulse every self.interval_ms (e.g. 50) ms. That means a
# self.interval_ms (50/etc) ms pulse would be 100%.
pulse_length = strength * self.interval_ms
# Don't cap strength to 1.0 as velocity calculation can easily
# exceed 1. Also, some may rely on overriding the multiplier
# to adjust for different timescales, etc.

# Check if there's a queued force pulse
if start_time < self.hack_pulse_force_stop_time:
# Convert to milliseconds
# (Don't cap this so if a pulse limit is specified, the
# leftover pulse carries over.)
force_pulse_duration = (self.hack_pulse_force_stop_time - start_time) * 1000
# Pick the biggest number for pulse_length
pulse_length = max(pulse_length, force_pulse_duration)

# If a maximum pulse length is specified...
if self.hack_pulse_limit_ms > 0:
# ...and we exceed it...
if pulse_length > self.hack_pulse_limit_ms:
if not self.hack_pulse_limit_exceeded:
print(f"[VibrationManager] {round(pulse_length, 2)} ms pulse exceeds {self.interval_ms} ms limit for {self.tracker.serial}, extending next pulse [this warning won't repeat]")
self.hack_pulse_limit_exceeded = True

# Carry over any excess to the next loop iteration by
# queuing it as a forced pulse. Don't subtract anything as
# this pulse's time.sleep() also reduces the next pulse.
self.force_pulse(pulse_length)
# Do a max length pulse now
pulse_length = self.hack_pulse_limit_ms

# 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 all else 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)
Expand Down Expand Up @@ -77,4 +167,15 @@ 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)
# NOTE: This is also used by run() to handle lengths that exceed
# the maximum.
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))