Skip to content

Commit

Permalink
[Update] Dynamic tracker loading and vibration patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
Z4urce committed Jan 18, 2024
1 parent b865dc2 commit 1e014a8
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 50 deletions.
1 change: 1 addition & 0 deletions BridgeApp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class AppConfig(BaseModel):
tracker_to_osc: Dict[str, str] = {}
global_vibration_intensity: int = 100
global_vibration_cooldown: int = 100
global_vibration_pattern: str = "None"

@staticmethod
def load():
Expand Down
91 changes: 58 additions & 33 deletions BridgeApp/gui.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,104 @@
import PySimpleGUI as sg
import webbrowser

import config
from config import AppConfig
from pattern import VibrationPattern

WINDOW_NAME = "Haptic Pancake Bridge v0.1.1"
WINDOW_NAME = "Haptic Pancake Bridge v0.2.0"

KEY_REC_IP = '-RECIP-'
KEY_REC_PORT = '-RECPORT-'
KEY_BTN_APPLY = '-BTNAPPLY-'
KEY_REC_IP = '-REC-IP-'
KEY_REC_PORT = '-REC-PORT-'
KEY_BTN_APPLY = '-BTN-APPLY-'
KEY_BTN_REFRESH = '-BTN-REFRESH'
KEY_VIB_STR = '-VIBSTR-'
KEY_VIB_STR = '-VIB-STR-'
KEY_VIB_PATTERN = '-VIB-PATTERN-'
KEY_OPEN_URL = '-OPENURL'
KEY_OSC_STATUS_BAR = '-OSC-STATUS-BAR-'
KEY_LAYOUT_TRACKERS = '-LAYOUT-TRACKERS-'
KEY_OSC_ADDRESS = '-ADDRESS-OF-'
KEY_BTN_TEST = '-BTN-TEST'




class GUIRenderer:
def __init__(self, config: AppConfig, tracker_test_event, restart_osc_event, refresh_trackers_event):
def __init__(self, app_config: AppConfig, tracker_test_event, restart_osc_event, refresh_trackers_event):
sg.theme('DarkAmber') # Add a touch of color
self.tracker_test_event = tracker_test_event
self.restart_osc_event = restart_osc_event
self.refresh_trackers_event = refresh_trackers_event
self.config = config
self.config = app_config
self.window = None
self.trackers = []
self.osc_status_bar = sg.StatusBar('', key=KEY_OSC_STATUS_BAR)
# self.tracker_layout = []
# self.tracker_frame = sg.Column(self.tracker_layout, key=KEY_LAYOUT_TRACKERS)
self.osc_status_bar = sg.Text('', key=KEY_OSC_STATUS_BAR)
self.tracker_frame = sg.Column([], key=KEY_LAYOUT_TRACKERS)
self.layout = []
self.build_layout()

def build_layout(self):
self.layout = [
[sg.Text('OSC Listener settings:', font='_ 14')],
[sg.Text("Address:"), sg.InputText(self.config.osc_address, key=KEY_REC_IP, size=16), sg.Text("Port:"),
[sg.Text("Address:"),
sg.InputText(self.config.osc_address, k=KEY_REC_IP, size=16, tooltip="IP Address. Default is 127.0.0.1"),
sg.Text("Port:", tooltip="UDP Port. Default is 9001"),
sg.InputText(self.config.osc_receiver_port, key=KEY_REC_PORT, size=16),
sg.Button("Apply", key=KEY_BTN_APPLY)],
[self.osc_status_bar],
sg.Button("Apply", key=KEY_BTN_APPLY, tooltip="Apply and restart OSC server.")],
[sg.Text("Server status:"), self.osc_status_bar],
[self.small_vertical_space()],
[sg.Text('Haptic settings:', font='_ 14')],
[sg.Text("Vibration Intensity:"),
sg.Slider(range=(1, 100), size=(25, 10), default_value=self.config.global_vibration_intensity,
sg.Slider(range=(1, 100), size=(31, 10), default_value=self.config.global_vibration_intensity,
orientation='horizontal', key=KEY_VIB_STR, enable_events=True)],
[sg.Text("Vibration Pattern:", size=14),
sg.Drop(VibrationPattern.VIB_PATTERN_LIST, self.config.global_vibration_pattern,
k=KEY_VIB_PATTERN, size=37, readonly=True, enable_events=True)],
[self.small_vertical_space()],
[sg.Text('Trackers found:', font='_ 14')],
[self.tracker_frame],
]

@staticmethod
def small_vertical_space():
return sg.Text('', font=('AnyFont', 1), auto_size_text=True)

def tracker_row(self, tracker_id, tracker_serial, tracker_model):
string = f"⚫ {tracker_serial} {tracker_model}"
default_text = self.config.tracker_to_osc[tracker_serial] \
if tracker_serial in self.config.tracker_to_osc else "/avatar/parameters/..."
print(f"[GUI] Adding tracker: {string}")
layout = [[sg.Text(string, pad=(0, 0))],
[sg.Text(" "), sg.Text("OSC Address:"),
sg.InputText(default_text, k=(KEY_OSC_ADDRESS, tracker_serial), enable_events=True, size=32,
tooltip="OSC Address"),
sg.Button("Test", k=(KEY_BTN_TEST, tracker_id), tooltip="Send a 200ms pulse to the tracker")]]

row = [sg.pin(sg.Col(layout, key=('-ROW-', tracker_id)))]
return row

def add_tracker(self, tracker_id, tracker_serial, tracker_model):
if tracker_serial in self.trackers:
print(f"[GUI] Tracker {tracker_serial} is already on the list. Skipping...")
return

row = [self.tracker_row(tracker_id, tracker_serial, tracker_model)]
if self.window is not None:
self.window.extend_layout(self.window[KEY_LAYOUT_TRACKERS], row)
else:
self.tracker_frame.layout(row)

self.trackers.append(tracker_serial)
string = f"{tracker_serial} {tracker_model}"
print(f"[GUI] Adding tracker: {string}")
default_text = self.config.tracker_to_osc[tracker_serial] \
if tracker_serial in self.config.tracker_to_osc else '/avatar/parameters/...'
self.layout.append(
[sg.Text(string),
sg.InputText(default_text, key=f"ADDRESS_OF={tracker_serial}", enable_events=True),
sg.Button("Test", key=f"TEST_ID={tracker_id}")])

def add_message(self, message):
self.layout.append([sg.HSep()])
self.layout.append([sg.Text(message, text_color='red')])

def add_footer(self):
self.layout.append([self.small_vertical_space()])
# self.layout.append([sg.Button("Refresh Tracker List", key=KEY_BTN_REFRESH)])
self.layout.append([sg.Button("Refresh Tracker List", key=KEY_BTN_REFRESH)])
self.layout.append([sg.HSep()])
self.layout.append(
[sg.Text("Made by Z4urce", enable_events=True, font='Default 8 underline', key=KEY_OPEN_URL)])

def show_error_window(self, error_message):
sg.popup_ok(error_message)

def update_osc_status_bar(self, message, is_error=False):
text_color = 'red' if is_error else 'green'
if self.window is None:
Expand All @@ -97,8 +120,8 @@ def run(self):
if event == sg.WIN_CLOSED or event == 'Exit': # if user closes window or clicks cancel
print("[GUI] Closing application.")
return False
if event.startswith("TEST_ID="):
self.tracker_test_event(int(event.split("=")[1]))
if event[0] == KEY_BTN_TEST:
self.tracker_test_event(event[1])
if event == KEY_BTN_APPLY:
self.restart_osc_event()
if event == KEY_BTN_REFRESH:
Expand All @@ -109,19 +132,21 @@ def run(self):
return True

def update_values(self, values):
# print(f"Values: {values}")
if values is None or values[KEY_VIB_STR] is None:
return

# Update Tracker OSC Addresses
for tracker in self.trackers:
tracker_key = f"ADDRESS_OF={tracker}"
if tracker_key in values:
self.config.tracker_to_osc[tracker] = values[tracker_key]
key = (KEY_OSC_ADDRESS, tracker)
if key in values:
self.config.tracker_to_osc[tracker] = values[key]

# Update OSC Addresses
self.config.osc_address = values[KEY_REC_IP]
self.config.osc_receiver_port = int(values[KEY_REC_PORT])

# Update vibration intensity
# Update vibration intensity and pattern
self.config.global_vibration_intensity = int(values[KEY_VIB_STR]) if KEY_VIB_STR in values else 0
self.config.global_vibration_pattern = values[KEY_VIB_PATTERN]
self.config.save()
20 changes: 10 additions & 10 deletions BridgeApp/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import traceback
import platform
from pattern import VibrationPattern
from osc import VRChatOSCReceiver
from config import AppConfig
from tracker import OpenVRTracker
Expand All @@ -9,6 +10,7 @@
osc_receiver: VRChatOSCReceiver = None
config: AppConfig = None
gui: GUIRenderer = None
vp: VibrationPattern = None


def main():
Expand All @@ -25,6 +27,9 @@ def main():
gui = GUIRenderer(config, pulse_test, restart_osc_server, refresh_tracker_list)
print("[Main] GUI initialized")

global vp
vp = VibrationPattern(config)

# Start the OSC receiver thread
global osc_receiver
osc_receiver = VRChatOSCReceiver(config, param_received, gui.update_osc_status_bar)
Expand All @@ -35,15 +40,9 @@ def main():
global vr
vr = OpenVRTracker()

print("[Main] OpenVR initialized" if vr.is_alive() else "[Main] OpenVR is not alive")

# Add trackers to GUI
refresh_tracker_list()

# Report errors to GUI is any exists
if not vr.is_alive():
gui.add_message("Error: Could not connect to Steam VR. Please restart the app.")

# Add footer
gui.add_footer()

Expand All @@ -52,8 +51,9 @@ def main():


# Adapter functions
def pulse_test(id):
vr.pulse(id, 200)
def pulse_test(tracker_id):
print(f"[Main] Pulse test for {tracker_id} executed.")
vr.pulse(tracker_id, 200)


def restart_osc_server():
Expand All @@ -70,16 +70,16 @@ def refresh_tracker_list():

# Debug tracker (Uncomment this for debug purposes)
# gui.add_tracker(99, "T35T-53R1AL", "Test Model 1.0")

print("[Main] Tracker list refreshed")


def param_received(address, value):
# address is the OSC address
# value is the floating value (0..1) that determines how intense the feedback should be
pulse_length: int = int(vp.apply_pattern(value * config.global_vibration_intensity))
for key in config.tracker_to_osc.keys():
if config.tracker_to_osc[key] == address:
vr.pulse_by_serial(key, int(value * config.global_vibration_intensity))
vr.pulse_by_serial(key, pulse_length)


if __name__ == '__main__':
Expand Down
27 changes: 27 additions & 0 deletions BridgeApp/pattern.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import math
import time

from config import AppConfig


class VibrationPattern:
VIB_PATTERN_LIST = ["None", "SineSlow", "Sine", "SineFast"]

def __init__(self, app_config: AppConfig):
self.config = app_config

def apply_pattern(self, value):
pattern = self.config.global_vibration_pattern
if pattern == self.VIB_PATTERN_LIST[1]:
return self.get_sine_value(1) * value
if pattern == self.VIB_PATTERN_LIST[2]:
return self.get_sine_value(2) * value
if pattern == self.VIB_PATTERN_LIST[3]:
return self.get_sine_value(4) * value
return value

@staticmethod
def get_sine_value(speed):
# Use the sine function to map the current time to a value between 0 and 1
result = (math.sin(time.time() * speed) + 1) / 2 # Map from [-1, 1] to [0, 1]
return max(0, min(result, 1))
21 changes: 14 additions & 7 deletions BridgeApp/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@ def __init__(self):
self.devices: [VRTracker] = []
self.vr = None

def try_init_openvr(self):
if self.vr is not None:
return True
try:
self.init_openvr()
self.vr = openvr.init(openvr.VRApplication_Background)
self.devices: [VRTracker] = []
print("[OpenVRTracker] Successfully initialized.")
return True
except:
pass

def init_openvr(self):
self.vr = openvr.init(openvr.VRApplication_Background)
self.devices: [VRTracker] = []
print("[OpenVRTracker] Failed to initialize OpenVR.")
return False

def query_devices(self):
poses = self.vr.getDeviceToAbsoluteTrackingPose(openvr.TrackingUniverseStanding, 0, openvr.k_unMaxTrackedDeviceCount)
if not self.try_init_openvr():
return self.devices

poses = self.vr.getDeviceToAbsoluteTrackingPose(openvr.TrackingUniverseStanding, 0,
openvr.k_unMaxTrackedDeviceCount)
self.devices.clear()
for i in range(openvr.k_unMaxTrackedDeviceCount):
if poses[i].bPoseIsValid and self.vr.getTrackedDeviceClass(i) == openvr.TrackedDeviceClass_GenericTracker:
Expand Down

0 comments on commit 1e014a8

Please sign in to comment.