Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ def __init__(
self,
suppress_logs: bool = False,
use_conventional_port: bool = False,
record_stats: bool = False
) -> None:
"""Run Gamecontroller

:param suppress_logs: Whether to suppress the logs
:param use_conventional_port: whether or not to use the conventional port!
"""
self.suppress_logs = suppress_logs
self.record_stats = record_stats

# We default to using a non-conventional port to avoid emitting
# on the same port as what other teams may be listening on.
Expand All @@ -68,6 +70,7 @@ def __init__(
self.latest_world = None
self.blue_removed_robot_ids = queue.Queue()
self.yellow_removed_robot_ids = queue.Queue()
self._max_robots_per_team = 6 # Tracks current max sent to SSL GC (for record_stats red card compensation)

def get_referee_port(self) -> int:
"""Sometimes, the port that we are using changes depending on context.
Expand Down Expand Up @@ -192,8 +195,17 @@ def handle_referee(self, referee: Referee) -> None:
block=False, return_cached=True
)

max_allowed_bots_yellow: int = referee.yellow.max_allowed_bots
max_allowed_bots_blue: int = referee.blue.max_allowed_bots
max_allowed_bots_yellow: int = 6 if self.record_stats else referee.yellow.max_allowed_bots
max_allowed_bots_blue: int = 6 if self.record_stats else referee.blue.max_allowed_bots

if self.record_stats:
# The SSL GC computes effective_max = max_robots_per_team - red_cards.
# Compensate so the GC's effective limit stays at 6 and it doesn't force halt.
max_red_cards = max(referee.blue.red_cards, referee.yellow.red_cards)
needed = 6 + max_red_cards
if needed != self._max_robots_per_team:
self._send_max_robots_update(needed)
self._max_robots_per_team = needed
# Ignore if nothing needs to be updated
if (
len(self.latest_world.friendly_team.team_robots) == max_allowed_bots_blue
Expand Down Expand Up @@ -316,6 +328,20 @@ def __send_referee_command(data: Referee) -> None:
)
self.simulator_proto_unix_io = simulator_proto_unix_io

def _send_max_robots_update(self, max_robots: int) -> None:
"""Send UpdateConfig to the SSL GC to set max_robots_per_team.
Used in record_stats mode to compensate for red cards so the GC doesn't force halt.

:param max_robots: the new max_robots_per_team value to send
"""
ci_input = CiInput(timestamp=int(time.time_ns()))
update_config = Change.UpdateConfig()
update_config.max_robots_per_team.value = max_robots
api_input = Input()
api_input.change.update_config_change.CopyFrom(update_config)
ci_input.api_inputs.append(api_input)
self.send_ci_input(ci_input)

def send_gc_command(
self,
gc_command: proto.ssl_gc_state_pb2.Command,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def __init__(
buffer_size: int = 5,
suppress_logs: bool = True,
show_gui: bool = False,
record_stats: bool = False,
) -> None:
"""Constructor

Expand All @@ -62,6 +63,7 @@ def __init__(
:param buffer_size: buffer size for the SSL wrapper and referee packets
:param suppress_logs: true silences logs from the Autoref binary, otherwise shows them (its very verbose)
:param show_gui: true shows the Tigers' autoref GUI, false runs it in headless mode
:param record_stats: true if stats recording mode is enabled
"""
self.tigers_autoref_proc = None
self.auto_ref_proc_thread = None
Expand All @@ -74,6 +76,7 @@ def __init__(
self.suppress_logs = suppress_logs
self.tick_rate_ms = tick_rate_ms
self.show_gui = show_gui
self.record_stats = record_stats

self.current_timestamp = int(time.time_ns())
self.timestamp_mutex = threading.Lock()
Expand Down Expand Up @@ -236,7 +239,7 @@ def _start_autoref(self) -> None:
if not self.show_gui:
autoref_cmd += " -hl"

if self.ci_mode:
if self.ci_mode or self.record_stats:
autoref_cmd += " --ci"

if self.suppress_logs:
Expand Down
44 changes: 36 additions & 8 deletions src/software/thunderscope/thunderscope_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@
default=False,
help="Run unix_full_system under sudo",
)
parser.add_argument(
"--record_stats_duration_s",
type=int,
default = 60,
help="Run unix_full_system under sudo",
)

estop_group = parser.add_mutually_exclusive_group()
estop_group.add_argument(
Expand Down Expand Up @@ -343,7 +349,7 @@

with (
Gamecontroller(
suppress_logs=(not args.verbose), use_conventional_port=False
suppress_logs=(not args.verbose), use_conventional_port=False, record_stats=args.record_stats
)
if args.launch_gc
else contextlib.nullcontext()
Expand Down Expand Up @@ -433,7 +439,7 @@ def __ticker(tick_rate_ms: int) -> None:
0 if args.empty else DIV_B_NUM_ROBOTS,
)

if args.ci_mode:
if args.ci_mode or args.record_stats:
async_sim_ticker(
tick_rate_ms,
tscope.proto_unix_io_map[ProtoUnixIOTypes.BLUE],
Expand All @@ -459,7 +465,7 @@ def __ticker(tick_rate_ms: int) -> None:
friendly_colour_yellow=False,
should_restart_on_crash=False,
run_sudo=args.sudo,
running_in_realtime=(not args.ci_mode),
running_in_realtime=(not args.ci_mode and not args.record_stats),
log_level=args.log_level,
) as blue_fs, FullSystem(
path_to_binary=runtime_config.get_yellow_runtime_path(),
Expand All @@ -468,12 +474,12 @@ def __ticker(tick_rate_ms: int) -> None:
friendly_colour_yellow=True,
should_restart_on_crash=False,
run_sudo=args.sudo,
running_in_realtime=(not args.ci_mode),
running_in_realtime=(not args.ci_mode and not args.record_stats),
log_level=args.log_level,
) as yellow_fs, Gamecontroller(
suppress_logs=(not args.verbose),
suppress_logs=(not args.verbose), record_stats=args.record_stats
) as gamecontroller, (
# Here we only initialize autoref if the --enable_autoref flag is requested.
# Here we only initialize autoref if the --enable_autoref flag is requested, or when we are recording stats.
# To avoid nested Python withs, the autoref is initialized as None when this flag doesn't exist.
# All calls to autoref should be guarded with args.enable_autoref
TigersAutoref(
Expand All @@ -482,8 +488,9 @@ def __ticker(tick_rate_ms: int) -> None:
suppress_logs=(not args.verbose),
tick_rate_ms=DEFAULT_SIMULATOR_TICK_RATE_MILLISECONDS_PER_TICK,
show_gui=args.show_autoref_gui,
record_stats=args.record_stats
)
if args.enable_autoref
if args.enable_autoref or args.record_stats
else contextlib.nullcontext()
) as autoref, (
Stats(
Expand Down Expand Up @@ -527,7 +534,7 @@ def __ticker(tick_rate_ms: int) -> None:
autoref_proto_unix_io=autoref_proto_unix_io,
simulator_proto_unix_io=tscope.proto_unix_io_map[ProtoUnixIOTypes.SIM],
)
if args.enable_autoref:
if args.enable_autoref or args.record_stats:
autoref.setup_ssl_wrapper_packets(
autoref_proto_unix_io,
)
Expand Down Expand Up @@ -559,6 +566,27 @@ def __ticker(tick_rate_ms: int) -> None:
exiter_thread.join()
sim_ticker_thread.join()

sys.exit(0)
elif args.record_stats:
# In record stats mode mode, we want AI vs AI to end automatically after a given time (args.record_stats_duration_s). The exiter
# thread is passed an exit handler that will close the Thunderscope window
# This exit handler is necessary because Qt runs on the main thread, so tscope.show() is a blocking
# call so we need to somehow close it before doing our resource cleanup
exiter_thread = threading.Thread(
target=exit_poller,
args=(autoref, args.record_stats_duration_s, lambda: tscope.close()),
daemon=True,
)

exiter_thread.start() # start the exit countdown
sim_ticker_thread.start() # start the simulation ticking

tscope.show() # blocking!

# these resource cleanups occur after tscope.close() is called by the exiter_thread
exiter_thread.join()
sim_ticker_thread.join()

sys.exit(0)
else:
sim_ticker_thread.start() # start the simulation ticking
Expand Down