11from __future__ import annotations
22
3- import asyncio
4- import logging
5- import signal
6- import sys
7- from pathlib import Path
3+ import asyncio
4+ import logging
5+ import signal
6+ import sys
7+ from pathlib import Path
88
9- import yaml
10- import argparse
9+ import yaml
10+ import argparse
1111
12- from vd_client import VDClient
13- from osc_sender import OSCSender
14- from oscquery_server import OSCQueryServer
15- from steamvr_overlay import SteamVROverlay
12+ from vd_client import VDClient
13+ from osc_sender import OSCSender
14+ from oscquery_server import OSCQueryServer
15+ from steamvr_overlay import SteamVROverlay
1616
17- logging .basicConfig (
18- level = logging .INFO ,
19- format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
20- )
21- logger = logging .getLogger ("main" )
17+ logging .basicConfig (
18+ level = logging .INFO ,
19+ format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
20+ )
21+ logger = logging .getLogger ("main" )
2222
23- CONFIG_PATH = Path (__file__ ).parent / "config.yaml"
23+ CONFIG_PATH = Path (__file__ ).parent / "config.yaml"
2424
2525
2626def parse_args ():
@@ -32,107 +32,107 @@ def parse_args():
3232 # Add more flags later (e.g. --config, --no-overlay)
3333 return parser .parse_args ()
3434
35- class BatteryState :
36- def __init__ (self , low_threshold : float ):
37- self .left : float | None = None
38- self .right : float | None = None
39- self .low_threshold = low_threshold
40- self ._lock = asyncio .Lock ()
41-
42- async def update (self , left : float | None , right : float | None ):
43- async with self ._lock :
44- self .left = left
45- self .right = right
46-
47- async def get (self ) -> dict :
48- async with self ._lock :
49- low = False
50- if self .left is not None and self .left < self .low_threshold :
51- low = True
52- if self .right is not None and self .right < self .low_threshold :
53- low = True
54- return {
55- "left" : self .left ,
56- "right" : self .right ,
57- "low" : low
58- }
59-
60- async def poll_sender (state : BatteryState , sender : OSCSender , interval : float ):
61- while True :
62- try :
63- s = await state .get ()
64- sender .send_battery (s ["left" ], s ["right" ], s ["low" ])
65- except Exception as e :
66- logger .error (f"Poll sender error: { e } " )
67- await asyncio .sleep (interval )
68-
69- async def main ():
70- # Load config
71- with open (CONFIG_PATH , "r" ) as f :
72- cfg = yaml .safe_load (f )
73-
74- vd_url = cfg ["vd_ws_url" ]
75- backoff = cfg ["vd_reconnect_backoff_s" ]
76- osc_ip = cfg ["osc_target_ip" ]
77- osc_port = cfg ["osc_target_port" ]
78- http_port = cfg ["oscquery_http_port" ]
79- service_name = cfg ["oscquery_service_name" ]
80- poll_interval = cfg ["poll_interval_s" ]
81- low_thresh = cfg ["low_battery_threshold" ]
82- overlay_enabled = cfg .get ("overlay_enabled" , False )
83-
84- logger .info ("Starting Quest Pro Battery Monitor prototype" )
85-
86- state = BatteryState (low_thresh )
87- sender = OSCSender (osc_ip , osc_port )
88-
89- def on_battery (left : float | None , right : float | None ):
90- asyncio .create_task (state .update (left , right ))
91-
92- vd = VDClient (vd_url , backoff , on_battery )
93- oscq = OSCQueryServer (http_port , service_name , osc_port , state .get )
94- overlay = SteamVROverlay (overlay_enabled )
95-
96- # Start components
97- await vd .start ()
98- await oscq .start ()
99- await overlay .start (state .get )
100-
101- poll_task = asyncio .create_task (poll_sender (state , sender , poll_interval ))
102-
103- logger .info ("Bridge running. Press Ctrl+C to stop." )
104-
105- # Graceful shutdown
106- stop_event = asyncio .Event ()
107-
108- def _signal_handler ():
109- logger .info ("Shutdown signal received" )
110- stop_event .set ()
111-
112- loop = asyncio .get_running_loop ()
113- for sig in (signal .SIGINT , signal .SIGTERM ):
114- try :
115- loop .add_signal_handler (sig , _signal_handler )
116- except NotImplementedError :
117- # Windows
118- pass
119-
120- await stop_event .wait ()
121-
122- # Cleanup
123- poll_task .cancel ()
124- try :
125- await poll_task
126- except asyncio .CancelledError :
127- pass
128- await vd .stop ()
129- await oscq .stop ()
130- await overlay .stop ()
131- logger .info ("Shutdown complete" )
132-
133- if __name__ == "__main__" :
134- parse_args () # handles --help / --version (exits on those)
135- try :
136- asyncio .run (main ())
137- except KeyboardInterrupt :
138- pass
35+ class BatteryState :
36+ def __init__ (self , low_threshold : float ):
37+ self .left : float | None = None
38+ self .right : float | None = None
39+ self .low_threshold = low_threshold
40+ self ._lock = asyncio .Lock ()
41+
42+ async def update (self , left : float | None , right : float | None ):
43+ async with self ._lock :
44+ self .left = left
45+ self .right = right
46+
47+ async def get (self ) -> dict :
48+ async with self ._lock :
49+ low = False
50+ if self .left is not None and self .left < self .low_threshold :
51+ low = True
52+ if self .right is not None and self .right < self .low_threshold :
53+ low = True
54+ return {
55+ "left" : self .left ,
56+ "right" : self .right ,
57+ "low" : low
58+ }
59+
60+ async def poll_sender (state : BatteryState , sender : OSCSender , interval : float ):
61+ while True :
62+ try :
63+ s = await state .get ()
64+ sender .send_battery (s ["left" ], s ["right" ], s ["low" ])
65+ except Exception as e :
66+ logger .error (f"Poll sender error: { e } " )
67+ await asyncio .sleep (interval )
68+
69+ async def main ():
70+ # Load config
71+ with open (CONFIG_PATH , "r" ) as f :
72+ cfg = yaml .safe_load (f )
73+
74+ vd_url = cfg ["vd_ws_url" ]
75+ backoff = cfg ["vd_reconnect_backoff_s" ]
76+ osc_ip = cfg ["osc_target_ip" ]
77+ osc_port = cfg ["osc_target_port" ]
78+ http_port = cfg ["oscquery_http_port" ]
79+ service_name = cfg ["oscquery_service_name" ]
80+ poll_interval = cfg ["poll_interval_s" ]
81+ low_thresh = cfg ["low_battery_threshold" ]
82+ overlay_enabled = cfg .get ("overlay_enabled" , False )
83+
84+ logger .info ("Starting Quest Pro Battery Monitor prototype" )
85+
86+ state = BatteryState (low_thresh )
87+ sender = OSCSender (osc_ip , osc_port )
88+
89+ def on_battery (left : float | None , right : float | None ):
90+ asyncio .create_task (state .update (left , right ))
91+
92+ vd = VDClient (vd_url , backoff , on_battery )
93+ oscq = OSCQueryServer (http_port , service_name , osc_port , state .get )
94+ overlay = SteamVROverlay (overlay_enabled )
95+
96+ # Start components
97+ await vd .start ()
98+ await oscq .start ()
99+ await overlay .start (state .get )
100+
101+ poll_task = asyncio .create_task (poll_sender (state , sender , poll_interval ))
102+
103+ logger .info ("Bridge running. Press Ctrl+C to stop." )
104+
105+ # Graceful shutdown
106+ stop_event = asyncio .Event ()
107+
108+ def _signal_handler ():
109+ logger .info ("Shutdown signal received" )
110+ stop_event .set ()
111+
112+ loop = asyncio .get_running_loop ()
113+ for sig in (signal .SIGINT , signal .SIGTERM ):
114+ try :
115+ loop .add_signal_handler (sig , _signal_handler )
116+ except NotImplementedError :
117+ # Windows
118+ pass
119+
120+ await stop_event .wait ()
121+
122+ # Cleanup
123+ poll_task .cancel ()
124+ try :
125+ await poll_task
126+ except asyncio .CancelledError :
127+ pass
128+ await vd .stop ()
129+ await oscq .stop ()
130+ await overlay .stop ()
131+ logger .info ("Shutdown complete" )
132+
133+ if __name__ == "__main__" :
134+ parse_args () # handles --help / --version (exits on those)
135+ try :
136+ asyncio .run (main ())
137+ except KeyboardInterrupt :
138+ pass
0 commit comments