diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bdfda36..9e68079 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,29 +4,32 @@ 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.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 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 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.py_version }} - name: Install dependencies @@ -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 @@ -74,11 +78,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python 3.10 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install release dependencies @@ -99,7 +103,7 @@ jobs: unzip dist/*.zip -d dist/ rm dist/sbot-simulator*.zip - name: Save built release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ steps.release.outputs.RELEASE_NAME }} path: | diff --git a/.gitignore b/.gitignore index 7954551..a4f8db0 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,4 @@ cython_debug/ /simulator/controllers/competition_supervisor/runtime.ini /zone_* /dist +/uv.lock diff --git a/example_robots/basic_robot.py b/example_robots/basic_robot.py index c87070c..1a16fea 100644 --- a/example_robots/basic_robot.py +++ b/example_robots/basic_robot.py @@ -1,7 +1,5 @@ from sbot import arduino, motors, utils -# robot = Robot() - motors.set_power(0, 1) motors.set_power(1, 1) diff --git a/requirements.txt b/requirements.txt index 14fa6a6..fec2b47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -sbot==2025.1.1 -april_vision==3.0.0 +sbot==2026.1.0 +april_vision==3.1.0 opencv-python-headless >=4.10,<5 diff --git a/scripts/generate_release.py b/scripts/generate_release.py index 8b7aa8b..d06cc32 100755 --- a/scripts/generate_release.py +++ b/scripts/generate_release.py @@ -84,6 +84,7 @@ script_dir = temp_dir / "scripts" script_dir.mkdir() + shutil.copy(project_root / "scripts/setup_python_fix.py", script_dir) logger.info("Copying example code to temp directory") shutil.copytree(project_root / "example_robots", temp_dir / "example_robots") 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 b5776a0..1d2d562 100755 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -8,6 +8,7 @@ 3. Set the python path in runtime.ini to the virtual environment python 4. Repopulate the zone 0 folder with basic_robot.py if robot.py is missing """ + from __future__ import annotations import logging @@ -16,11 +17,14 @@ import sys from pathlib import Path 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: """ @@ -50,18 +54,20 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: elif not in_python_section: runtime_content.append(line) - runtime_content.extend([ - "", - "[python]", - f"COMMAND = {venv_python.absolute()}", - "", - ]) + runtime_content.extend( + [ + "", + "[python]", + f"COMMAND = {venv_python.absolute()}", + "", + ] + ) - runtime_ini.write_text('\n'.join(runtime_content)) + runtime_ini.write_text("\n".join(runtime_content)) try: - if (Path(__file__).parent / 'simulator/VERSION').exists(): + if (Path(__file__).parent / "simulator/VERSION").exists(): # This is running from a release print("Running in release mode") project_root = Path(__file__).parent @@ -76,8 +82,11 @@ 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) + check_call([sys.executable, "-m", "venv", venv_dir]) logger.info(f"Installing dependencies from {requirements.absolute()}") if platform.system() == "Windows": @@ -90,7 +99,13 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: [str(venv_python), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"], cwd=venv_dir, ) - check_call([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") + check_call([str(venv_python), "-c", "import cv2"], cwd=venv_dir) logger.info("Setting up Webots Python location") @@ -100,6 +115,9 @@ 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(): @@ -107,10 +125,12 @@ def populate_python_config(runtime_ini: Path, venv_python: Path) -> None: 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("An error occurred, press enter to close.") + 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/scripts/setup_python_fix.py b/scripts/setup_python_fix.py new file mode 100644 index 0000000..5d16565 --- /dev/null +++ b/scripts/setup_python_fix.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +A script to setup the environment for running the project in Webots. + +It will: +1. Create a setup virtual environment and install uv into it +2. Use uv to run the standard setup using the desired Python version +""" +import logging +import platform +import sys +from pathlib import Path +from subprocess import SubprocessError, check_call + +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' + +PYTHON_VERSION = "3.13" + +try: + if (Path(__file__).parent / 'simulator/VERSION').exists(): + # This is running from a release + print("Running in release mode") + project_root = Path(__file__).parent + else: + # This is running from the repository + print("Running in development mode") + project_root = Path(__file__).parents[1] + + print(f"Starting with python version: {sys.version} on {platform.platform()}") + + venv_dir = project_root / "simulator/setup_venv" + script_dir = Path(__file__).parent + + check_call([sys.executable, "-m", "venv", venv_dir]) + + logger.info("Installing uv") + if platform.system() == "Windows": + venv_bin_dir = venv_dir / "Scripts" + pip = venv_bin_dir / "pip.exe" + uv_bin = venv_bin_dir / "uv.exe" + else: + venv_bin_dir = venv_dir / "bin" + pip = venv_bin_dir / "pip" + uv_bin = venv_bin_dir / "uv" + + check_call( + [pip, "install", "--only-binary=:all:", "--upgrade", "wheel", "uv"], + cwd=venv_dir, + ) + + logger.info(f"Running setup using Python {PYTHON_VERSION}") + setup_script = script_dir.relative_to(project_root) / "setup.py" + check_call([uv_bin, "run", "--python", PYTHON_VERSION, setup_script], cwd=project_root) +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(f"An error occurred, press enter to close.{RESET_COLOUR}") diff --git a/simulator/worlds/Arena.afphoto b/simulator/worlds/Arena.afphoto new file mode 100644 index 0000000..7faa1fc Binary files /dev/null and b/simulator/worlds/Arena.afphoto differ diff --git a/simulator/worlds/arena_floor.png b/simulator/worlds/arena_floor.png index 62abce7..e0b0bcf 100644 Binary files a/simulator/worlds/arena_floor.png and b/simulator/worlds/arena_floor.png differ