diff --git a/src/software/gameplay_tests/simulated_test_fixture.py b/src/software/gameplay_tests/simulated_test_fixture.py index 71c14e1c85..3b7c26213b 100644 --- a/src/software/gameplay_tests/simulated_test_fixture.py +++ b/src/software/gameplay_tests/simulated_test_fixture.py @@ -591,7 +591,8 @@ def simulated_test_runner(): running_in_realtime=args.enable_thunderscope and not args.ci_mode, ) as yellow_fs: with Gamecontroller( - suppress_logs=(not args.show_gamecontroller_logs) + suppress_logs=(not args.show_gamecontroller_logs), + parallelized=True, ) as gamecontroller: blue_fs.setup_proto_unix_io(blue_full_system_proto_unix_io) yellow_fs.setup_proto_unix_io(yellow_full_system_proto_unix_io) diff --git a/src/software/thunderscope/binary_context_managers/BUILD b/src/software/thunderscope/binary_context_managers/BUILD index 0fe462dd43..1d363161ce 100644 --- a/src/software/thunderscope/binary_context_managers/BUILD +++ b/src/software/thunderscope/binary_context_managers/BUILD @@ -24,6 +24,7 @@ py_library( "//software/networking:ssl_proto_communication", "//software/thunderscope:util", "//software/thunderscope/common:thread_safe_circular_buffer", + requirement("netifaces"), ], ) diff --git a/src/software/thunderscope/binary_context_managers/game_controller.py b/src/software/thunderscope/binary_context_managers/game_controller.py index 99555b0758..f4150067d6 100644 --- a/src/software/thunderscope/binary_context_managers/game_controller.py +++ b/src/software/thunderscope/binary_context_managers/game_controller.py @@ -7,6 +7,8 @@ import logging import os import time +import netifaces + from subprocess import Popen from typing import Any @@ -44,15 +46,18 @@ def __init__( suppress_logs: bool = False, use_conventional_port: bool = False, automate_referee: bool = False, + parallelized: bool = False, ) -> None: """Run Gamecontroller :param suppress_logs: True if logs should be suppressed :param use_conventional_port: True when using static referee port. False for dynamic port assignments. - :param automate_referee: True if referee commands should be automated + :param automate_referee: True if referee commands should be automated. + :param parallelized: True when this is one of many Gamecontrollers running at once. """ self.suppress_logs = suppress_logs self.automate_referee = automate_referee + self.parallelized = parallelized self.use_conventional_port = use_conventional_port self.referee_port = None @@ -109,17 +114,20 @@ def __enter__(self) -> Gamecontroller: command += ["-publishAddress", f"{self.REFEREE_IP}:{self.referee_port}"] command += ["-ciAddress", f"localhost:{self.ci_port}"] - command += [ - "-address", - "localhost:0", - "-autorefAddress", - "localhost:0", - "-remoteControlAddress", - "localhost:0", - "-teamAddress", - "localhost:0", - "-backendOnly", - ] + if self.parallelized: + # One of many GCs running at once: no web UI and all-dynamic ports so + # instances don't collide on the fixed UI / autoref ports. + command += [ + "-address", + "localhost:0", + "-autorefAddress", + "localhost:0", + "-remoteControlAddress", + "localhost:0", + "-teamAddress", + "localhost:0", + "-backendOnly", + ] if self.suppress_logs: with open(os.devnull, "w") as fp: @@ -245,15 +253,10 @@ def __send_referee_command(data: Referee) -> None: if autoref_proto_unix_io is not None: autoref_proto_unix_io.send_proto(Referee, data) - if is_current_platform_macos(): - loopback_iface = "en0" - else: - loopback_iface = "lo" - self.receive_referee_command = tbots_cpp.SSLRefereeProtoListener( Gamecontroller.REFEREE_IP, self.referee_port, - loopback_iface, + self.__get_referee_multicast_interface(), __send_referee_command, True, ) @@ -630,3 +633,19 @@ def __update_robot_count( robot_states[removed_robot_ids.get_nowait()].CopyFrom(place_state) except queue.Empty: return + + @staticmethod + def __get_referee_multicast_interface() -> str: + """Determine the network interface to join the referee multicast group on. + + :return: the name of the interface to receive referee multicast on + """ + if not is_current_platform_macos(): + return "lo" + + default = netifaces.gateways().get("default", {}).get(netifaces.AF_INET) + if default: + # default is a (gateway_ip, interface_name) tuple + return default[1] + + raise RuntimeError("Could not determine the default network interface on macOS")