diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bdfda36..ff3070e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,22 +4,25 @@ on: push: workflow_dispatch: +env: + WEBOTS_VER: "2025a" + jobs: test: strategy: fail-fast: false matrix: os: [ubuntu-latest] - py_version: ["3.8", "3.9", "3.10", "3.11"] + py_version: ["3.10", "3.11", "3.12", "3.13"] include: - os: windows-latest - py_version: "3.8" + py_version: "3.10" - os: windows-latest - py_version: "3.11" + py_version: "3.13" - os: macos-latest - py_version: "3.8" + py_version: "3.10" - os: macos-latest - py_version: "3.11" + py_version: "3.13" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -36,8 +39,9 @@ jobs: - name: Download & install Webots (Linux) if: runner.os == 'Linux' run: | + sudo apt-get update wget -O ./webots.deb \ - https://github.com/cyberbotics/webots/releases/download/R2023b/webots_2023b_amd64.deb + https://github.com/cyberbotics/webots/releases/download/R$WEBOTS_VER/webots_${WEBOTS_VER}_amd64.deb sudo apt-get install --yes ./webots.deb - name: Download & extract Webots (Windows) # Windows install was problematic so we just extract the python library for typehints @@ -46,14 +50,14 @@ jobs: run: | mkdir stubs C:\\msys64\\usr\\bin\\wget.exe -O ./webots.tar.bz2 \ - https://github.com/cyberbotics/webots/releases/download/R2023b/webots-R2023b-x86-64.tar.bz2 + https://github.com/cyberbotics/webots/releases/download/R$WEBOTS_VER/webots-R$WEBOTS_VER-x86-64.tar.bz2 C:\\msys64\\usr\\bin\\tar.exe xvf webots.tar.bz2 --strip-components=4 \ --directory=stubs webots/lib/controller/python/controller - name: Download & install Webots (macOS) if: runner.os == 'macOS' run: | wget -O ./webots.dmg \ - https://github.com/cyberbotics/webots/releases/download/R2023b/webots-R2023b.dmg + https://github.com/cyberbotics/webots/releases/download/R$WEBOTS_VER/webots-R$WEBOTS_VER.dmg hdiutil mount ./webots.dmg sudo cp -R /Volumes/Webots/Webots.app /Applications hdiutil unmount /Volumes/Webots diff --git a/requirements.txt b/requirements.txt index ff61e2b..68d87cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ -sr-robot3==2025.0.1 -april_vision==2.2.0 -opencv-python-headless >=4.8.0.76,<5 +sr-robot3==2026.0.1 +april_vision==3.0.0 +opencv-python-headless >=4.10,<5 -# Library versions are selected to provide support for Python 3.9-3.12 +# Library versions are selected to provide support for Python 3.10-3.13 flask==2.3.3 -matplotlib==3.9.2 +matplotlib==3.10.6 networkx==3.1 -numpy==1.26.4 -pandas==2.2.3 -pillow==11.0.0 -scikit-learn==1.3.1 -scipy==1.11.2 -shapely==2.0.6 +numpy>=2.2.6,<=2.3.2 +pandas==2.3.2 # no 3.14 support +pillow==11.3.0 +scikit-learn==1.7.1 # no 3.14 support +scipy>=1.15.3,<=1.16.1 +shapely==2.1.1 # no 3.14 support diff --git a/scripts/run_simulator.py b/scripts/run_simulator.py index e4a6a7d..fe311fc 100755 --- a/scripts/run_simulator.py +++ b/scripts/run_simulator.py @@ -14,6 +14,10 @@ from shutil import which from subprocess import Popen +BOLD_RED = '\x1b[31;1m' +RESET_COLOUR = '\x1b[0m' + + if sys.platform == "win32": from subprocess import CREATE_NEW_PROCESS_GROUP, DETACHED_PROCESS @@ -51,6 +55,10 @@ def get_webots_parameters() -> tuple[Path, Path]: if not (SIM_BASE / "venv").exists(): raise RuntimeError("Please run the setup.py script before running the simulator.") + # Check setup finish successfully + if not (SIM_BASE / "venv/setup_success").exists(): + raise RuntimeError("Setup has not completed successfully. Please re-run the setup.py script.") + # Check if Webots is in the PATH webots = which("webots") @@ -86,13 +94,15 @@ def main() -> None: else: Popen([str(webots), str(world_file)], start_new_session=True) except RuntimeError as e: - print(f"An error occurred: {e}") - input("Press enter to continue...") + print(BOLD_RED) + print(f"An error occurred: \n{e}") + input(f"Press enter to continue...{RESET_COLOUR}") exit(1) except Exception as e: + print(BOLD_RED) print(f"An error occurred: {e}") print(traceback.format_exc()) - input("Press enter to continue...") + input(f"Press enter to continue...{RESET_COLOUR}") exit(1) diff --git a/scripts/setup.py b/scripts/setup.py index bbf1d30..db75d0a 100755 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -15,12 +15,16 @@ import shutil import sys from pathlib import Path -from subprocess import run +from subprocess import SubprocessError, check_call from venv import create logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s") logger = logging.getLogger(__name__) +BOLD_RED = '\x1b[31;1m' +GREEN = '\x1b[32;20m' +RESET_COLOUR = '\x1b[0m' + def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: """ @@ -76,6 +80,9 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: venv_dir = project_root / "venv" + # Reset success flag + (venv_dir / "setup_success").unlink(missing_ok=True) + logger.info(f"Creating virtual environment in {venv_dir.absolute()}") create(venv_dir, with_pip=True) @@ -86,11 +93,17 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: else: pip = venv_dir / "bin/pip" venv_python = venv_dir / "bin/python" - run( + check_call( [str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], cwd=venv_dir, ) - run([str(pip), "install", "-r", str(requirements)], cwd=venv_dir) + check_call( + [str(pip), "install", "--only-binary=:all:", "-r", str(requirements)], + cwd=venv_dir, + ) + + logger.info("Preloading OpenCV & sr.robot3") + check_call([str(venv_python), "-c", "import cv2;import sr.robot3"], cwd=venv_dir) logger.info("Setting up Webots Python location") @@ -100,14 +113,22 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: populate_python_config(usercode_ini, venv_python) populate_python_config(supervisor_ini, venv_python) + # Mark that we succeeded + (venv_dir / "setup_success").touch() + # repopulate zone 0 with example code if robot.py is missing zone_0 = project_root / "zone_0" if not (zone_0 / "robot.py").exists(): logger.info("Repopulating zone 0 with example code") zone_0.mkdir(exist_ok=True) shutil.copy(project_root / "example_robots/basic_robot.py", zone_0 / "robot.py") +except SubprocessError: + print(BOLD_RED) + logger.error("Setup failed due to an error.") + input(f"An error occurred, press enter to close.{RESET_COLOUR}") except Exception: + print(BOLD_RED) logger.exception("Setup failed due to an error.") - input("An error occurred, press enter to close.") + input(f"An error occurred, press enter to close.{RESET_COLOUR}") else: - input("Setup complete, press enter to close.") + input(f"{GREEN}Setup complete, press enter to close.{RESET_COLOUR}") diff --git a/simulator/controllers/competition_supervisor/competition_supervisor.py b/simulator/controllers/competition_supervisor/competition_supervisor.py index 6eafd23..08a7f26 100644 --- a/simulator/controllers/competition_supervisor/competition_supervisor.py +++ b/simulator/controllers/competition_supervisor/competition_supervisor.py @@ -226,6 +226,8 @@ def run_match( def main() -> None: """Run the competition supervisor.""" if is_dev_mode(): + robots = Robots() + robots.remove_unoccupied_robots() exit() match_data = get_match_data() diff --git a/simulator/modules/sbot_interface/boards/servo_board.py b/simulator/modules/sbot_interface/boards/servo_board.py index 536f623..0aa922e 100644 --- a/simulator/modules/sbot_interface/boards/servo_board.py +++ b/simulator/modules/sbot_interface/boards/servo_board.py @@ -44,7 +44,7 @@ def handle_command(self, command: str) -> str: if args[0] == '*IDN?': return f'Student Robotics:SBv4B:{self.asset_tag}:{self.software_version}' elif args[0] == '*STATUS?': - return f"{self.watchdog_fail}:{self.pgood}" + return f"{self.watchdog_fail:d}:{self.pgood:d}" elif args[0] == '*RESET': LOGGER.info(f'Resetting servo board {self.asset_tag}') for servo in self.servos: diff --git a/simulator/modules/sbot_interface/devices/servo.py b/simulator/modules/sbot_interface/devices/servo.py index 92e7e07..a71f040 100644 --- a/simulator/modules/sbot_interface/devices/servo.py +++ b/simulator/modules/sbot_interface/devices/servo.py @@ -20,8 +20,10 @@ if TYPE_CHECKING: from controller import PositionSensor -MAX_POSITION = 2000 -MIN_POSITION = 1000 +MAX_POSITION = 4000 +MIN_POSITION = 500 +SERVO_MAX = 2000 +SERVO_MIN = 1000 class BaseServo(ABC): @@ -98,7 +100,7 @@ class Servo(BaseServo): """A servo connected to the Servo board.""" def __init__(self, device_name: str) -> None: - self.position = (MAX_POSITION + MIN_POSITION) // 2 + self.position = (SERVO_MAX + SERVO_MIN) // 2 # TODO use setAvailableForce to simulate disabled self._enabled = False g = get_globals() @@ -121,11 +123,11 @@ def set_position(self, value: int) -> None: """ # Apply a small amount of variation to the power setting to simulate # inaccuracies in the servo - value = int(add_jitter(value, (MIN_POSITION, MAX_POSITION), std_dev_percent=0.5)) + value = int(add_jitter(value, (SERVO_MIN, SERVO_MAX), std_dev_percent=0.5)) self._device.setPosition(map_to_range( value, - (MIN_POSITION, MAX_POSITION), + (SERVO_MIN, SERVO_MAX), (self._min_position + 0.001, self._max_position - 0.001), )) self.position = value