Skip to content

Commit ffeff98

Browse files
authored
Merge pull request #33 from digitalf0x/ft-param-debug
[feat] Debug window for OSC/WebSocket input, beta3
2 parents 02dd264 + 488e0db commit ffeff98

9 files changed

Lines changed: 216 additions & 14 deletions

BridgeApp/app_gui.py

Lines changed: 203 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from app_config import AppConfig, PatternConfig
55
from app_pattern import VibrationPattern
66

7-
WINDOW_NAME = "Haptic Pancake Bridge v0.8.0-beta.2"
7+
WINDOW_NAME = "Haptic Pancake Bridge v0.8.0-beta.3"
88
WINDOW_ICON = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpVIrDhYVcchQnezgB+JYqlgEC6Wt0KqDyaVf0KQhSXFxFFwLDn4sVh1cnHV1cBUEwQ8QZwcnRRcp8X9NoUWMB8f9eHfvcfcOEOplpppdEUDVLCMZi4qZ7Kroe0UvBjEEPyYlZurx1GIaruPrHh6+3oV5lvu5P0efkjMZ4BGJI0w3LOIN4tlNS+e8TxxkRUkhPieeMOiCxI9clx1+41xossAzg0Y6OU8cJBYLHSx3MCsaKvEMcUhRNcoXMg4rnLc4q+Uqa92TvzCQ01ZSXKc5ihiWEEcCImRUUUIZFsK0aqSYSNJ+1MU/0vQnyCWTqwRGjgVUoEJq+sH/4He3Zn56ykkKRIHuF9v+GAN8u0CjZtvfx7bdOAG8z8CV1vZX6sDcJ+m1thY6Avq3gYvrtibvAZc7wPCTLhlSU/LSFPJ54P2MvikLDNwC/jWnt9Y+Th+ANHW1fAMcHALjBcped3l3T2dv/55p9fcD3S9y0apk9h0AAAAGYktHRAD/AP8A/6C9p5MAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfoCxYXCzDoJVaPAAACuElEQVQ4y2WTTW8bdRDGfzO767f1xnGcJiSNVNoKqMoJgRBCwifuIHFFuSDRTwDi2CNfgC/gGxfElV6ockEISAJBiAJ5KVnqxk7idbx+ie39DwcngaqH0Wj0HJ756ZmRjz7940Ym0nCe1DMVnArZRbn/d+85bcMJ6744GqrUzYFimIAamMF5P8GCAC1GqAMDlFk3qItKQ9WsrmaoGd32LjYeMeg0edj4mOP9Hzj4/ku2v77PJO3MtPZj1GYmYlb31c1ch2ct5uZW6XZikpN9Rr02v377BenRNsuvvkfa2sUP5wlKFbJhDy1FmAm+GpiD6XkfRFGDfD7infc/p9XcobTwGbmoRpq2sKBI8VqNXjemUCijAr6YXa2kCL74RHOrEORYufkW5vk4haJlOAQFyDLUAAeqzhBn5IOQwMsxGfUo5MqMBwm++IjLmPQTovk1PFFGyVPCygpqhphdIBiUS4t0zg5ZWrqL84RcWL2KraTgVPEWb5KmLQIvT+bsWYThIKF1+BO9XpPDg+9YufU2raPfqCy/zOnJHvlyFcmVyMhYq65h/yGAOGA64cmjBxzHW/Tbf5Ec/c5Z6xFh+RphtESvfUAn3mFh+Q5yEbuYoZc3oAbVxZc4T2KiuRcYnP7N3dc/JCzVZjpGLiiyv/kVl6bqDPnk3o51ksc0n25SrFwn85TxdIRXiBi7Mc5Tpm5CYX6F5uGP5Mo1BoMON974AK8YzRCycUpn9yHd5i9M0xO6/2xzGm9ynsSEpRrZ8Ixee4+9b+5TrqzSP96buV8ieA5eefMeMh7Re/IzleqLyGSEOkcn3iKJt+i3/qR2612GpzGlcBFPfdSBL8bG/MLtulMhfO06mQoWBLMIRXCesnZn+sxnLtkUTzww2/AxWw+8fMOJ1NUv4Lzn39lXIVOu5kAFJ7rhnK3/C07bcJ2GHOyzAAAAAElFTkSuQmCC'
99

1010
# If changing order, also change update_oscquery_state()
@@ -62,6 +62,7 @@
6262
KEY_BTN_TEST = '-BTN-TEST-'
6363
KEY_BTN_CALIBRATE = '-BTN-CALIBRATE-'
6464
KEY_BTN_ADD_EXTERNAL = '-BTN-ADD-EXTERNAL-'
65+
KEY_BTN_DEBUG = '-BTN-DEBUG-'
6566
KEY_BATTERY_THRESHOLD = '-BATTERY-'
6667
KEY_START_WITH_STEAMVR = '-START-WITH-STEAMVR-'
6768
KEY_AUTOSTART_STATUS_BAR = '-AUTOSTART-STATUS-BAR-'
@@ -87,6 +88,16 @@
8788
# Resaving config
8889
KEY_SAVE_TO_CONFIG = '-FAUX-SAVE-EVENT-'
8990

91+
# Debug window
92+
KEY_DEBUG_BTN_CLEAR = '-DEBUG-BTN-CLEAR-'
93+
KEY_DEBUG_BTN_COPY = '-DEBUG-BTN-COPY-'
94+
KEY_DEBUG_BTN_REFRESH = '-DEBUG-BTN-REFRESH-'
95+
KEY_DEBUG_CHK_AUTOREFRESH = '-DEBUG-CHK-AUTOREFRESH-'
96+
KEY_DEBUG_INPUT_FILTER = '-DEBUG-INPUT-FILTER-'
97+
KEY_DEBUG_TBL_ADDRESS= '-DEBUG-TBL-ADDRESS-'
98+
KEY_DEBUG_TIMER_REFRESH = '-DEBUG-TIMER-REFRESH-'
99+
DEBUG_TIMER_REFRESH_MS = 0.5 * 1000
100+
90101
# If changing this, also update app_config.py!
91102
DEFAULT_THEME="DarkAmber"
92103

@@ -111,6 +122,7 @@ def __init__(self, app_config: AppConfig, tracker_test_event,
111122
self.config = app_config
112123
self.shutting_down = False
113124
self.window = None
125+
self.debug_popup = None
114126
self.layout_dirty = False
115127
self.trackers = []
116128

@@ -143,6 +155,8 @@ def build_layout(self):
143155
# Ideally these would be updated to maintain contrast
144156
self.theme_color_bad = "red"
145157
self.theme_color_good = "lime"
158+
# Help with alignment
159+
small_button_size = 6
146160

147161
self.layout = []
148162

@@ -173,7 +187,7 @@ def build_layout(self):
173187
# Replace with a simple Text string instead
174188
self.server_port_input = sg.InputText(self.config.server_port, key=KEY_REC_PORT, size=13)
175189
self.server_port_auto = sg.Text("[automatic]", size=13, visible=False)
176-
self.server_apply_btn = sg.Button("Apply", key=KEY_BTN_APPLY, tooltip="Apply and restart server.", disabled=self.cache_server_apply_disabled)
190+
self.server_apply_btn = sg.Button("Apply", key=KEY_BTN_APPLY, size=small_button_size, tooltip="Apply and restart server.", disabled=self.cache_server_apply_disabled)
177191

178192
self.no_data_timeout = sg.Spin(
179193
[num for num in range(1, 601)], self.config.no_data_timeout, size=4, pad=((0,3),(0,0)),
@@ -185,8 +199,10 @@ def build_layout(self):
185199
[sg.Checkbox("Start minimized", default=self.config.start_minimized, key=KEY_START_MINIMIZED, enable_events=True)],
186200
[sg.Text("Theme:", justification='right', size=7),
187201
sg.InputCombo(LIST_THEME, self.config.theme, key=KEY_THEME, readonly=True, enable_events=True, tooltip="Change app theme.\nCAUTION: Some themes are hard to read. Be ready to reset."),
188-
sg.Button("Reset", key=KEY_BTN_THEME_RESET, tooltip=f"Reset theme to default ({DEFAULT_THEME})")],
189-
[sg.Text('Server settings:', font='_ 14')],
202+
sg.Button("Reset", key=KEY_BTN_THEME_RESET, size=small_button_size, tooltip=f"Reset theme to default ({DEFAULT_THEME})")],
203+
[sg.Text('Server settings:', font='_ 14'),
204+
sg.Push(),
205+
sg.Button("Debug", key=KEY_BTN_DEBUG, size=small_button_size)],
190206
[sg.Text("Type:", justification='right', size=7),
191207
sg.InputCombo(LIST_SERVER_TYPE, LIST_SERVER_TYPE[self.config.server_type], key=KEY_SERVER_TYPE, readonly=True, enable_events=True),
192208
sg.pin(self.server_oscquery_chkbox)],
@@ -205,7 +221,10 @@ def build_layout(self):
205221
sg.Text("(seconds)")],
206222
# Padding from "Stop stuck haptics" checkbox removes the need for this:
207223
#[self.small_vertical_space()],
208-
[sg.Text('Devices:', font='_ 14'), self.tracker_status_bar, sg.Push(), sg.Button("Refresh", key=KEY_BTN_REFRESH)],
224+
[sg.Text('Devices:', font='_ 14'),
225+
self.tracker_status_bar,
226+
sg.Push(),
227+
sg.Button("Refresh", key=KEY_BTN_REFRESH, size=small_button_size)],
209228
# add_external_button],
210229
[self.tracker_frame],
211230
[sg.HSep()],
@@ -517,6 +536,8 @@ def run(self):
517536
self.setup_tracker_preset(event[1])
518537
elif event == KEY_BTN_ADD_EXTERNAL:
519538
self.add_external_event(values[KEY_BTN_ADD_EXTERNAL])
539+
elif event == KEY_BTN_DEBUG:
540+
self.popup_debug_params()
520541
elif event == KEY_BTN_APPLY:
521542
self.restart_osc_event()
522543
elif event == KEY_BTN_THEME_RESET:
@@ -607,6 +628,22 @@ def popup_pick_address_preset(self, tracker_serial):
607628
window.close()
608629
return None
609630

631+
def popup_debug_params(self):
632+
"""GUI to pick a tracker location preset"""
633+
if self.debug_popup:
634+
# Don't try to open twice
635+
return
636+
637+
self.debug_popup = DebugAddressPopup()
638+
# Separate object creation from event loop so object gets assigned
639+
# This is needed to pass parameters along
640+
self.debug_popup.show_popup()
641+
self.debug_popup = None
642+
643+
def debug_params_received(self, address, value):
644+
if self.debug_popup:
645+
self.debug_popup.params_received(address, value)
646+
610647
def setup_tracker_preset(self, tracker_serial):
611648
# Find value if possible
612649
address_widget = (KEY_OSC_ADDRESS, tracker_serial)
@@ -694,3 +731,164 @@ def update_pattern_config(self, values, index: int, key: str):
694731
except ValueError:
695732
pass
696733
self.config.pattern_config_list[index].speed = int(values[key + KEY_VIB_SPEED])
734+
735+
class DebugAddressPopup:
736+
def __init__(self):
737+
"""GUI to show parameter addresses and values"""
738+
button_size = 6
739+
self.timer_active = False
740+
self.param_list = {}
741+
self.debug_table = sg.Table([], headings=["Address", "Value"], justification='left', key=KEY_DEBUG_TBL_ADDRESS, enable_events=True, expand_x = True, expand_y = True, col_widths=[25, 5], auto_size_columns=False, select_mode=sg.TABLE_SELECT_MODE_BROWSE)
742+
# Set a specific column width to show more of address
743+
744+
self.debug_autorefresh_chkbox = sg.Checkbox("Auto-refresh",
745+
default=True, key=KEY_DEBUG_CHK_AUTOREFRESH, enable_events=True,
746+
tooltip="Automatically refresh list of addresses and values.", pad=0)
747+
self.debug_filter_input = sg.InputText('haptic', key=KEY_DEBUG_INPUT_FILTER, size=25, expand_x=True)
748+
749+
self.debug_stats = sg.Text('Refresh to see addresses')
750+
751+
self.debug_copy_button = sg.Button("Copy Row", key=KEY_DEBUG_BTN_COPY, tooltip="Copy selected Address and Value to clipboard.", disabled=True)
752+
753+
self.debug_layout = [
754+
[sg.Text("Filter:"),
755+
self.debug_filter_input,
756+
sg.Button("Refresh", key=KEY_DEBUG_BTN_REFRESH, size=button_size, tooltip="Refresh list of addresses and values.")],
757+
[self.debug_autorefresh_chkbox,
758+
sg.Push(),
759+
self.debug_copy_button,
760+
sg.Button("Clear", key=KEY_DEBUG_BTN_CLEAR, size=button_size, tooltip="Clear list of known addresses.")],
761+
[self.debug_table],
762+
[sg.HSep()],
763+
[self.debug_stats,
764+
sg.Push(),
765+
sg.Sizegrip()],
766+
]
767+
768+
@property
769+
def autorefresh_active(self):
770+
return self.debug_autorefresh_chkbox.get()
771+
772+
def clear(self):
773+
self.param_list = {}
774+
# Preemptively clear selection too
775+
self.debug_table.update(select_rows=[])
776+
self.refresh()
777+
778+
def copy_row(self):
779+
selected_rows = self.debug_table.get()
780+
if not len(selected_rows):
781+
return
782+
783+
# Get selected row text (assume first row if multiple are selected)
784+
selected_row = self.debug_table.Values[selected_rows[0]]
785+
print(f"[DebugAddress] Copying row to clipboard: {selected_row}")
786+
# NOTE: On Linux, Tkinter requires the app to stay running to keep text
787+
# Modern desktop environments resolve this with clipboard managers.
788+
sg.clipboard_set(selected_row)
789+
790+
def params_received(self, address, value):
791+
self.param_list[address] = value
792+
793+
def refresh(self):
794+
## Intentionally out of order to test sorting
795+
#self.param_list["/avatar/parameter/TestIndex"] = 3
796+
#self.param_list["/avatar/parameter/HapticChest"] = 0.3
797+
#self.param_list["/avatar/parameter/HapticHips"] = 0.3567465434
798+
#self.param_list["/avatar/parameter/WillGiveYouUp"] = False
799+
#self.param_list["/avatar/parameter/WillLetYouDown"] = False
800+
#self.param_list["/avatar/parameter/WillRunAroundAndDesertYou"] = False
801+
#self.param_list["/avatar/parameter/PineappleOnPizza"] = True
802+
803+
filter_text = self.debug_filter_input.get().casefold()
804+
# Find addresses by matching substring
805+
param_list_filtered = [(k, v) for k, v in self.param_list.items() if filter_text in k.casefold()]
806+
# A-Z
807+
param_list_filtered.sort()
808+
809+
# Get selected rows
810+
selected_rows = self.debug_table.get()
811+
selected_addr_text = None
812+
if len(selected_rows):
813+
# Check first selected row
814+
selected_addr_text = self.debug_table.Values[selected_rows[0]][0]
815+
816+
# Update
817+
self.debug_table.update(param_list_filtered)
818+
self.debug_stats.update(f"{len(param_list_filtered)} matching addresses, {len(self.param_list)} total")
819+
820+
# Restore selected rows
821+
if selected_addr_text:
822+
# Find new index
823+
try:
824+
# Find index of first matching address
825+
new_row_index = [row[0] for row in self.debug_table.Values].index(selected_addr_text)
826+
# Wrap in list (only a single item)
827+
self.debug_table.update(select_rows=[new_row_index])
828+
except ValueError:
829+
# No value found
830+
pass
831+
832+
# Refresh again if enabled
833+
self.schedule_autorefresh()
834+
835+
def run_loop(self):
836+
while True:
837+
# Wait for event
838+
event, values = self.window.read()
839+
if event == KEY_DEBUG_BTN_CLEAR:
840+
self.clear()
841+
self.refresh()
842+
elif event == KEY_DEBUG_BTN_COPY:
843+
self.copy_row()
844+
elif event == KEY_DEBUG_CHK_AUTOREFRESH:
845+
if self.autorefresh_active:
846+
self.refresh()
847+
elif event == KEY_DEBUG_BTN_REFRESH:
848+
self.refresh()
849+
elif event == KEY_DEBUG_TBL_ADDRESS:
850+
self.update_select_status()
851+
elif event == KEY_DEBUG_TIMER_REFRESH:
852+
self.timer_active = False
853+
if self.autorefresh_active:
854+
self.refresh()
855+
else:
856+
self.window.close()
857+
return None
858+
859+
def schedule_autorefresh(self):
860+
if self.autorefresh_active and not self.timer_active:
861+
self.timer_active = True
862+
self.window.timer_start(DEBUG_TIMER_REFRESH_MS, key=KEY_DEBUG_TIMER_REFRESH, repeating=False)
863+
864+
def show_popup(self):
865+
self.window = sg.Window("Debug server input", self.debug_layout, keep_on_top=True, finalize=True, modal=True, icon=WINDOW_ICON, use_default_focus=False)
866+
# Py/FreeSimpleGUI doesn't have real modal windows, so set keep_on_top
867+
# to help avoid losing the dialog box.
868+
869+
# Center window on mouse cursor
870+
loc_mid_x = self.window.size[0] / 2
871+
loc_mid_y = self.window.size[1] / 2
872+
mouse_loc = self.window.mouse_location()
873+
self.window.move(int(mouse_loc[0] - loc_mid_x), int(mouse_loc[1] - loc_mid_y))
874+
# Allow resizing
875+
self.window.set_resizable(True, True)
876+
# Lock in current window size as minimum
877+
self.window.set_min_size(self.window.size)
878+
# Expand from minimum size in X and Y direction
879+
self.window.size = (self.window.size[0] + 100, self.window.size[1] + 100)
880+
881+
# Make it easy to clear default filter
882+
self.debug_filter_input.update(select=True)
883+
self.debug_filter_input.set_focus()
884+
885+
# Close on Escape key
886+
self.window.bind("<Escape>", "-ESCAPE-")
887+
888+
self.schedule_autorefresh()
889+
890+
self.run_loop()
891+
892+
def update_select_status(self):
893+
"""Update status of Copy Row button based on row selection"""
894+
self.debug_copy_button.update(disabled=not len(self.debug_table.get()))

BridgeApp/main.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,18 @@ def add_external_target(external_type):
135135

136136

137137
def param_received(address, value):
138-
# value is the floating value (0..1) that determines how intense the feedback should be
139-
for serial, tracker_config in config.tracker_config_dict.items():
140-
if address in tracker_config.address_list:
141-
vr.set_strength(serial, value)
138+
# Pass all types to debug viewer
139+
gui.debug_params_received(address, value)
140+
141+
# Only allow floating point values for haptics
142+
try:
143+
float_value = float(value)
144+
# floating value (0..1) that determines how intense feedback should be
145+
for serial, tracker_config in config.tracker_config_dict.items():
146+
if address in tracker_config.address_list:
147+
vr.set_strength(serial, float_value)
148+
except ValueError:
149+
pass
142150

143151
def setup_autostart(autostart: bool):
144152
vr.setup_autostart(autostart=autostart)

BridgeApp/server_osc.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@ def shutdown(self):
4545
self.print_status("Shutdown completed.")
4646

4747
def event_received(self, address, osc_value):
48-
try:
49-
float_value = float(osc_value)
50-
self.param_received_event(address, float_value)
51-
except ValueError:
52-
pass
48+
self.param_received_event(address, osc_value)
5349

5450
def run(self):
5551
# Don't double-start
870 KB
Loading
-840 KB
Binary file not shown.

0 commit comments

Comments
 (0)