Skip to content
20 changes: 12 additions & 8 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 11 additions & 11 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
16 changes: 13 additions & 3 deletions scripts/run_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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)


Expand Down
31 changes: 26 additions & 5 deletions scripts/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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)

Expand All @@ -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")

Expand All @@ -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}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought: Should we be explicit here that the setup failed and that the simulator isn't usable? I wonder if people will just see "press enter to close" and keep following the docs.

Alternatively, perhaps this is something we can put in the docs instead?

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}")
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion simulator/modules/sbot_interface/boards/servo_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 7 additions & 5 deletions simulator/modules/sbot_interface/devices/servo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
Loading