2222import os
2323import pathlib
2424import time
25+ from collections import deque
2526from typing import Optional
2627
2728import modal
@@ -90,29 +91,31 @@ async def main() -> None:
9091 policy = Policy ()
9192 hits = misses = 0
9293
93- # When each raw state packet arrived, by seq, on our own clock. Portal pairs
94- # a frame and a state by the capture time the robot stamped, so the video's
95- # extra delay never shows up as a timestamp. Instead the fused observation
96- # fires later than the state, once the matching frame lands. obs-fire minus
97- # state-arrival is that extra video delay, one way.
94+ # When each raw state packet arrived, by seq, on our own clock.
9895 state_arrival : dict [int , float ] = {}
9996
97+ # Only ever hold the freshest observations. If decoding cannot keep up with
98+ # the frame rate, we drop the backlog and act on the newest frame instead of
99+ # working through stale ones. Acting on a stale observation is what would
100+ # defeat the point of Portal, and it shows up as latency that climbs far
101+ # above the network round trip.
102+ latest : deque = deque (maxlen = 2 )
103+ got_obs = asyncio .Event ()
104+
100105 def on_state (state : State ) -> None :
101106 state_arrival [int (state .values ["seq" ])] = time .monotonic ()
102107
103108 def on_observation (obs : Observation ) -> None :
104- nonlocal hits , misses
105- action = policy .get_action (obs ) # QR decode is fast, so run it inline
106- if action is None :
107- misses += 1
108- return
109- arrived = state_arrival .pop (int (action ["seq" ]), None )
109+ # Measure codec lag here, at arrival, so a decode backlog cannot distort
110+ # it. Portal pairs the frame and state on the capture time the robot
111+ # stamped, so the video's extra delay is not a timestamp gap. It is the
112+ # observation firing later than the state, once the frame lands. That gap
113+ # (obs arrival minus state arrival) is the one-way video-over-data cost.
114+ seq = int (obs .state ["seq" ])
115+ arrived = state_arrival .pop (seq , None )
110116 lag_us = int ((time .monotonic () - arrived ) * 1_000_000 ) if arrived is not None else 0
111- action ["codec_lag_us" ] = float (lag_us )
112- # in_reply_to_ts_us feeds Portal's built-in e2e metric the QR capture
113- # time, which turns that metric into a glass-to-glass number.
114- op .send_action (action , in_reply_to_ts_us = int (action ["t_capture_us" ]))
115- hits += 1
117+ latest .append ((obs , lag_us ))
118+ got_obs .set ()
116119
117120 op .on_state (on_state )
118121 op .on_observation (on_observation )
@@ -130,12 +133,34 @@ def on_observation(obs: Observation) -> None:
130133 print (f"[policy] robot '{ op .robot_identity ()} ' joined, controlling as '{ op .local_identity ()} '" )
131134
132135 stop_at = time .monotonic () + DURATION_S + 5.0 # outlast the robot a little
136+ last_log = time .monotonic ()
133137 try :
134138 while time .monotonic () < stop_at :
135- await asyncio .sleep (1.0 )
136- total = hits + misses
137- rate = (hits / total * 100.0 ) if total else 0.0
138- print (f"[policy] decoded={ hits } missed={ misses } decode_rate={ rate :.0f} %" )
139+ try :
140+ await asyncio .wait_for (got_obs .wait (), timeout = 1.0 )
141+ except asyncio .TimeoutError :
142+ pass
143+ got_obs .clear ()
144+
145+ if latest :
146+ obs , lag_us = latest [- 1 ] # freshest; any older frames are skipped
147+ latest .clear ()
148+ action = policy .get_action (obs )
149+ if action is None :
150+ misses += 1
151+ else :
152+ action ["codec_lag_us" ] = float (lag_us )
153+ # in_reply_to_ts_us feeds Portal's built-in e2e metric the QR
154+ # capture time, making that metric a glass-to-glass number.
155+ op .send_action (action , in_reply_to_ts_us = int (action ["t_capture_us" ]))
156+ hits += 1
157+
158+ now = time .monotonic ()
159+ if now - last_log >= 1.0 :
160+ total = hits + misses
161+ rate = (hits / total * 100.0 ) if total else 0.0
162+ print (f"[policy] decoded={ hits } missed={ misses } decode_rate={ rate :.0f} %" )
163+ last_log = now
139164 finally :
140165 print (f"[policy] decoded { hits } frames, disconnecting..." )
141166 await op .disconnect ()
0 commit comments