1616 poll_for_token ,
1717 refresh_tokens ,
1818)
19+ from intercomclient .telemetry import TelemetryClient
1920from intercomclient .token_store import TokenStatus , TokenStore
2021
2122logging .basicConfig (level = logging .INFO )
@@ -26,6 +27,7 @@ class PiClient:
2627 def __init__ (self , config : Config ):
2728 self .config = config
2829 self .token_store = TokenStore (config )
30+ self .telemetry = TelemetryClient (config , self .token_store )
2931 self .pc : RTCPeerConnection | None = None
3032 self .ws = None
3133 self .running = True
@@ -136,8 +138,14 @@ async def setup_peer_connection(self):
136138 async def on_connectionstatechange ():
137139 if pc is not self .pc :
138140 return # stale event from a replaced PC
139- LOG .info ("WebRTC state: %s" , pc .connectionState )
140- if pc .connectionState == "failed" :
141+ state = pc .connectionState
142+ LOG .info ("WebRTC state: %s" , state )
143+ if state == "connected" :
144+ await asyncio .to_thread (self .telemetry .send , "streaming" )
145+ elif state == "failed" :
146+ await asyncio .to_thread (
147+ self .telemetry .send , "error" , "WebRTC connection failed" , "WARNING"
148+ )
141149 # Reset the PC so we're ready for the next offer.
142150 # The signaling WebSocket stays alive — don't close it here.
143151 await self .setup_peer_connection ()
@@ -184,6 +192,7 @@ async def signaling_loop(self):
184192
185193 await self .setup_peer_connection ()
186194 self ._register_ice_handler ()
195+ await asyncio .to_thread (self .telemetry .send , "connected" )
187196
188197 async for message in ws :
189198 data = json .loads (message )
@@ -205,6 +214,9 @@ async def signaling_loop(self):
205214 LOG .warning (
206215 "Camera unavailable (%s), sending answer without video" , e
207216 )
217+ await asyncio .to_thread (
218+ self .telemetry .send , "error" , str (e ), "WARNING"
219+ )
208220
209221 answer = await self .pc .createAnswer ()
210222
@@ -236,25 +248,39 @@ async def signaling_loop(self):
236248 LOG .info (
237249 "Viewer disconnected; resetting peer connection for new offer"
238250 )
251+ await asyncio .to_thread (
252+ self .telemetry .send , "disconnected" , "Viewer disconnected"
253+ )
239254 await self .setup_peer_connection ()
240255 self ._register_ice_handler ()
256+ await asyncio .to_thread (self .telemetry .send , "connected" )
241257
242258 # =========================
243259 # Lifecycle
244260 # =========================
245261
262+ async def _heartbeat_loop (self ):
263+ while self .running :
264+ await asyncio .sleep (30 )
265+ await asyncio .to_thread (self .telemetry .send , "heartbeat" )
266+
246267 async def run (self ):
268+ asyncio .create_task (self ._heartbeat_loop ())
247269 while self .running :
248270 try :
249271 await self .ensure_valid_tokens ()
250272 await self .signaling_loop ()
251273 except Exception as e :
252274 LOG .exception ("Client error: %s" , e )
275+ await asyncio .to_thread (self .telemetry .send , "error" , str (e ), "ERROR" )
253276 await asyncio .sleep (5 )
254277
255278 async def shutdown (self ):
256279 LOG .info ("Shutting down client..." )
257280 self .running = False
281+ await asyncio .to_thread (
282+ self .telemetry .send , "disconnected" , "Client shutting down"
283+ )
258284
259285 if self .pc :
260286 await self .pc .close ()
0 commit comments