diff --git a/src/software/field_tests/field_test_fixture.py b/src/software/field_tests/field_test_fixture.py index 7ae36de467..77e0f87c22 100644 --- a/src/software/field_tests/field_test_fixture.py +++ b/src/software/field_tests/field_test_fixture.py @@ -51,7 +51,7 @@ def __init__( :param test_name: The name of the test to run :param blue_full_system_proto_unix_io: The blue full system proto unix io to use :param yellow_full_system_proto_unix_io: The yellow full system proto unix io to use - :param gamecontroller: The gamecontroller context managed instance + :param gamecontroller: The gamecontroller context managed instance :param publish_validation_protos: whether to publish validation protos :param: is_yellow_friendly: if yellow is the friendly team """ @@ -373,84 +373,86 @@ def field_test_runner(): debug_full_system=debug_full_sys, friendly_colour_yellow=args.run_yellow, should_restart_on_crash=False, - ) as friendly_fs, RobotCommunication( + ) as friendly_fs, Gamecontroller( + # we would be using conventional port if and only if we are playing in robocup. + supress_logs=(not args.show_gamecontroller_logs), + use_conventional_port=False, + ) as gamecontroller, RobotCommunication( current_proto_unix_io=friendly_proto_unix_io, multicast_channel=getRobotMulticastChannel(args.channel), interface=args.interface, estop_mode=estop_mode, estop_path=estop_path, enable_radio=args.enable_radio, + referee_port=Gamecontroller.get_referee_port_staticmethod(gamecontroller), ) as rc_friendly: - with Gamecontroller( - supress_logs=(not args.show_gamecontroller_logs) - ) as gamecontroller: - friendly_fs.setup_proto_unix_io(friendly_proto_unix_io) - rc_friendly.setup_for_fullsystem() - - gamecontroller.setup_proto_unix_io( - blue_full_system_proto_unix_io, yellow_full_system_proto_unix_io, - ) - # Inject the proto unix ios into thunderscope and start the test - tscope = Thunderscope( - configure_field_test_view( - simulator_proto_unix_io=simulator_proto_unix_io, - blue_full_system_proto_unix_io=blue_full_system_proto_unix_io, - yellow_full_system_proto_unix_io=yellow_full_system_proto_unix_io, - yellow_is_friendly=args.run_yellow, - ), - layout_path=None, - ) - - # connect the keyboard estop toggle to the key event if needed - if estop_mode == EstopMode.KEYBOARD_ESTOP: - tscope.keyboard_estop_shortcut.activated.connect( - rc_friendly.toggle_keyboard_estop - ) - # we call this method to enable estop automatically when a field test starts - rc_friendly.toggle_keyboard_estop() - logger.warning( - "\x1b[31;20m" - + "Keyboard Estop Enabled, robots will start moving automatically when test starts!" - + "\x1b[0m" - ) + friendly_fs.setup_proto_unix_io(friendly_proto_unix_io) + rc_friendly.setup_for_fullsystem() - time.sleep(LAUNCH_DELAY_S) - runner = FieldTestRunner( - test_name=current_test, + gamecontroller.setup_proto_unix_io( + blue_full_system_proto_unix_io, yellow_full_system_proto_unix_io, + ) + # Inject the proto unix ios into thunderscope and start the test + tscope = Thunderscope( + configure_field_test_view( + simulator_proto_unix_io=simulator_proto_unix_io, blue_full_system_proto_unix_io=blue_full_system_proto_unix_io, yellow_full_system_proto_unix_io=yellow_full_system_proto_unix_io, - gamecontroller=gamecontroller, - thunderscope=tscope, - is_yellow_friendly=args.run_yellow, + yellow_is_friendly=args.run_yellow, + ), + layout_path=None, + ) + + # connect the keyboard estop toggle to the key event if needed + if estop_mode == EstopMode.KEYBOARD_ESTOP: + tscope.keyboard_estop_shortcut.activated.connect( + rc_friendly.toggle_keyboard_estop + ) + # we call this method to enable estop automatically when a field test starts + rc_friendly.toggle_keyboard_estop() + logger.warning( + "\x1b[31;20m" + + "Keyboard Estop Enabled, robots will start moving automatically when test starts!" + + "\x1b[0m" ) - friendly_proto_unix_io.register_observer(World, runner.world_buffer) - - # Setup proto loggers. - # - # NOTE: Its important we use the test runners time provider because - # test will run as fast as possible with a varying tick rate. The - # SimulatorTestRunner time provider is tied to the simulators - # t_capture coming out of the wrapper packet (rather than time.time). - with ProtoLogger( - f"{args.blue_full_system_runtime_dir}/logs/{current_test}", - time_provider=runner.time_provider, - ) as blue_logger, ProtoLogger( - f"{args.yellow_full_system_runtime_dir}/logs/{current_test}", - time_provider=runner.time_provider, - ) as yellow_logger: - blue_full_system_proto_unix_io.register_to_observe_everything( - blue_logger.buffer - ) - yellow_full_system_proto_unix_io.register_to_observe_everything( - yellow_logger.buffer - ) - yield runner - print( - f"\n\nTo replay this test for the blue team, go to the `src` folder and run \n./tbots.py run thunderscope --blue_log {blue_logger.log_folder}", - flush=True, - ) - print( - f"\n\nTo replay this test for the yellow team, go to the `src` folder and run \n./tbots.py run thunderscope --yellow_log {yellow_logger.log_folder}", - flush=True, - ) + time.sleep(LAUNCH_DELAY_S) + runner = FieldTestRunner( + test_name=current_test, + blue_full_system_proto_unix_io=blue_full_system_proto_unix_io, + yellow_full_system_proto_unix_io=yellow_full_system_proto_unix_io, + gamecontroller=gamecontroller, + thunderscope=tscope, + is_yellow_friendly=args.run_yellow, + ) + + friendly_proto_unix_io.register_observer(World, runner.world_buffer) + + # Setup proto loggers. + # + # NOTE: Its important we use the test runners time provider because + # test will run as fast as possible with a varying tick rate. The + # SimulatorTestRunner time provider is tied to the simulators + # t_capture coming out of the wrapper packet (rather than time.time). + with ProtoLogger( + f"{args.blue_full_system_runtime_dir}/logs/{current_test}", + time_provider=runner.time_provider, + ) as blue_logger, ProtoLogger( + f"{args.yellow_full_system_runtime_dir}/logs/{current_test}", + time_provider=runner.time_provider, + ) as yellow_logger: + blue_full_system_proto_unix_io.register_to_observe_everything( + blue_logger.buffer + ) + yellow_full_system_proto_unix_io.register_to_observe_everything( + yellow_logger.buffer + ) + yield runner + print( + f"\n\nTo replay this test for the blue team, go to the `src` folder and run \n./tbots.py run thunderscope --blue_log {blue_logger.log_folder}", + flush=True, + ) + print( + f"\n\nTo replay this test for the yellow team, go to the `src` folder and run \n./tbots.py run thunderscope --yellow_log {yellow_logger.log_folder}", + flush=True, + ) diff --git a/src/software/networking/ssl_proto_communication.py b/src/software/networking/ssl_proto_communication.py index 54a571977c..a6c4ac686a 100644 --- a/src/software/networking/ssl_proto_communication.py +++ b/src/software/networking/ssl_proto_communication.py @@ -34,9 +34,14 @@ def __init__(self, port: int) -> None: :param port the port to bind to """ - # bind to all local interfaces, TCP - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.connect(("", port)) + try: + # bind to all local interfaces, TCP + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect(("", port)) + except ConnectionRefusedError: + raise ConnectionRefusedError( + f"SSL Socket connection refused on port {port}. Is binary already running in a separate process?" + ) def send(self, proto: protobuf_message.Message) -> None: """ diff --git a/src/software/thunderscope/binary_context_managers/game_controller.py b/src/software/thunderscope/binary_context_managers/game_controller.py index a8c01f87cb..d85e66298a 100644 --- a/src/software/thunderscope/binary_context_managers/game_controller.py +++ b/src/software/thunderscope/binary_context_managers/game_controller.py @@ -1,5 +1,6 @@ from __future__ import annotations +import random import logging import os import socket @@ -21,29 +22,61 @@ class Gamecontroller(object): - """ Gamecontroller Context Manager """ + """Gamecontroller Context Manager""" CI_MODE_LAUNCH_DELAY_S = 0.3 REFEREE_IP = "224.5.23.1" CI_MODE_OUTPUT_RECEIVE_BUFFER_SIZE = 9000 - def __init__(self, supress_logs: bool = False) -> None: + def __init__(self, supress_logs: bool = False, use_conventional_port=False) -> None: """Run Gamecontroller :param supress_logs: Whether to suppress the logs + :not_launch_gc Whether to launch the gamecontroller or not! """ + self.supress_logs = supress_logs - # We need to find 2 free ports to use for the gamecontroller - # so that we can run multiple gamecontroller instances in parallel - self.referee_port = self.next_free_port() self.ci_port = self.next_free_port() + # we are not using conventional by default since most of the time + # we are not in competition and. Thus, we would be conflicting with + # each other if we are using conventional port + self.referee_port = self.next_free_port(random.randint(1024, 65535)) + if use_conventional_port: + if not self.is_valid_port(40000): + raise OSError("Cannot use port 40000 for Gamecontroller") + + self.referee_port = 40000 + self.ci_port = self.next_free_port() # this allows gamecontroller to listen to override commands self.command_override_buffer = ThreadSafeBuffer( buffer_size=2, protobuf_type=ManualGCCommand ) + @staticmethod + def get_referee_port_staticmethod(gamecontroller: Gamecontroller): + """ + return the default port if gamecontroller is None, otherwise the port that the gamecontroller is using. + + :param gamecontroller: the gamecontroller we are using + :return: the default port if gamecontroller is None, otherwise the port that the gamecontroller is using. + """ + if gamecontroller is not None: + return gamecontroller.get_referee_port() + + return 40000 + + def get_referee_port(self) -> int: + """ + Sometimes, the port that we are using changes depending on context. + We want a getter function that returns the port we are using. + + :return: the port that the game controller is currently using! + """ + + return self.referee_port + def __enter__(self) -> "self": """Enter the gamecontroller context manager. @@ -102,24 +135,35 @@ def refresh(self): ) manual_command = self.command_override_buffer.get(return_cached=False) - def next_free_port(self, port: int = 40000, max_port: int = 65535) -> None: + def is_valid_port(self, port): + """ + determine whether or not a given port is valid + + :param port: the port we are checking + :return: True if a port is valid False otherwise + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + try: + sock.bind(("", port)) + sock.close() + return True + except OSError: + return False + + def next_free_port(self, start_port: int = 40000, max_port: int = 65535) -> int: """Find the next free port. We need to find 2 free ports to use for the gamecontroller so that we can run multiple gamecontroller instances in parallel. - :param port: The port to start looking from + :param start_port: The port to start looking from :param max_port: The maximum port to look up to :return: The next free port """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - while port <= max_port: - try: - sock.bind(("", port)) - sock.close() - return port - except OSError: - port += 1 + while start_port <= max_port: + if self.is_valid_port(start_port): + return start_port + start_port += 1 raise IOError("no free ports") diff --git a/src/software/thunderscope/robot_communication.py b/src/software/thunderscope/robot_communication.py index 8f37b76664..b9cbb06279 100644 --- a/src/software/thunderscope/robot_communication.py +++ b/src/software/thunderscope/robot_communication.py @@ -27,6 +27,7 @@ def __init__( estop_path: os.PathLike = None, estop_baudrate: int = 115200, enable_radio: bool = False, + referee_port: int = SSL_REFEREE_PORT, ): """Initialize the communication with the robots @@ -37,8 +38,11 @@ def __init__( :param estop_path: The path to the estop :param estop_baudrate: The baudrate of the estop :param enable_radio: Whether to use radio to send primitives to robots + :param referee_port: the referee port that we are using """ + + self.referee_port = referee_port self.receive_ssl_referee_proto = None self.receive_ssl_wrapper = None self.sequence_number = 0 @@ -113,7 +117,7 @@ def setup_for_fullsystem(self) -> None: self.receive_ssl_referee_proto = tbots_cpp.SSLRefereeProtoListener( SSL_REFEREE_ADDRESS, - SSL_REFEREE_PORT, + self.referee_port, lambda data: self.current_proto_unix_io.send_proto(Referee, data), True, ) diff --git a/src/software/thunderscope/thunderscope_main.py b/src/software/thunderscope/thunderscope_main.py index fd53817470..ee940d5d3f 100644 --- a/src/software/thunderscope/thunderscope_main.py +++ b/src/software/thunderscope/thunderscope_main.py @@ -27,6 +27,7 @@ from software.thunderscope.binary_context_managers.full_system import FullSystem from software.thunderscope.binary_context_managers.simulator import Simulator from software.thunderscope.binary_context_managers.game_controller import Gamecontroller + from software.thunderscope.binary_context_managers.tigers_autoref import TigersAutoref @@ -218,6 +219,13 @@ help="Whether to populate with default robot positions (False) or start with an empty field (True) for AI vs AI", ) + parser.add_argument( + "--launch_gc", + action="store_true", + default=False, + help="whether or not to launch the gamecontroller when --run_blue or --run_yellow is ran", + ) + args = parser.parse_args() # Sanity check that an interface was provided @@ -310,13 +318,18 @@ args.keyboard_estop, args.disable_communication ) - with RobotCommunication( + with ( + Gamecontroller(supress_logs=(not args.verbose), use_conventional_port=False) + if args.launch_gc + else contextlib.nullcontext() + ) as gamecontroller, RobotCommunication( current_proto_unix_io=current_proto_unix_io, multicast_channel=getRobotMulticastChannel(args.channel), interface=args.interface, estop_mode=estop_mode, estop_path=estop_path, enable_radio=args.enable_radio, + referee_port=Gamecontroller.get_referee_port_staticmethod(gamecontroller), ) as robot_communication: if estop_mode == EstopMode.KEYBOARD_ESTOP: