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