@@ -53,7 +53,6 @@ class RealRobotController:
5353 def __init__ (self , is_team_yellow = True , n_friendly = 2 ):
5454
5555 self .n_friendly = n_friendly
56- self ._dribbler_seconds : dict = {}
5756
5857 print ("\n [DUMMY CONTROLLER ENABLED]" )
5958 print ("No serial/network/hardware required.\n " )
@@ -74,12 +73,14 @@ def send_robot_commands(self):
7473
7574
7675# --- Config ---
77- ROBOT_ID = 1
78- N_FRIENDLY = 2
76+ ROBOT_ID = 0
77+ N_FRIENDLY = 4
7978IS_YELLOW = True
8079LOOP_HZ = 60
8180MAX_VEL = 0.1
81+ FAST_VEL = 0.5
8282MAX_ANG_VEL = 1
83+ CONNECTION_TIMEOUT = 0.5 # seconds without a response before a robot is marked disconnected
8384
8485BG = "#1a1a1a"
8586SURFACE = "#2a2a2a"
@@ -90,7 +91,6 @@ def send_robot_commands(self):
9091KICK_C = "#d94a4a"
9192ON_C = "#4ad97a"
9293BALL_C = "#d9a84a"
93- HEAT_C = "#d94a4a" # Red when dribbler near/at thermal limit
9494DUMMY_C = "#d9a84a" # Orange for dummy warning
9595
9696
@@ -107,6 +107,12 @@ def __init__(self, root: tk.Tk, controller: RealRobotController):
107107
108108 self .dribble = False
109109 self .chip = False
110+ self ._shift_held = False
111+ self .active_robot_id = ROBOT_ID
112+
113+ # Per-robot connection tracking
114+ self ._last_seen : dict [int , float ] = {}
115+ self ._last_has_ball : dict [int , bool ] = {}
110116
111117 # Impulse flags
112118 self ._kick_impulse = False
@@ -289,9 +295,9 @@ def _build_ui(self):
289295
290296 self ._fb_ball_vars : dict [int , tk .StringVar ] = {}
291297 self ._fb_ball_lbls : dict [int , tk .Label ] = {}
292- self ._fb_heat_vars : dict [int , tk .StringVar ] = {}
293- self ._fb_heat_lbls : dict [int , tk .Label ] = {}
294298 self ._fb_status_vars : dict [int , tk .StringVar ] = {}
299+ self ._fb_rows : dict [int , tk .Frame ] = {}
300+ self ._fb_id_lbls : dict [int , tk .Label ] = {}
295301 for i in range (N_FRIENDLY ):
296302 row = tk .Frame (fb_frame , bg = SURFACE )
297303 row .pack (fill = "x" , padx = 8 , pady = 4 )
@@ -319,33 +325,28 @@ def _build_ui(self):
319325 )
320326 ball_lbl .pack (side = "left" , padx = (8 , 0 ))
321327
322- heat_var = tk .StringVar (value = "heat: 0%" )
323- heat_lbl = tk .Label (
324- row ,
325- textvariable = heat_var ,
326- bg = SURFACE ,
327- fg = MUTED ,
328- font = ("monospace" , 11 ),
329- width = 10 ,
330- anchor = "w" ,
331- )
332- heat_lbl .pack (side = "left" , padx = (8 , 0 ))
333-
334328 status_var = tk .StringVar (value = "no data" )
335- tk .Label (
329+ status_lbl = tk .Label (
336330 row ,
337331 textvariable = status_var ,
338332 bg = SURFACE ,
339333 fg = MUTED ,
340334 font = ("monospace" , 10 ),
341335 anchor = "e" ,
342- ).pack (side = "right" )
336+ )
337+ status_lbl .pack (side = "right" )
338+
339+ # Click anywhere on the row to make this robot active.
340+ for widget in (row , id_lbl , ball_lbl , status_lbl ):
341+ widget .bind ("<Button-1>" , lambda e , rid = i : self ._set_active_robot (rid ))
343342
344343 self ._fb_ball_vars [i ] = ball_var
345344 self ._fb_ball_lbls [i ] = ball_lbl
346- self ._fb_heat_vars [i ] = heat_var
347- self ._fb_heat_lbls [i ] = heat_lbl
348345 self ._fb_status_vars [i ] = status_var
346+ self ._fb_rows [i ] = row
347+ self ._fb_id_lbls [i ] = id_lbl
348+
349+ self ._refresh_active_robot ()
349350
350351 # --- Command readout ---
351352 readout_frame = tk .Frame (self .root , bg = SURFACE , highlightbackground = BORDER , highlightthickness = 1 )
@@ -395,8 +396,40 @@ def _bind_keys(self):
395396 self .root .bind ("<KeyPress-B>" , lambda e : self ._press ("b" ))
396397 self .root .bind ("<KeyRelease-B>" , lambda e : self ._release ("b" ))
397398
399+ # Shift boost
400+ self .root .bind ("<KeyPress-Shift_L>" , lambda e : self ._set_shift (True ))
401+ self .root .bind ("<KeyRelease-Shift_L>" , lambda e : self ._set_shift (False ))
402+ self .root .bind ("<KeyPress-Shift_R>" , lambda e : self ._set_shift (True ))
403+ self .root .bind ("<KeyRelease-Shift_R>" , lambda e : self ._set_shift (False ))
404+
405+ # Number keys select the active robot
406+ for i in range (min (N_FRIENDLY , 10 )):
407+ self .root .bind (f"<KeyPress-{ i } >" , lambda e , rid = i : self ._set_active_robot (rid ))
408+
398409 self .root .bind ("<Escape>" , lambda e : self ._quit ())
399410
411+ def _set_shift (self , value : bool ):
412+ with self ._lock :
413+ self ._shift_held = value
414+
415+ def _set_active_robot (self , robot_id : int ):
416+ if robot_id < 0 or robot_id >= N_FRIENDLY :
417+ return
418+ with self ._lock :
419+ self .active_robot_id = robot_id
420+ self ._refresh_active_robot ()
421+
422+ def _refresh_active_robot (self ):
423+ with self ._lock :
424+ active = self .active_robot_id
425+ for rid , id_lbl in self ._fb_id_lbls .items ():
426+ if rid == active :
427+ id_lbl .configure (fg = ACTIVE )
428+ self ._fb_rows [rid ].configure (bg = BORDER )
429+ else :
430+ id_lbl .configure (fg = TEXT )
431+ self ._fb_rows [rid ].configure (bg = SURFACE )
432+
400433 def _press (self , key : str ):
401434 with self ._lock :
402435 # Check for auto-repeat
@@ -470,14 +503,17 @@ def _control_loop(self):
470503
471504 with self ._lock :
472505 keys = set (self .held )
506+ shift = self ._shift_held
507+ active_id = self .active_robot_id
473508 # Impulse capture
474509 kick = self ._kick_impulse # don't worry about kick cooldown, handled in controller.
475510 chip = self ._chip_impulse # don't worry about kick and chip at same time, handled in controller.
476511 self ._kick_impulse = False
477512 self ._chip_impulse = False
478513
479- fwd = MAX_VEL if "w" in keys else (- MAX_VEL if "s" in keys else 0.0 )
480- left = MAX_VEL if "a" in keys else (- MAX_VEL if "d" in keys else 0.0 )
514+ vel = FAST_VEL if shift else MAX_VEL
515+ fwd = vel if "w" in keys else (- vel if "s" in keys else 0.0 )
516+ left = vel if "a" in keys else (- vel if "d" in keys else 0.0 )
481517 ang = MAX_ANG_VEL if "q" in keys else (- MAX_ANG_VEL if "e" in keys else 0.0 )
482518
483519 cmd = RobotCommand (
@@ -491,39 +527,47 @@ def _control_loop(self):
491527
492528 responses = self .controller .get_robots_responses () or []
493529
494- self .controller .add_robot_commands (cmd , ROBOT_ID )
530+ for rid in range (N_FRIENDLY ):
531+ self .controller .add_robot_commands (cmd if rid == active_id else empty_command (), rid )
495532 self .controller .send_robot_commands ()
496533
534+ now = time .perf_counter ()
535+ for resp in responses :
536+ self ._last_seen [resp .id ] = now
537+ self ._last_has_ball [resp .id ] = resp .has_ball
538+
539+ feedback = [
540+ (
541+ i ,
542+ self ._last_has_ball .get (i , False ),
543+ i in self ._last_seen and (now - self ._last_seen [i ]) < CONNECTION_TIMEOUT ,
544+ )
545+ for i in range (N_FRIENDLY )
546+ ]
547+
497548 # Safely pass updates to the main thread via the queue
498549 self ._ui_queue .put (("readout" , cmd ))
499- if responses :
500- self ._ui_queue .put (("feedback" , responses ))
550+ self ._ui_queue .put (("feedback" , feedback ))
501551
502552 elapsed = time .perf_counter () - t0
503553 time .sleep (max (0.0 , dt - elapsed ))
504554
505- def _update_feedback (self , responses ):
506- from utama_core .team_controller .src .controllers .real .real_robot_controller import (
507- DRIBBLER_MAX_ON_SECONDS ,
508- )
509-
510- for resp in responses :
511- if resp .id not in self ._fb_ball_vars :
555+ def _update_feedback (self , feedback ):
556+ for robot_id , has_ball , connected in feedback :
557+ if robot_id not in self ._fb_ball_vars :
558+ continue
559+ if not connected :
560+ self ._fb_ball_vars [robot_id ].set ("ball: --" )
561+ self ._fb_ball_lbls [robot_id ].configure (fg = MUTED )
562+ self ._fb_status_vars [robot_id ].set ("no data" )
512563 continue
513- if resp . has_ball :
514- self ._fb_ball_vars [resp . id ].set ("ball: YES" )
515- self ._fb_ball_lbls [resp . id ].configure (fg = BALL_C )
564+ if has_ball :
565+ self ._fb_ball_vars [robot_id ].set ("ball: YES" )
566+ self ._fb_ball_lbls [robot_id ].configure (fg = BALL_C )
516567 else :
517- self ._fb_ball_vars [resp .id ].set ("ball: no" )
518- self ._fb_ball_lbls [resp .id ].configure (fg = MUTED )
519- self ._fb_status_vars [resp .id ].set ("connected" )
520-
521- # dribbler heat display
522- bucket = self .controller ._dribbler_seconds .get (resp .id , 0.0 )
523- pct = int (bucket / DRIBBLER_MAX_ON_SECONDS * 100 )
524- self ._fb_heat_vars [resp .id ].set (f"heat: { pct :3d} %" )
525- heat_colour = HEAT_C if pct >= 80 else (BALL_C if pct >= 50 else MUTED )
526- self ._fb_heat_lbls [resp .id ].configure (fg = heat_colour )
568+ self ._fb_ball_vars [robot_id ].set ("ball: no" )
569+ self ._fb_ball_lbls [robot_id ].configure (fg = MUTED )
570+ self ._fb_status_vars [robot_id ].set ("connected" )
527571
528572 def _update_readout (self , cmd : RobotCommand ):
529573 self ._metrics ["fwd" ].set (f"{ cmd .local_forward_vel :+.2f} " )
@@ -554,7 +598,8 @@ def _quit(self):
554598
555599 print ("\n Sending stop commands..." )
556600 for _ in range (10 ):
557- self .controller .add_robot_commands (empty_command (), ROBOT_ID )
601+ for rid in range (N_FRIENDLY ):
602+ self .controller .add_robot_commands (empty_command (), rid )
558603 self .controller .send_robot_commands ()
559604
560605 self .root .destroy ()
0 commit comments