Skip to content

Commit 4be8a7b

Browse files
update telop gui (#134)
1 parent 8d5cdcd commit 4be8a7b

1 file changed

Lines changed: 93 additions & 48 deletions

File tree

utama_core/team_controller/src/debug_utils/telop_gui.py

Lines changed: 93 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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
7978
IS_YELLOW = True
8079
LOOP_HZ = 60
8180
MAX_VEL = 0.1
81+
FAST_VEL = 0.5
8282
MAX_ANG_VEL = 1
83+
CONNECTION_TIMEOUT = 0.5 # seconds without a response before a robot is marked disconnected
8384

8485
BG = "#1a1a1a"
8586
SURFACE = "#2a2a2a"
@@ -90,7 +91,6 @@ def send_robot_commands(self):
9091
KICK_C = "#d94a4a"
9192
ON_C = "#4ad97a"
9293
BALL_C = "#d9a84a"
93-
HEAT_C = "#d94a4a" # Red when dribbler near/at thermal limit
9494
DUMMY_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("\nSending 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

Comments
 (0)