From ef30a481b860434cba261b6eb1da41070206cecd Mon Sep 17 00:00:00 2001 From: src24 Date: Wed, 15 Apr 2026 15:16:23 +0100 Subject: [PATCH 01/10] Add robot `has_ball` feedback --- .../controllers/real/real_robot_controller.py | 21 +- .../src/controllers/real/telop_gui.py | 334 ++++++++++++++++++ 2 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 utama_core/team_controller/src/controllers/real/telop_gui.py diff --git a/utama_core/team_controller/src/controllers/real/real_robot_controller.py b/utama_core/team_controller/src/controllers/real/real_robot_controller.py index c93ff98d..c20b5715 100644 --- a/utama_core/team_controller/src/controllers/real/real_robot_controller.py +++ b/utama_core/team_controller/src/controllers/real/real_robot_controller.py @@ -58,8 +58,21 @@ def __init__(self, is_team_yellow: bool, n_friendly: int): self._kicker_tracker: Dict[int, KickTrackerEntry] = {} def get_robots_responses(self) -> Optional[List[RobotResponse]]: - ### TODO: Not implemented yet - return None + responses = [] + while self._serial_port.in_waiting: + header = self._serial_port.read()[0] + if header != 0xAA: + continue + + robot_id = self._serial_port.read()[0] + length = self._serial_port.read()[0] + + data = self._serial_port.read(size=length) + responses.append(RobotResponse(robot_id, has_ball=data[0] & 0x01 != 0)) + footer = self._serial_port.read()[0] + assert footer == 0x55 + + return responses def send_robot_commands(self) -> None: """Sends the robot commands to the appropriate team (yellow or blue).""" @@ -71,10 +84,6 @@ def send_robot_commands(self) -> None: f"Only {len(self._assigned_mapping)} out of {self._n_friendly} robots have been assigned commands. Sending incomplete command packet." ) self._serial_port.write(self._out_packet) - self._serial_port.read_all() - # data_in = self._serial.read_all() - # print(data_in) - # TODO: add receiving feedback from the robots ### update kick and chip trackers. We persist the kick/chip command for KICKER_PERSIST_TIMESTEPS ### this feature is to combat packet loss and to ensure the robot does not kick within its cooldown period diff --git a/utama_core/team_controller/src/controllers/real/telop_gui.py b/utama_core/team_controller/src/controllers/real/telop_gui.py new file mode 100644 index 00000000..c2d80669 --- /dev/null +++ b/utama_core/team_controller/src/controllers/real/telop_gui.py @@ -0,0 +1,334 @@ +""" +Controls (keyboard or click): + W / S — forward / reverse + A / D — strafe left / right + Q / E — rotate CCW / CW + Space — kick + B — toggle dribbler + C — toggle chip + Esc — quit +""" + +import threading +import time +import tkinter as tk + +from utama_core.team_controller.src.controllers.real.real_robot_controller import ( + RealRobotController, + RobotCommand, + empty_command, +) + +# --- Config --- +ROBOT_ID = 1 +N_FRIENDLY = 2 +IS_YELLOW = True +LOOP_HZ = 60 +MAX_VEL = -0.1 +MAX_ANG_VEL = 1 + +BG = "#1a1a1a" +SURFACE = "#2a2a2a" +BORDER = "#3a3a3a" +TEXT = "#eeeeee" +MUTED = "#888888" +ACTIVE = "#4a90d9" +KICK_C = "#d94a4a" +ON_C = "#4ad97a" +BALL_C = "#d9a84a" + + +class TeleopGUI: + def __init__(self, root: tk.Tk, controller: RealRobotController): + self.root = root + self.controller = controller + self.held: set[str] = set() + self.dribble = False + self.chip = False + self._running = True + + root.title("Robot Teleop") + root.configure(bg=BG) + root.resizable(False, False) + + self._build_ui() + self._bind_keys() + + self._loop_thread = threading.Thread(target=self._control_loop, daemon=True) + self._loop_thread.start() + + root.protocol("WM_DELETE_WINDOW", self._quit) + + # ------------------------------------------------------------------ UI -- + + def _build_ui(self): + pad = dict(padx=12, pady=8) + + # --- Status bar --- + status_frame = tk.Frame(self.root, bg=BG) + status_frame.pack(fill="x", **pad) + + self._metrics = {} + for label, key in [("Forward", "fwd"), ("Left", "left"), ("Angular", "ang")]: + col = tk.Frame(status_frame, bg=SURFACE, padx=14, pady=8, highlightbackground=BORDER, highlightthickness=1) + col.pack(side="left", expand=True, fill="x", padx=4) + tk.Label(col, text=label, bg=SURFACE, fg=MUTED, font=("monospace", 10)).pack() + var = tk.StringVar(value="0.00") + tk.Label(col, textvariable=var, bg=SURFACE, fg=TEXT, font=("monospace", 18)).pack() + self._metrics[key] = var + + # --- WASD keypad --- + keypad = tk.Frame(self.root, bg=BG) + keypad.pack(**pad) + + layout = [ + [None, "Q", "W", "E", None], + [None, "A", "S", "D", None], + ] + self._key_buttons: dict[str, tk.Label] = {} + for row_i, row in enumerate(layout): + for col_i, k in enumerate(row): + if k is None: + tk.Frame(keypad, width=58, height=58, bg=BG).grid(row=row_i, column=col_i, padx=3, pady=3) + continue + btn = tk.Label( + keypad, + text=k, + width=3, + bg=SURFACE, + fg=TEXT, + font=("monospace", 16, "bold"), + highlightbackground=BORDER, + highlightthickness=1, + padx=10, + pady=10, + ) + btn.grid(row=row_i, column=col_i, padx=3, pady=3) + self._key_buttons[k.lower()] = btn + + # Arrow labels for Q/E + tk.Label(keypad, text="↺", bg=BG, fg=MUTED, font=("monospace", 10)).grid(row=2, column=1) + tk.Label(keypad, text="↻", bg=BG, fg=MUTED, font=("monospace", 10)).grid(row=2, column=3) + + # --- Kick button --- + kick_frame = tk.Frame(self.root, bg=BG) + kick_frame.pack(**pad) + self._kick_btn = tk.Label( + kick_frame, + text="SPACE — kick", + bg=SURFACE, + fg=TEXT, + font=("monospace", 13), + highlightbackground=BORDER, + highlightthickness=1, + padx=40, + pady=10, + ) + self._kick_btn.pack() + + # --- Toggle buttons --- + toggle_frame = tk.Frame(self.root, bg=BG) + toggle_frame.pack(**pad) + + self._dribble_btn = tk.Label( + toggle_frame, + text="B dribble: OFF", + bg=SURFACE, + fg=MUTED, + font=("monospace", 12), + highlightbackground=BORDER, + highlightthickness=1, + padx=16, + pady=8, + ) + self._dribble_btn.pack(side="left", padx=6) + self._dribble_btn.bind("", lambda e: self._toggle_dribble()) + + self._chip_btn = tk.Label( + toggle_frame, + text="C chip: OFF", + bg=SURFACE, + fg=MUTED, + font=("monospace", 12), + highlightbackground=BORDER, + highlightthickness=1, + padx=16, + pady=8, + ) + self._chip_btn.pack(side="left", padx=6) + self._chip_btn.bind("", lambda e: self._toggle_chip()) + + # --- Robot feedback --- + fb_outer = tk.Frame(self.root, bg=BG) + fb_outer.pack(fill="x", padx=12, pady=(0, 8)) + tk.Label(fb_outer, text="Robot Feedback", bg=BG, fg=MUTED, font=("monospace", 10)).pack(anchor="w") + + fb_frame = tk.Frame(fb_outer, bg=SURFACE, highlightbackground=BORDER, highlightthickness=1) + fb_frame.pack(fill="x") + + self._fb_ball_vars: dict[int, tk.StringVar] = {} + self._fb_ball_lbls: dict[int, tk.Label] = {} + self._fb_status_vars: dict[int, tk.StringVar] = {} + for i in range(N_FRIENDLY): + row = tk.Frame(fb_frame, bg=SURFACE) + row.pack(fill="x", padx=8, pady=4) + + id_lbl = tk.Label( + row, text=f"Robot {i}", bg=SURFACE, fg=TEXT, font=("monospace", 11, "bold"), width=8, anchor="w" + ) + id_lbl.pack(side="left") + + ball_var = tk.StringVar(value="ball: --") + ball_lbl = tk.Label( + row, textvariable=ball_var, bg=SURFACE, fg=MUTED, font=("monospace", 11), width=12, anchor="w" + ) + ball_lbl.pack(side="left", padx=(8, 0)) + + status_var = tk.StringVar(value="no data") + tk.Label(row, textvariable=status_var, bg=SURFACE, fg=MUTED, font=("monospace", 10), anchor="e").pack( + side="right" + ) + + self._fb_ball_vars[i] = ball_var + self._fb_ball_lbls[i] = ball_lbl + self._fb_status_vars[i] = status_var + + # --- Command readout --- + readout_frame = tk.Frame(self.root, bg=SURFACE, highlightbackground=BORDER, highlightthickness=1) + readout_frame.pack(fill="x", padx=12, pady=(0, 12)) + self._readout = tk.StringVar(value="RobotCommand(...)") + tk.Label( + readout_frame, + textvariable=self._readout, + bg=SURFACE, + fg=MUTED, + font=("monospace", 10), + justify="left", + anchor="w", + padx=10, + pady=8, + ).pack(fill="x") + + def _bind_keys(self): + movement = ["w", "a", "s", "d", "q", "e"] + for k in movement: + self.root.bind(f"", lambda e, k=k: self._press(k)) + self.root.bind(f"", lambda e, k=k: self._release(k)) + self.root.bind(f"", lambda e, k=k: self._press(k)) + self.root.bind(f"", lambda e, k=k: self._release(k)) + + self.root.bind("", lambda e: self._press("space")) + self.root.bind("", lambda e: self._release("space")) + self.root.bind("", lambda e: self._toggle_dribble()) + self.root.bind("", lambda e: self._toggle_chip()) + self.root.bind("", lambda e: self._quit()) + + def _press(self, key: str): + self.held.add(key) + self._refresh_key_visuals() + + def _release(self, key: str): + self.held.discard(key) + self._refresh_key_visuals() + + def _refresh_key_visuals(self): + for k, btn in self._key_buttons.items(): + if k in self.held: + btn.configure(bg=ACTIVE, fg="white", highlightbackground=ACTIVE) + else: + btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + if "space" in self.held: + self._kick_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) + else: + self._kick_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + + def _toggle_dribble(self): + self.dribble = not self.dribble + if self.dribble: + self._dribble_btn.configure(text="B dribble: ON", fg=ON_C, highlightbackground=ON_C) + else: + self._dribble_btn.configure(text="B dribble: OFF", fg=MUTED, highlightbackground=BORDER) + + def _toggle_chip(self): + self.chip = not self.chip + if self.chip: + self._chip_btn.configure(text="C chip: ON", fg=ON_C, highlightbackground=ON_C) + else: + self._chip_btn.configure(text="C chip: OFF", fg=MUTED, highlightbackground=BORDER) + + # --------------------------------------------------------- Control loop -- + + def _control_loop(self): + dt = 1.0 / LOOP_HZ + while self._running: + t0 = time.perf_counter() + + keys = set(self.held) + fwd = MAX_VEL if "w" in keys else (-MAX_VEL if "s" in keys else 0.0) + left = MAX_VEL if "a" in keys else (-MAX_VEL if "d" in keys else 0.0) + ang = MAX_ANG_VEL if "q" in keys else (-MAX_ANG_VEL if "e" in keys else 0.0) + kick = "space" in keys + + cmd = RobotCommand( + local_forward_vel=fwd, + local_left_vel=left, + angular_vel=ang, + kick=kick, + chip=self.chip, + dribble=self.dribble, + ) + + responses = self.controller.get_robots_responses() or [] + + self.controller.add_robot_commands(cmd, 0) + self.controller.add_robot_commands(cmd, 1) + self.controller.send_robot_commands() + print("Sending commands", cmd) + + self.root.after(0, self._update_readout, cmd) + if responses: + self.root.after(0, self._update_feedback, responses) + + elapsed = time.perf_counter() - t0 + time.sleep(max(0.0, dt - elapsed)) + + def _update_feedback(self, responses): + for resp in responses: + if resp.id not in self._fb_ball_vars: + continue + if resp.has_ball: + self._fb_ball_vars[resp.id].set("ball: YES") + self._fb_ball_lbls[resp.id].configure(fg=BALL_C) + else: + self._fb_ball_vars[resp.id].set("ball: no") + self._fb_ball_lbls[resp.id].configure(fg=MUTED) + self._fb_status_vars[resp.id].set("connected") + + def _update_readout(self, cmd: RobotCommand): + self._metrics["fwd"].set(f"{cmd.local_forward_vel:+.2f}") + self._metrics["left"].set(f"{cmd.local_left_vel:+.2f}") + self._metrics["ang"].set(f"{cmd.angular_vel:+.2f}") + self._readout.set( + f"fwd={cmd.local_forward_vel:+.2f} left={cmd.local_left_vel:+.2f}" + f" ang={cmd.angular_vel:+.2f} kick={int(cmd.kick)}" + f" drib={int(cmd.dribble)} chip={int(cmd.chip)}" + ) + + def _quit(self): + self._running = False + print("\nSending stop commands...") + for _ in range(10): + self.controller.add_robot_commands(empty_command(), ROBOT_ID) + self.controller.send_robot_commands() + self.root.destroy() + + +def main(): + controller = RealRobotController(is_team_yellow=IS_YELLOW, n_friendly=N_FRIENDLY) + root = tk.Tk() + TeleopGUI(root, controller) + root.mainloop() + + +if __name__ == "__main__": + main() From 27238c84aa9b69185de01ee2deadc8152ad41d93 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 5 May 2026 21:53:43 +0100 Subject: [PATCH 02/10] improved robot response handling --- .../controllers/real/real_robot_controller.py | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/utama_core/team_controller/src/controllers/real/real_robot_controller.py b/utama_core/team_controller/src/controllers/real/real_robot_controller.py index c20b5715..c3532450 100644 --- a/utama_core/team_controller/src/controllers/real/real_robot_controller.py +++ b/utama_core/team_controller/src/controllers/real/real_robot_controller.py @@ -57,20 +57,51 @@ def __init__(self, is_team_yellow: bool, n_friendly: int): # track last kick time for each robot to transmit kick as HIGH for n timesteps after command self._kicker_tracker: Dict[int, KickTrackerEntry] = {} - def get_robots_responses(self) -> Optional[List[RobotResponse]]: + def get_robots_responses(self) -> List[RobotResponse]: + HEADER = 0xAA + FOOTER = 0x55 + if not hasattr(self, "_buffer"): + self._buffer = bytearray() + + # Read whatever is available + self._buffer.extend(self._serial_port.read(self._serial_port.in_waiting or 1)) + responses = [] - while self._serial_port.in_waiting: - header = self._serial_port.read()[0] - if header != 0xAA: + + while True: + # Look for header + if len(self._buffer) < 1: + break + + if self._buffer[0] != HEADER: + self._buffer.pop(0) continue - robot_id = self._serial_port.read()[0] - length = self._serial_port.read()[0] + # Need at least header + id + length + if len(self._buffer) < 3: + break + + robot_id = self._buffer[1] + length = self._buffer[2] + + packet_len = 1 + 1 + 1 + length + 1 # header + id + len + data + footer + + if len(self._buffer) < packet_len: + break # wait for more data + + # Validate footer + if self._buffer[packet_len - 1] != FOOTER: + # Corrupted packet → resync + self._buffer.pop(0) + continue + + # Extract packet + data = self._buffer[3 : 3 + length] + + responses.append(RobotResponse(robot_id, has_ball=(data[0] & 0x01) != 0)) - data = self._serial_port.read(size=length) - responses.append(RobotResponse(robot_id, has_ball=data[0] & 0x01 != 0)) - footer = self._serial_port.read()[0] - assert footer == 0x55 + # Remove parsed packet + del self._buffer[:packet_len] return responses From 85a7246675eb56cc6d49f2ebdfda457457deb5e8 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 7 May 2026 14:37:35 +0100 Subject: [PATCH 03/10] some comment fixes --- pixi.toml | 1 + .../controllers/real/real_robot_controller.py | 2 +- .../src/controllers/real/telop_gui.py | 66 ++++++++++++++++--- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/pixi.toml b/pixi.toml index 71e20ea7..52fe6140 100644 --- a/pixi.toml +++ b/pixi.toml @@ -50,6 +50,7 @@ lint = "pre-commit run --all-files" test = "pytest utama_core/tests/" main = "python -m main" replay = "python -m utama_core.replay.replay_player" +debug-robots = "python -m utama_core.team_controller.src.debug_utils.telop_gui" precommit-install = "pre-commit install && pixi run pre-commit install --hook-type pre-commit" precommit-uninstall = "pre-commit uninstall" diff --git a/utama_core/team_controller/src/controllers/real/real_robot_controller.py b/utama_core/team_controller/src/controllers/real/real_robot_controller.py index c3532450..e762c25b 100644 --- a/utama_core/team_controller/src/controllers/real/real_robot_controller.py +++ b/utama_core/team_controller/src/controllers/real/real_robot_controller.py @@ -112,7 +112,7 @@ def send_robot_commands(self) -> None: # print(binary_representation) if len(self._assigned_mapping) != self._n_friendly: warnings.warn( - f"Only {len(self._assigned_mapping)} out of {self._n_friendly} robots have been assigned commands. Sending incomplete command packet." + f"Only {len(self._assigned_mapping)} out of {self._n_friendly} robots have been assigned commands. Sending empty commands for unassigned robots." ) self._serial_port.write(self._out_packet) diff --git a/utama_core/team_controller/src/controllers/real/telop_gui.py b/utama_core/team_controller/src/controllers/real/telop_gui.py index c2d80669..30bc70be 100644 --- a/utama_core/team_controller/src/controllers/real/telop_gui.py +++ b/utama_core/team_controller/src/controllers/real/telop_gui.py @@ -70,7 +70,14 @@ def _build_ui(self): self._metrics = {} for label, key in [("Forward", "fwd"), ("Left", "left"), ("Angular", "ang")]: - col = tk.Frame(status_frame, bg=SURFACE, padx=14, pady=8, highlightbackground=BORDER, highlightthickness=1) + col = tk.Frame( + status_frame, + bg=SURFACE, + padx=14, + pady=8, + highlightbackground=BORDER, + highlightthickness=1, + ) col.pack(side="left", expand=True, fill="x", padx=4) tk.Label(col, text=label, bg=SURFACE, fg=MUTED, font=("monospace", 10)).pack() var = tk.StringVar(value="0.00") @@ -174,20 +181,37 @@ def _build_ui(self): row.pack(fill="x", padx=8, pady=4) id_lbl = tk.Label( - row, text=f"Robot {i}", bg=SURFACE, fg=TEXT, font=("monospace", 11, "bold"), width=8, anchor="w" + row, + text=f"Robot {i}", + bg=SURFACE, + fg=TEXT, + font=("monospace", 11, "bold"), + width=8, + anchor="w", ) id_lbl.pack(side="left") ball_var = tk.StringVar(value="ball: --") ball_lbl = tk.Label( - row, textvariable=ball_var, bg=SURFACE, fg=MUTED, font=("monospace", 11), width=12, anchor="w" + row, + textvariable=ball_var, + bg=SURFACE, + fg=MUTED, + font=("monospace", 11), + width=12, + anchor="w", ) ball_lbl.pack(side="left", padx=(8, 0)) status_var = tk.StringVar(value="no data") - tk.Label(row, textvariable=status_var, bg=SURFACE, fg=MUTED, font=("monospace", 10), anchor="e").pack( - side="right" - ) + tk.Label( + row, + textvariable=status_var, + bg=SURFACE, + fg=MUTED, + font=("monospace", 10), + anchor="e", + ).pack(side="right") self._fb_ball_vars[i] = ball_var self._fb_ball_lbls[i] = ball_lbl @@ -209,6 +233,33 @@ def _build_ui(self): pady=8, ).pack(fill="x") + def _bind_keys(self): + movement = ["w", "a", "s", "d", "q", "e"] + + for key in movement: + for k in (key.lower(), key.upper()): + self.root.bind(f"", lambda e, key=key: self._press(key)) + self.root.bind(f"", lambda e, key=key: self._release(key)) + + special_keys = { + "space": (self._press, self._release), + } + + for key, (press_fn, release_fn) in special_keys.items(): + self.root.bind(f"", lambda e, k=key, fn=press_fn: fn(k)) + self.root.bind(f"", lambda e, k=key, fn=release_fn: fn(k)) + + toggles = { + "b": self._toggle_dribble, + "c": self._toggle_chip, + } + + for key, fn in toggles.items(): + for k in (key.lower(), key.upper()): + self.root.bind(f"", lambda e, fn=fn: fn()) + + self.root.bind("", lambda e: self._quit()) + def _bind_keys(self): movement = ["w", "a", "s", "d", "q", "e"] for k in movement: @@ -280,8 +331,7 @@ def _control_loop(self): responses = self.controller.get_robots_responses() or [] - self.controller.add_robot_commands(cmd, 0) - self.controller.add_robot_commands(cmd, 1) + self.controller.add_robot_commands(cmd, ROBOT_ID) self.controller.send_robot_commands() print("Sending commands", cmd) From 6bf871f69d5b817e0557abab330d1b10e8afef43 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 7 May 2026 14:44:14 +0100 Subject: [PATCH 04/10] fix threading issues --- .../src/controllers/real/telop_gui.py | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/utama_core/team_controller/src/controllers/real/telop_gui.py b/utama_core/team_controller/src/controllers/real/telop_gui.py index 30bc70be..86c1d451 100644 --- a/utama_core/team_controller/src/controllers/real/telop_gui.py +++ b/utama_core/team_controller/src/controllers/real/telop_gui.py @@ -9,6 +9,7 @@ Esc — quit """ +import queue import threading import time import tkinter as tk @@ -42,7 +43,12 @@ class TeleopGUI: def __init__(self, root: tk.Tk, controller: RealRobotController): self.root = root self.controller = controller + + # Thread safety mechanics + self._lock = threading.Lock() + self._ui_queue = queue.Queue() self.held: set[str] = set() + self.dribble = False self.chip = False self._running = True @@ -53,6 +59,7 @@ def __init__(self, root: tk.Tk, controller: RealRobotController): self._build_ui() self._bind_keys() + self._poll_queue() self._loop_thread = threading.Thread(target=self._control_loop, daemon=True) self._loop_thread.start() @@ -260,35 +267,27 @@ def _bind_keys(self): self.root.bind("", lambda e: self._quit()) - def _bind_keys(self): - movement = ["w", "a", "s", "d", "q", "e"] - for k in movement: - self.root.bind(f"", lambda e, k=k: self._press(k)) - self.root.bind(f"", lambda e, k=k: self._release(k)) - self.root.bind(f"", lambda e, k=k: self._press(k)) - self.root.bind(f"", lambda e, k=k: self._release(k)) - - self.root.bind("", lambda e: self._press("space")) - self.root.bind("", lambda e: self._release("space")) - self.root.bind("", lambda e: self._toggle_dribble()) - self.root.bind("", lambda e: self._toggle_chip()) - self.root.bind("", lambda e: self._quit()) - def _press(self, key: str): - self.held.add(key) + with self._lock: + self.held.add(key) self._refresh_key_visuals() def _release(self, key: str): - self.held.discard(key) + with self._lock: + self.held.discard(key) self._refresh_key_visuals() def _refresh_key_visuals(self): + with self._lock: + current_held = set(self.held) + for k, btn in self._key_buttons.items(): - if k in self.held: + if k in current_held: btn.configure(bg=ACTIVE, fg="white", highlightbackground=ACTIVE) else: btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) - if "space" in self.held: + + if "space" in current_held: self._kick_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) else: self._kick_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) @@ -309,12 +308,29 @@ def _toggle_chip(self): # --------------------------------------------------------- Control loop -- + def _poll_queue(self): + """Processes UI updates safely on the Tkinter main thread.""" + try: + while True: + msg_type, data = self._ui_queue.get_nowait() + if msg_type == "readout": + self._update_readout(data) + elif msg_type == "feedback": + self._update_feedback(data) + except queue.Empty: + pass + + if self._running: + self.root.after(16, self._poll_queue) + def _control_loop(self): dt = 1.0 / LOOP_HZ while self._running: t0 = time.perf_counter() - keys = set(self.held) + with self._lock: + keys = set(self.held) + fwd = MAX_VEL if "w" in keys else (-MAX_VEL if "s" in keys else 0.0) left = MAX_VEL if "a" in keys else (-MAX_VEL if "d" in keys else 0.0) ang = MAX_ANG_VEL if "q" in keys else (-MAX_ANG_VEL if "e" in keys else 0.0) @@ -335,9 +351,10 @@ def _control_loop(self): self.controller.send_robot_commands() print("Sending commands", cmd) - self.root.after(0, self._update_readout, cmd) + # Safely pass updates to the main thread via the queue + self._ui_queue.put(("readout", cmd)) if responses: - self.root.after(0, self._update_feedback, responses) + self._ui_queue.put(("feedback", responses)) elapsed = time.perf_counter() - t0 time.sleep(max(0.0, dt - elapsed)) @@ -366,10 +383,16 @@ def _update_readout(self, cmd: RobotCommand): def _quit(self): self._running = False + + # Ensure the control loop thread fully terminates before destroying Tk root + if self._loop_thread.is_alive(): + self._loop_thread.join(timeout=1.0) + print("\nSending stop commands...") for _ in range(10): self.controller.add_robot_commands(empty_command(), ROBOT_ID) self.controller.send_robot_commands() + self.root.destroy() From c28c412b4081557e4e1c21e4f9d7aeb75f2a437b Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 7 May 2026 14:45:53 +0100 Subject: [PATCH 05/10] move telop to debug utils --- .../src/{controllers/real => debug_utils}/telop_gui.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename utama_core/team_controller/src/{controllers/real => debug_utils}/telop_gui.py (100%) diff --git a/utama_core/team_controller/src/controllers/real/telop_gui.py b/utama_core/team_controller/src/debug_utils/telop_gui.py similarity index 100% rename from utama_core/team_controller/src/controllers/real/telop_gui.py rename to utama_core/team_controller/src/debug_utils/telop_gui.py From c8d4157ea2a4cedef354c55b406cd47099aab207 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 7 May 2026 17:41:12 +0100 Subject: [PATCH 06/10] fix characters --- .../src/debug_utils/telop_gui.py | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/utama_core/team_controller/src/debug_utils/telop_gui.py b/utama_core/team_controller/src/debug_utils/telop_gui.py index 86c1d451..6262fce2 100644 --- a/utama_core/team_controller/src/debug_utils/telop_gui.py +++ b/utama_core/team_controller/src/debug_utils/telop_gui.py @@ -99,15 +99,29 @@ def _build_ui(self): [None, "Q", "W", "E", None], [None, "A", "S", "D", None], ] - self._key_buttons: dict[str, tk.Label] = {} + + self._key_buttons = {} + for row_i, row in enumerate(layout): - for col_i, k in enumerate(row): - if k is None: - tk.Frame(keypad, width=58, height=58, bg=BG).grid(row=row_i, column=col_i, padx=3, pady=3) + for col_i, key in enumerate(row): + + if key is None: + tk.Frame( + keypad, + width=58, + height=58, + bg=BG, + ).grid( + row=row_i + 1, + column=col_i, + padx=3, + pady=3, + ) continue + btn = tk.Label( keypad, - text=k, + text=key, width=3, bg=SURFACE, fg=TEXT, @@ -117,19 +131,39 @@ def _build_ui(self): padx=10, pady=10, ) - btn.grid(row=row_i, column=col_i, padx=3, pady=3) - self._key_buttons[k.lower()] = btn - # Arrow labels for Q/E - tk.Label(keypad, text="↺", bg=BG, fg=MUTED, font=("monospace", 10)).grid(row=2, column=1) - tk.Label(keypad, text="↻", bg=BG, fg=MUTED, font=("monospace", 10)).grid(row=2, column=3) + btn.grid( + row=row_i + 1, + column=col_i, + padx=3, + pady=3, + ) + + self._key_buttons[key.lower()] = btn + + # Labels ABOVE Q and E + tk.Label( + keypad, + text="CCW", + bg=BG, + fg=MUTED, + font=("monospace", 10), + ).grid(row=0, column=1) + + tk.Label( + keypad, + text="CW", + bg=BG, + fg=MUTED, + font=("monospace", 10), + ).grid(row=0, column=3) # --- Kick button --- kick_frame = tk.Frame(self.root, bg=BG) kick_frame.pack(**pad) self._kick_btn = tk.Label( kick_frame, - text="SPACE — kick", + text="SPACE - kick", bg=SURFACE, fg=TEXT, font=("monospace", 13), From 0d60baaaf281068250b8e81079e06c9cd35d1bb6 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 7 May 2026 17:56:14 +0100 Subject: [PATCH 07/10] remap UI so it makes more sense --- .../src/debug_utils/telop_gui.py | 66 ++++++++++++------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/utama_core/team_controller/src/debug_utils/telop_gui.py b/utama_core/team_controller/src/debug_utils/telop_gui.py index 6262fce2..69e782c0 100644 --- a/utama_core/team_controller/src/debug_utils/telop_gui.py +++ b/utama_core/team_controller/src/debug_utils/telop_gui.py @@ -163,7 +163,7 @@ def _build_ui(self): kick_frame.pack(**pad) self._kick_btn = tk.Label( kick_frame, - text="SPACE - kick", + text="B - kick", bg=SURFACE, fg=TEXT, font=("monospace", 13), @@ -180,7 +180,7 @@ def _build_ui(self): self._dribble_btn = tk.Label( toggle_frame, - text="B dribble: OFF", + text="SPACE dribble: OFF", bg=SURFACE, fg=MUTED, font=("monospace", 12), @@ -194,9 +194,9 @@ def _build_ui(self): self._chip_btn = tk.Label( toggle_frame, - text="C chip: OFF", + text="C chip", bg=SURFACE, - fg=MUTED, + fg=TEXT, font=("monospace", 12), highlightbackground=BORDER, highlightthickness=1, @@ -204,7 +204,6 @@ def _build_ui(self): pady=8, ) self._chip_btn.pack(side="left", padx=6) - self._chip_btn.bind("", lambda e: self._toggle_chip()) # --- Robot feedback --- fb_outer = tk.Frame(self.root, bg=BG) @@ -275,29 +274,36 @@ def _build_ui(self): ).pack(fill="x") def _bind_keys(self): + movement = ["w", "a", "s", "d", "q", "e"] for key in movement: for k in (key.lower(), key.upper()): - self.root.bind(f"", lambda e, key=key: self._press(key)) - self.root.bind(f"", lambda e, key=key: self._release(key)) - special_keys = { - "space": (self._press, self._release), - } + self.root.bind( + f"", + lambda e, key=key: self._press(key), + ) + + self.root.bind( + f"", + lambda e, key=key: self._release(key), + ) - for key, (press_fn, release_fn) in special_keys.items(): - self.root.bind(f"", lambda e, k=key, fn=press_fn: fn(k)) - self.root.bind(f"", lambda e, k=key, fn=release_fn: fn(k)) + # Dribbler toggle (Space) + self.root.bind("", lambda e: self._toggle_dribble()) - toggles = { - "b": self._toggle_dribble, - "c": self._toggle_chip, - } + # Chip (Press C) + self.root.bind("", lambda e: self._press("c")) + self.root.bind("", lambda e: self._release("c")) + self.root.bind("", lambda e: self._press("c")) + self.root.bind("", lambda e: self._release("c")) - for key, fn in toggles.items(): - for k in (key.lower(), key.upper()): - self.root.bind(f"", lambda e, fn=fn: fn()) + # Kick (Press B) + self.root.bind("", lambda e: self._press("b")) + self.root.bind("", lambda e: self._release("b")) + self.root.bind("", lambda e: self._press("b")) + self.root.bind("", lambda e: self._release("b")) self.root.bind("", lambda e: self._quit()) @@ -321,17 +327,24 @@ def _refresh_key_visuals(self): else: btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) - if "space" in current_held: + # Kick visual (now bound to B) + if "b" in current_held: self._kick_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) else: self._kick_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + # Chip visual (now bound to C) + if "c" in current_held: + self._chip_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) + else: + self._chip_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + def _toggle_dribble(self): self.dribble = not self.dribble if self.dribble: - self._dribble_btn.configure(text="B dribble: ON", fg=ON_C, highlightbackground=ON_C) + self._dribble_btn.configure(text="SPACE dribble: ON", fg=ON_C, highlightbackground=ON_C) else: - self._dribble_btn.configure(text="B dribble: OFF", fg=MUTED, highlightbackground=BORDER) + self._dribble_btn.configure(text="SPACE dribble: OFF", fg=MUTED, highlightbackground=BORDER) def _toggle_chip(self): self.chip = not self.chip @@ -368,14 +381,17 @@ def _control_loop(self): fwd = MAX_VEL if "w" in keys else (-MAX_VEL if "s" in keys else 0.0) left = MAX_VEL if "a" in keys else (-MAX_VEL if "d" in keys else 0.0) ang = MAX_ANG_VEL if "q" in keys else (-MAX_ANG_VEL if "e" in keys else 0.0) - kick = "space" in keys + + # Updated remapped logic + kick = "b" in keys + chip = "c" in keys cmd = RobotCommand( local_forward_vel=fwd, local_left_vel=left, angular_vel=ang, kick=kick, - chip=self.chip, + chip=chip, dribble=self.dribble, ) From 9641c411606f5415583bd0dccd1acae1477ebccc Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 May 2026 01:04:31 +0100 Subject: [PATCH 08/10] add guardrails and improve telop --- .../src/debug_utils/telop_gui.py | 197 +++++++++++++----- 1 file changed, 141 insertions(+), 56 deletions(-) diff --git a/utama_core/team_controller/src/debug_utils/telop_gui.py b/utama_core/team_controller/src/debug_utils/telop_gui.py index 69e782c0..87fe4fca 100644 --- a/utama_core/team_controller/src/debug_utils/telop_gui.py +++ b/utama_core/team_controller/src/debug_utils/telop_gui.py @@ -1,10 +1,18 @@ +from dataclasses import dataclass + +# ============================================================ +# Dummy mode +# ============================================================ + +USE_DUMMY_CONTROLLER = False # dummy controller for checking ui with fake serial data. + """ Controls (keyboard or click): W / S — forward / reverse A / D — strafe left / right Q / E — rotate CCW / CW - Space — kick - B — toggle dribbler + Space — dribble + B — toggle kick C — toggle chip Esc — quit """ @@ -14,18 +22,62 @@ import time import tkinter as tk -from utama_core.team_controller.src.controllers.real.real_robot_controller import ( - RealRobotController, - RobotCommand, - empty_command, -) +if not USE_DUMMY_CONTROLLER: + + from utama_core.team_controller.src.controllers.real.real_robot_controller import ( + RealRobotController, + RobotCommand, + empty_command, + ) + +else: + + @dataclass + class RobotCommand: + local_forward_vel: float = 0.0 + local_left_vel: float = 0.0 + angular_vel: float = 0.0 + kick: bool = False + chip: bool = False + dribble: bool = False + + def empty_command(): + return RobotCommand() + + class DummyResponse: + def __init__(self, robot_id, has_ball=False): + self.id = robot_id + self.has_ball = has_ball + + class RealRobotController: + def __init__(self, is_team_yellow=True, n_friendly=2): + + self.n_friendly = n_friendly + + print("\n[DUMMY CONTROLLER ENABLED]") + print("No serial/network/hardware required.\n") + + def get_robots_responses(self): + + # Fake UI feedback + return [ + DummyResponse(0, has_ball=True), + DummyResponse(1, has_ball=False), + ] + + def add_robot_commands(self, cmd, robot_id): + pass + + def send_robot_commands(self): + pass + # --- Config --- ROBOT_ID = 1 N_FRIENDLY = 2 IS_YELLOW = True LOOP_HZ = 60 -MAX_VEL = -0.1 +MAX_VEL = 0.1 MAX_ANG_VEL = 1 BG = "#1a1a1a" @@ -37,6 +89,7 @@ KICK_C = "#d94a4a" ON_C = "#4ad97a" BALL_C = "#d9a84a" +DUMMY_C = "#d9a84a" # Orange for dummy warning class TeleopGUI: @@ -48,9 +101,15 @@ def __init__(self, root: tk.Tk, controller: RealRobotController): self._lock = threading.Lock() self._ui_queue = queue.Queue() self.held: set[str] = set() + self._release_timers: dict[str, str] = {} self.dribble = False self.chip = False + + # Impulse flags + self._kick_impulse = False + self._chip_impulse = False + self._running = True root.title("Robot Teleop") @@ -71,6 +130,18 @@ def __init__(self, root: tk.Tk, controller: RealRobotController): def _build_ui(self): pad = dict(padx=12, pady=8) + # --- Dummy Mode Indicator --- + if USE_DUMMY_CONTROLLER: + dummy_banner = tk.Label( + self.root, + text="[ DUMMY MODE ACTIVE ]", + bg=BG, + fg=DUMMY_C, + font=("monospace", 10, "bold"), + pady=4, + ) + dummy_banner.pack(fill="x") + # --- Status bar --- status_frame = tk.Frame(self.root, bg=BG) status_frame.pack(fill="x", **pad) @@ -158,49 +229,50 @@ def _build_ui(self): font=("monospace", 10), ).grid(row=0, column=3) - # --- Kick button --- - kick_frame = tk.Frame(self.root, bg=BG) - kick_frame.pack(**pad) - self._kick_btn = tk.Label( - kick_frame, - text="B - kick", + # --- Space row (Toggle Dribble) --- + space_frame = tk.Frame(self.root, bg=BG) + space_frame.pack(**pad) + + self._dribble_btn = tk.Label( + space_frame, + text="SPACE dribble: OFF", bg=SURFACE, - fg=TEXT, + fg=MUTED, font=("monospace", 13), highlightbackground=BORDER, highlightthickness=1, padx=40, pady=10, ) - self._kick_btn.pack() + self._dribble_btn.pack() + self._dribble_btn.bind("", lambda e: self._toggle_dribble()) - # --- Toggle buttons --- - toggle_frame = tk.Frame(self.root, bg=BG) - toggle_frame.pack(**pad) + # --- B and C row (Kick and Chip) --- + action_frame = tk.Frame(self.root, bg=BG) + action_frame.pack(**pad) - self._dribble_btn = tk.Label( - toggle_frame, - text="SPACE dribble: OFF", + self._kick_btn = tk.Label( + action_frame, + text="B kick", bg=SURFACE, - fg=MUTED, + fg=TEXT, font=("monospace", 12), highlightbackground=BORDER, highlightthickness=1, - padx=16, + padx=28, pady=8, ) - self._dribble_btn.pack(side="left", padx=6) - self._dribble_btn.bind("", lambda e: self._toggle_dribble()) + self._kick_btn.pack(side="left", padx=6) self._chip_btn = tk.Label( - toggle_frame, + action_frame, text="C chip", bg=SURFACE, fg=TEXT, font=("monospace", 12), highlightbackground=BORDER, highlightthickness=1, - padx=16, + padx=28, pady=8, ) self._chip_btn.pack(side="left", padx=6) @@ -293,13 +365,13 @@ def _bind_keys(self): # Dribbler toggle (Space) self.root.bind("", lambda e: self._toggle_dribble()) - # Chip (Press C) + # Chip (Impulse C) self.root.bind("", lambda e: self._press("c")) self.root.bind("", lambda e: self._release("c")) self.root.bind("", lambda e: self._press("c")) self.root.bind("", lambda e: self._release("c")) - # Kick (Press B) + # Kick (Impulse B) self.root.bind("", lambda e: self._press("b")) self.root.bind("", lambda e: self._release("b")) self.root.bind("", lambda e: self._press("b")) @@ -309,12 +381,34 @@ def _bind_keys(self): def _press(self, key: str): with self._lock: - self.held.add(key) + # Check for auto-repeat + if key in self._release_timers: + self.root.after_cancel(self._release_timers.pop(key)) + return + + # Fresh press + if key not in self.held: + if key == "b": + self._kick_impulse = True + if key == "c": + self._chip_impulse = True + self.held.add(key) + self._refresh_key_visuals() def _release(self, key: str): + # Debounce release to filter OS auto-repeat events + with self._lock: + if key in self._release_timers: + self.root.after_cancel(self._release_timers.pop(key)) + + timer_id = self.root.after(30, lambda k=key: self._execute_release(k)) + self._release_timers[key] = timer_id + + def _execute_release(self, key: str): with self._lock: self.held.discard(key) + self._release_timers.pop(key, None) self._refresh_key_visuals() def _refresh_key_visuals(self): @@ -327,18 +421,6 @@ def _refresh_key_visuals(self): else: btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) - # Kick visual (now bound to B) - if "b" in current_held: - self._kick_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) - else: - self._kick_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) - - # Chip visual (now bound to C) - if "c" in current_held: - self._chip_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) - else: - self._chip_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) - def _toggle_dribble(self): self.dribble = not self.dribble if self.dribble: @@ -346,13 +428,6 @@ def _toggle_dribble(self): else: self._dribble_btn.configure(text="SPACE dribble: OFF", fg=MUTED, highlightbackground=BORDER) - def _toggle_chip(self): - self.chip = not self.chip - if self.chip: - self._chip_btn.configure(text="C chip: ON", fg=ON_C, highlightbackground=ON_C) - else: - self._chip_btn.configure(text="C chip: OFF", fg=MUTED, highlightbackground=BORDER) - # --------------------------------------------------------- Control loop -- def _poll_queue(self): @@ -377,15 +452,16 @@ def _control_loop(self): with self._lock: keys = set(self.held) + # Impulse capture + kick = self._kick_impulse + chip = self._chip_impulse + self._kick_impulse = False + self._chip_impulse = False fwd = MAX_VEL if "w" in keys else (-MAX_VEL if "s" in keys else 0.0) left = MAX_VEL if "a" in keys else (-MAX_VEL if "d" in keys else 0.0) ang = MAX_ANG_VEL if "q" in keys else (-MAX_ANG_VEL if "e" in keys else 0.0) - # Updated remapped logic - kick = "b" in keys - chip = "c" in keys - cmd = RobotCommand( local_forward_vel=fwd, local_left_vel=left, @@ -399,7 +475,6 @@ def _control_loop(self): self.controller.add_robot_commands(cmd, ROBOT_ID) self.controller.send_robot_commands() - print("Sending commands", cmd) # Safely pass updates to the main thread via the queue self._ui_queue.put(("readout", cmd)) @@ -431,10 +506,20 @@ def _update_readout(self, cmd: RobotCommand): f" drib={int(cmd.dribble)} chip={int(cmd.chip)}" ) + # UI Flash for impulse feedback + if cmd.kick: + self._kick_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) + else: + self._kick_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + + if cmd.chip: + self._chip_btn.configure(bg=KICK_C, fg="white", highlightbackground=KICK_C) + else: + self._chip_btn.configure(bg=SURFACE, fg=TEXT, highlightbackground=BORDER) + def _quit(self): self._running = False - # Ensure the control loop thread fully terminates before destroying Tk root if self._loop_thread.is_alive(): self._loop_thread.join(timeout=1.0) From 65f54aca6ccb6549e4505e4252267647c224b72b Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 May 2026 01:14:06 +0100 Subject: [PATCH 09/10] guardrail bug fix for kick sending --- .../controllers/real/real_robot_controller.py | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/utama_core/team_controller/src/controllers/real/real_robot_controller.py b/utama_core/team_controller/src/controllers/real/real_robot_controller.py index e762c25b..278d7f0a 100644 --- a/utama_core/team_controller/src/controllers/real/real_robot_controller.py +++ b/utama_core/team_controller/src/controllers/real/real_robot_controller.py @@ -60,16 +60,21 @@ def __init__(self, is_team_yellow: bool, n_friendly: int): def get_robots_responses(self) -> List[RobotResponse]: HEADER = 0xAA FOOTER = 0x55 + MAX_PAYLOAD_LEN = 32 # Sanity limit to prevent buffer bloat + MIN_PAYLOAD_LEN = 1 # We expect at least 1 byte for ball status + if not hasattr(self, "_buffer"): self._buffer = bytearray() - # Read whatever is available - self._buffer.extend(self._serial_port.read(self._serial_port.in_waiting or 1)) + # 1. Non-blocking read: Only read if bytes are actually waiting + bytes_available = self._serial_port.in_waiting + if bytes_available > 0: + self._buffer.extend(self._serial_port.read(bytes_available)) responses = [] while True: - # Look for header + # 2. Look for header if len(self._buffer) < 1: break @@ -77,30 +82,39 @@ def get_robots_responses(self) -> List[RobotResponse]: self._buffer.pop(0) continue - # Need at least header + id + length + # 3. Need at least header + id + length if len(self._buffer) < 3: break robot_id = self._buffer[1] length = self._buffer[2] + # 4. Validate length byte before proceeding + if length < MIN_PAYLOAD_LEN or length > MAX_PAYLOAD_LEN: + # Malformed length; pop the header to resync + self._buffer.pop(0) + continue + packet_len = 1 + 1 + 1 + length + 1 # header + id + len + data + footer + # 5. Wait for the full packet to arrive if len(self._buffer) < packet_len: - break # wait for more data + break - # Validate footer + # 6. Validate footer if self._buffer[packet_len - 1] != FOOTER: - # Corrupted packet → resync + # Corrupted packet or offset mismatch; resync self._buffer.pop(0) continue - # Extract packet + # 7. Extract packet data safely data = self._buffer[3 : 3 + length] - responses.append(RobotResponse(robot_id, has_ball=(data[0] & 0x01) != 0)) + # Guard against IndexError just in case, though validated by 'length' check + if len(data) >= 1: + responses.append(RobotResponse(robot_id, has_ball=(data[0] & 0x01) != 0)) - # Remove parsed packet + # 8. Clear parsed packet from buffer del self._buffer[:packet_len] return responses @@ -235,20 +249,21 @@ def _generate_command_buffer(self, robot_id: int, c_command: RobotCommand) -> by kicker_byte = 0 tracker = self._kicker_tracker.get(robot_id) - # If tracker_entry exists but persistence expired → send empty kicker byte and return - if tracker and tracker.remaining_persist <= 0: - packet.append(kicker_byte) - return packet - - # Decide whether we're kicking or chipping - kick_active = c_command.kick or (tracker and tracker.remaining_persist > 0 and tracker.is_kick) - - chip_active = c_command.chip or (tracker and tracker.remaining_persist > 0 and not tracker.is_kick) + if tracker: + # If a tracker exists, we are either PERSISTING or COOLING DOWN. + # We ignore raw c_command inputs until the tracker is removed. + kick_active = tracker.remaining_persist > 0 and tracker.is_kick + chip_active = tracker.remaining_persist > 0 and not tracker.is_kick + else: + # No active tracker: we are free to trigger a new action. + kick_active = c_command.kick + chip_active = c_command.chip + # Apply bitmasks based on the determined state if kick_active: kicker_byte |= 0xF0 # upper kicker full power elif chip_active: - kicker_byte |= 0x0F + kicker_byte |= 0x0F # chip full power packet.append(kicker_byte) From 12db8eafc97183b8917475b2f387ee4bfb88508f Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 May 2026 01:15:29 +0100 Subject: [PATCH 10/10] added comments --- utama_core/team_controller/src/debug_utils/telop_gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utama_core/team_controller/src/debug_utils/telop_gui.py b/utama_core/team_controller/src/debug_utils/telop_gui.py index 87fe4fca..79ce2ab3 100644 --- a/utama_core/team_controller/src/debug_utils/telop_gui.py +++ b/utama_core/team_controller/src/debug_utils/telop_gui.py @@ -453,8 +453,8 @@ def _control_loop(self): with self._lock: keys = set(self.held) # Impulse capture - kick = self._kick_impulse - chip = self._chip_impulse + kick = self._kick_impulse # don't worry about kick cooldown, handled in controller. + chip = self._chip_impulse # don't worry about kick and chip at same time, handled in controller. self._kick_impulse = False self._chip_impulse = False