From ca104c5d441904c080c4e1854db59e6070ddcadd Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 22:53:48 +0000 Subject: [PATCH 01/25] use newer `importlib.util.find_spec` function (other was removed) --- robot/__init__.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/robot/__init__.py b/robot/__init__.py index 0c1c77f..534c837 100644 --- a/robot/__init__.py +++ b/robot/__init__.py @@ -5,7 +5,7 @@ import importlib -has_picamera = importlib.find_loader("picamera") is not None +has_picamera = importlib.util.find_spec("picamera") is not None if not has_picamera: import sys diff --git a/setup.py b/setup.py index 7c93982..a3f570e 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ packages=["robot"], install_requires=[ + "fake-rpi>=0.7.1", ], author="Skyler Grey", From dc4d6200aeb771c57536e1d241012d36716c7acf Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 23:08:06 +0000 Subject: [PATCH 02/25] use relative imports --- robot/apriltags3.py | 2 +- robot/cytron.py | 2 +- robot/reset.py | 4 ++-- robot/vision.py | 5 ++--- robot/wrapper.py | 10 ++++------ 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/robot/apriltags3.py b/robot/apriltags3.py index ff344fa..99db0c2 100755 --- a/robot/apriltags3.py +++ b/robot/apriltags3.py @@ -21,7 +21,7 @@ import numpy as np import scipy.spatial.transform as transform -from robot.game_config import MARKER, MARKER_TYPE +from .game_config import MARKER, MARKER_TYPE ###################################################################### diff --git a/robot/cytron.py b/robot/cytron.py index 55d1e97..948eb5b 100644 --- a/robot/cytron.py +++ b/robot/cytron.py @@ -3,7 +3,7 @@ to apply """ import wiringpi as wp -from robot.greengiant import clamp +from .greengiant import clamp _MAX_OUTPUT_VOLTAGE = 12 diff --git a/robot/reset.py b/robot/reset.py index a8f907a..2217714 100644 --- a/robot/reset.py +++ b/robot/reset.py @@ -10,8 +10,8 @@ https://stackoverflow.com/a/45799209/5006710 """ from smbus2 import SMBus -import robot.cytron as c -import robot.greengiant as gg +import .cytron as c +import .greengiant as gg def reset(): diff --git a/robot/vision.py b/robot/vision.py index 4b1d211..0c33ac2 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -12,14 +12,13 @@ from typing import NamedTuple, Any -from robot.game_config import MARKER, WHITE -from .game_config import BASE_MARKER as MarkerInfo +from .game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo import cv2 import numpy as np import picamera2 -import robot.apriltags3 as AT +import .apriltags3 as AT # TODO put all of the paths together diff --git a/robot/wrapper.py b/robot/wrapper.py index 3f1711d..bb0ff1b 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -17,12 +17,10 @@ class to their respecitve classes from datetime import datetime from smbus2 import SMBus -from robot import vision -from robot.cytron import CytronBoard -from robot.greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE -from robot.game_config import TEAM -from . import game_config -from robot.game_config import POEM_ON_STARTUP +from .cytron import CytronBoard +from .greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE +from .game_config import TEAM, POEM_ON_STARTUP +from . import game_config, vision from hopper.client import * from hopper.common import * From c6811a174dd9232dd2f92567b3638b56784e09c8 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 23:31:46 +0000 Subject: [PATCH 03/25] add dependency packages to `setup.py` --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index a3f570e..5fdb269 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,11 @@ packages=["robot"], install_requires=[ + "smbus2>=0.4.2" "fake-rpi>=0.7.1", + "opencv3>=3.4.18", + "scipy>=1.9.1", + "wiringpi>=2.60.1", ], author="Skyler Grey", From 40319ca95e035830cc3c2b78b3e2cc68a0118f4c Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 23:34:30 +0000 Subject: [PATCH 04/25] fix minor syntax issue --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5fdb269..8134e01 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ packages=["robot"], install_requires=[ - "smbus2>=0.4.2" + "smbus2>=0.4.2", "fake-rpi>=0.7.1", "opencv3>=3.4.18", "scipy>=1.9.1", From 322f988f4ebda76603702663d0951f9b7ee102a4 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 23:38:46 +0000 Subject: [PATCH 05/25] fix `opencv` dependency name and version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8134e01..e96ec05 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ install_requires=[ "smbus2>=0.4.2", "fake-rpi>=0.7.1", - "opencv3>=3.4.18", + "opencv-python-headless~=3.14.0", "scipy>=1.9.1", "wiringpi>=2.60.1", ], From c0ea79b58ff8c8a5724dda9bdf87be44e70bbbbe Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 7 Feb 2026 23:40:12 +0000 Subject: [PATCH 06/25] fix yet another typo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e96ec05..8d021e6 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ install_requires=[ "smbus2>=0.4.2", "fake-rpi>=0.7.1", - "opencv-python-headless~=3.14.0", + "opencv-python-headless~=3.4.0", "scipy>=1.9.1", "wiringpi>=2.60.1", ], From 270934f4745c924dded2eef94a92bc70d9d34a6a Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Wed, 18 Feb 2026 17:21:00 +0000 Subject: [PATCH 07/25] remove `wiringpi` from dependency list --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 8d021e6..19bb60a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,6 @@ "fake-rpi>=0.7.1", "opencv-python-headless~=3.4.0", "scipy>=1.9.1", - "wiringpi>=2.60.1", ], author="Skyler Grey", From 8a4086b6a28effedbb3f321b74c017d2bf9d534f Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 28 Feb 2026 11:28:23 +0000 Subject: [PATCH 08/25] refactor(cytron): replace `wiringpi` with `RPi.GPIO` - `wiringpi` is now deprecated, replace it with `RPi.GPIO` as used in the rest of robot library. --- robot/cytron.py | 110 ++++++++++++++++++++---------------------------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/robot/cytron.py b/robot/cytron.py index 948eb5b..057e7fb 100644 --- a/robot/cytron.py +++ b/robot/cytron.py @@ -1,45 +1,26 @@ -"""An interface to the cyctron motor board. A gpio pin is used for each motor +""" +An interface to the cyctron motor board. A GPIO pin is used for each motor to give direction and has a PWM signal at 100Hz giving infomation about voltage to apply """ -import wiringpi as wp + +import RPi.GPIO as GPIO from .greengiant import clamp _MAX_OUTPUT_VOLTAGE = 12 -_PWM_PIN_1 = 26 -_PWM_PIN_2 = 23 -_DIR_PIN_1 = 25 -_DIR_PIN_2 = 5 - -_WP_OUT = 1 -_WP_PWM = 2 - -# Wiring pi's PWM has range 0-1024 but we want to present a range of 0-100 -_WP_PWM_MAX = 1024 -_RC_PWM_MAX = 100 - - -def wp_to_rc_pwm(wp_pwm): - """Convert from wiring pi's numbering to percentages""" - return (wp_pwm * _RC_PWM_MAX)/_WP_PWM_MAX - - -def rc_to_wp_pwm(rc_pwm): - """Convert from percenatges to wiring pi's numbering""" - return int((rc_pwm * _WP_PWM_MAX)/_RC_PWM_MAX) +_PWM_PIN_1 = 12 +_PWM_PIN_2 = 13 +_DIR_PIN_1 = 26 +_DIR_PIN_2 = 24 - -class CytronBoard(): +class CytronBoard: def __init__(self, max_motor_voltage): - """The interface to the CytronBoard + """ + The interface to the CytronBoard max_motor_voltage - The motors will be scaled so that this is the maxium - average voltage the Cyctron will output + average voltage the Cytron will output """ - # Set up Wiring Pi following the wiring pi numbering scheme - if wp.wiringPiSetup() != 0: - raise RuntimeError("Failed to init Wiring Pi") - if not (0 <= max_motor_voltage <= 12): raise ValueError("max_motor_voltage must satisfy 0 <= " "max_motor_voltage <= 12 but instead is " @@ -50,49 +31,50 @@ def __init__(self, max_motor_voltage): self.power_scaling_factor = ( max_motor_voltage / _MAX_OUTPUT_VOLTAGE) ** 2 - self._percentages = [0, 0] - self._dir = [_DIR_PIN_1, _DIR_PIN_2] - self._pwm_pins = [_PWM_PIN_1, _PWM_PIN_2] - - wp.pinMode(_DIR_PIN_1, _WP_OUT) - wp.pinMode(_DIR_PIN_2, _WP_OUT) - wp.pinMode(_PWM_PIN_1, _WP_PWM) - wp.pinMode(_PWM_PIN_2, _WP_PWM) - - wp.pwmSetClock(3000) - for pin in self._pwm_pins: - wp.pwmWrite(pin, 0) + self._dir = [ + (GPIO.LOW, _DIR_PIN_1), + (GPIO.LOW, _DIR_PIN_2), + ] + self._pwm = [ + (0, GPIO.PWM(_PWM_PIN_1, 100)), + (0, GPIO.PWM(_PWM_PIN_2, 100)), + ] + + GPIO.setmode(GPIO.BCM) + GPIO.setup(_DIR_PIN_1, GPIO.OUT) + GPIO.setup(_DIR_PIN_2, GPIO.OUT) + GPIO.setup(_PWM_PIN_1, GPIO.OUT) + GPIO.setup(_PWM_PIN_2, GPIO.OUT) def __getitem__(self, index): - """Returns the current PWM value in RC units. Adds a sign to represent""" - if index not in (1, 2): + """Returns current motor PWM value as a percentage""" + if index not in (0, 1): raise IndexError( - f"motor index must be in (1,2) but instead got {index}") + f"Motor index must be in (0,1) but instead got {index}") - index -= 1 - return self._percentages[index] + return self._pwm[index][0] def __setitem__(self, index, percent): - """Clamps input value, converts from percentage to wiring pi format and - sets a PWM format""" - if index not in (1, 2): + """Set current motor PWM percentage value""" + if index not in (0, 1): raise IndexError( - f"motor index must be in (1,2) but instead got {index}") + f"Motor index must be in (0,1) but instead got {index}") - index -= 1 - percent = clamp(percent, -100, 100) - self._percentages[index] = percent + if percent < 0: + self._dir[index][0] = GPIO.LOW + else: + self._dir[index][0] = GPIO.HIGH - direction = (percent < 0) - wp.digitalWrite(self._dir[index], direction) + GPIO.output(self._dir[index][1], self._dir[index][0]) + + percent = clamp(percent, -100, 100) + self._pwm[index][0] = percent - # Scale such that 50% with a motor limit of 6V is really 3V - scaled_value = abs(percent) * self.power_scaling_factor - wp_value = rc_to_wp_pwm(scaled_value) - wp.pwmWrite(self._pwm_pins[index], wp_value) + percent = abs(percent) * self.power_scaling_factor + self._pwm[index][1].start(percent) def stop(self): """Turns motors off""" - for pwm_pin, percentage in zip(self._pwm_pins, self._percentages): - wp.pwmWrite(pwm_pin, 0) - percentage = 0 + for i in range(len(self._pwm)): + self._pwm[i][0] = 0 + self._pwm[i][1].start(0) From e841d4910236c2b25f74316b825ea53e7b6165aa Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 28 Feb 2026 11:34:17 +0000 Subject: [PATCH 09/25] fix relative imports --- robot/apriltags3.py | 2 +- robot/vision.py | 2 +- robot/wrapper.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/robot/apriltags3.py b/robot/apriltags3.py index 99db0c2..ff344fa 100755 --- a/robot/apriltags3.py +++ b/robot/apriltags3.py @@ -21,7 +21,7 @@ import numpy as np import scipy.spatial.transform as transform -from .game_config import MARKER, MARKER_TYPE +from robot.game_config import MARKER, MARKER_TYPE ###################################################################### diff --git a/robot/vision.py b/robot/vision.py index 0c33ac2..876ef08 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -12,7 +12,7 @@ from typing import NamedTuple, Any -from .game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo +from robot.game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo import cv2 import numpy as np diff --git a/robot/wrapper.py b/robot/wrapper.py index bb0ff1b..805ad39 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -19,7 +19,7 @@ class to their respecitve classes from .cytron import CytronBoard from .greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE -from .game_config import TEAM, POEM_ON_STARTUP +from robot.game_config import TEAM, POEM_ON_STARTUP from . import game_config, vision from hopper.client import * From b977693ad4fe1c86b4d88bb46907b1e6c90c9865 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Thu, 12 Mar 2026 15:18:25 +0000 Subject: [PATCH 10/25] Revert "fix relative imports" This reverts commit e841d4910236c2b25f74316b825ea53e7b6165aa. --- robot/apriltags3.py | 2 +- robot/vision.py | 2 +- robot/wrapper.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/robot/apriltags3.py b/robot/apriltags3.py index ff344fa..99db0c2 100755 --- a/robot/apriltags3.py +++ b/robot/apriltags3.py @@ -21,7 +21,7 @@ import numpy as np import scipy.spatial.transform as transform -from robot.game_config import MARKER, MARKER_TYPE +from .game_config import MARKER, MARKER_TYPE ###################################################################### diff --git a/robot/vision.py b/robot/vision.py index 876ef08..0c33ac2 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -12,7 +12,7 @@ from typing import NamedTuple, Any -from robot.game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo +from .game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo import cv2 import numpy as np diff --git a/robot/wrapper.py b/robot/wrapper.py index 805ad39..bb0ff1b 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -19,7 +19,7 @@ class to their respecitve classes from .cytron import CytronBoard from .greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE -from robot.game_config import TEAM, POEM_ON_STARTUP +from .game_config import TEAM, POEM_ON_STARTUP from . import game_config, vision from hopper.client import * From 9cfcbbf9de700d8b4c96f8efdf2de6f0773b8564 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Thu, 12 Mar 2026 15:25:21 +0000 Subject: [PATCH 11/25] remove weird `game_config` import in wrapper --- robot/wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robot/wrapper.py b/robot/wrapper.py index bb0ff1b..f4bf2db 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -20,7 +20,7 @@ class to their respecitve classes from .cytron import CytronBoard from .greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE from .game_config import TEAM, POEM_ON_STARTUP -from . import game_config, vision +from . import vision from hopper.client import * from hopper.common import * @@ -67,7 +67,7 @@ def __init__(self, start_enable_5v = True, ): - self.zone = game_config.TEAM.RED + self.zone = TEAM.RED self.mode = "competition" self._max_motor_voltage = max_motor_voltage From 61a61247d06b69fe06ee85ae1bc9614e501a71b1 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 14 Mar 2026 12:32:26 +0000 Subject: [PATCH 12/25] export `game_config` submodule properly? --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 19bb60a..5130f2c 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ -from setuptools import setup +from setuptools import setup, find_packages setup( name="robot", version="2024.1", - packages=["robot"], + packages=find_packages(), install_requires=[ "smbus2>=0.4.2", From fe140a53483ab53652d6bff7b197d55d98d9e5fe Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 14 Mar 2026 12:35:46 +0000 Subject: [PATCH 13/25] fix relative imports (yet again) --- robot/reset.py | 4 ++-- robot/vision.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/robot/reset.py b/robot/reset.py index 2217714..a8f907a 100644 --- a/robot/reset.py +++ b/robot/reset.py @@ -10,8 +10,8 @@ https://stackoverflow.com/a/45799209/5006710 """ from smbus2 import SMBus -import .cytron as c -import .greengiant as gg +import robot.cytron as c +import robot.greengiant as gg def reset(): diff --git a/robot/vision.py b/robot/vision.py index 0c33ac2..7c17654 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -18,7 +18,7 @@ import numpy as np import picamera2 -import .apriltags3 as AT +import robot.apriltags3 as AT # TODO put all of the paths together From d24f24989aa0f64fef8b1d2dccdbb552bba191f7 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 14 Mar 2026 13:25:50 +0000 Subject: [PATCH 14/25] force numpy < 2.0.0 for opencv --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 5130f2c..6fede41 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ "fake-rpi>=0.7.1", "opencv-python-headless~=3.4.0", "scipy>=1.9.1", + "numpy<2.0.0", ], author="Skyler Grey", From e0bf369c95d92a13ab3d09835a9746e571debae5 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 14 Mar 2026 14:44:08 +0000 Subject: [PATCH 15/25] add mock picamera2 module --- robot/__init__.py | 7 ++++--- robot/mock_picamera2.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 robot/mock_picamera2.py diff --git a/robot/__init__.py b/robot/__init__.py index 534c837..c81b116 100644 --- a/robot/__init__.py +++ b/robot/__init__.py @@ -5,15 +5,16 @@ import importlib -has_picamera = importlib.util.find_spec("picamera") is not None +has_picamera2 = importlib.util.find_spec("picamera2") is not None -if not has_picamera: +if not has_picamera2: import sys import fake_rpi + import robot.mock_picamera2 as mock_picamera2 sys.modules["RPi"] = fake_rpi.RPi sys.modules["RPi.GPIO"] = fake_rpi.RPi.GPIO - sys.modules["picamera"] = fake_rpi.picamera + sys.modules["picamera2"] = mock_picamera2 sys.modules["smbus2"] = fake_rpi.smbus import sys diff --git a/robot/mock_picamera2.py b/robot/mock_picamera2.py new file mode 100644 index 0000000..6e81914 --- /dev/null +++ b/robot/mock_picamera2.py @@ -0,0 +1,33 @@ +class Picamera2: + """ Mock Picamera2 class """ + + ERROR = None + + camera_properties = {"Model": "Mock Picamera2"} + + def __init__(self): + pass + + def configure(self, _): + pass + + def start(self): + pass + + def stop(self): + pass + + def close(self): + pass + + def capture_array(self): + return None + + def create_still_configuration(self, _): + return {} + + @staticmethod + def set_logging(_): + pass + +print("<<< WARN: using mock picamera2 >>>") From f73abd77f548aedb88888601cdf2812d9808292c Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 21 Mar 2026 11:16:39 +0000 Subject: [PATCH 16/25] feat(wrapper): use new hopper modules for start fifo --- robot/wrapper.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/robot/wrapper.py b/robot/wrapper.py index f4bf2db..f141e4b 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -22,8 +22,7 @@ class to their respecitve classes from .game_config import TEAM, POEM_ON_STARTUP from . import vision -from hopper.client import * -from hopper.common import * +from hopper import HopperPipe, HopperPipeType, JsonReader _logger = logging.getLogger("robot") @@ -75,21 +74,9 @@ def __init__(self, self._start_pressed = False self._warnings = [] - # Initialize a RcMuxClient and open the start pipe - self._hopper_client = HopperClient() - self._start_pipe = PipeName((PipeType.OUTPUT, "start-button", "robot"), "/home/pi/pipes") - self._hopper_client.open_pipe(self._start_pipe, delete=True, create=True, blocking=True) # Make sure to use blocking mode, otherwise start button code fails - - self._log_pipe = PipeName((PipeType.INPUT, "log", "robot"), "/home/pi/pipes") - self._json_reader = JsonReader(self._hopper_client, self._start_pipe) - - # Close stdout and stderr - os.close(1) - os.close(2) - - # ...and open a pipe in its place - self._hopper_client.open_pipe(self._log_pipe, delete=True, create=True) - os.dup(self._hopper_client.get_pipe_by_pipe_name(self._log_pipe).fd) + # Initialize a RcMuxClient and open the start pipe + self._start_pipe = HopperPipe(HopperPipeType.OUT, "robot", "robot/control") + self._start_json_reader = JsonReader(self._start_pipe) self._parse_cmdline() @@ -282,7 +269,7 @@ def _get_start_info(self): """Get the start infomation from the named pipe""" # This call blocks until the start info is read - settings = self._json_reader.read() + settings = self._start_json_reader.read() assert "zone" in settings, "zone must be in startup info" if settings["zone"] not in range(4): From 5eeb8551c86514c74500840af643c26bb2ed649a Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 21 Mar 2026 11:48:08 +0000 Subject: [PATCH 17/25] declare `hopper` dependency, add `pyproject.toml` file, fix cytron issue --- pyproject.toml | 17 +++++++++++++++++ robot/cytron.py | 8 ++++---- setup.py | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..376132a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "robot" +version = "2024.1" +readme = "README.md" +requires-python = ">=3.6" +dependencies = [ + "smbus2>=0.4.2", + "opencv-python-headless~=3.4.0", + "scipy>=1.9.1", + "numpy<2.0.0", + "hopper @ git+https://github.com/systemetric/hopper.git", +] + +[dependency-groups] +dev = [ + "fake-rpi>=0.7.1", +] diff --git a/robot/cytron.py b/robot/cytron.py index 057e7fb..c91f420 100644 --- a/robot/cytron.py +++ b/robot/cytron.py @@ -32,12 +32,12 @@ def __init__(self, max_motor_voltage): max_motor_voltage / _MAX_OUTPUT_VOLTAGE) ** 2 self._dir = [ - (GPIO.LOW, _DIR_PIN_1), - (GPIO.LOW, _DIR_PIN_2), + [GPIO.LOW, _DIR_PIN_1], + [GPIO.LOW, _DIR_PIN_2], ] self._pwm = [ - (0, GPIO.PWM(_PWM_PIN_1, 100)), - (0, GPIO.PWM(_PWM_PIN_2, 100)), + [0, GPIO.PWM(_PWM_PIN_1, 100)], + [0, GPIO.PWM(_PWM_PIN_2, 100)], ] GPIO.setmode(GPIO.BCM) diff --git a/setup.py b/setup.py index 6fede41..97868b6 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ "opencv-python-headless~=3.4.0", "scipy>=1.9.1", "numpy<2.0.0", + "hopper @ git+https://github.com/systemetric/hopper.git", ], author="Skyler Grey", From ec1850444ce7848117aefe8dc99750d92add66a0 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 21 Mar 2026 12:25:50 +0000 Subject: [PATCH 18/25] fix: change various things for testing on non-brain hardware --- robot/__init__.py | 2 +- robot/greengiant.py | 5 ++--- robot/mock_picamera2.py | 11 ++++++++--- robot/reset.py | 7 +++---- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/robot/__init__.py b/robot/__init__.py index c81b116..c6db421 100644 --- a/robot/__init__.py +++ b/robot/__init__.py @@ -3,7 +3,7 @@ April tags a marker recognition system. Also performs convince functions for use by shepherd""" -import importlib +import importlib.util has_picamera2 = importlib.util.find_spec("picamera2") is not None diff --git a/robot/greengiant.py b/robot/greengiant.py index f2b24f8..a44cac3 100644 --- a/robot/greengiant.py +++ b/robot/greengiant.py @@ -234,15 +234,14 @@ def set_5v_acc_power(self, new_state): self._bus.write_byte_data(_GG_I2C_ADDR, _GG_ENABLE_5V_ACC, int(new_state)) else: # for GG versions 5v power is always enabled - raise IOError(f"Attempted to set 5v power to {new_state} on an unsupported BrainBox.") + print(f"WARN: Attempted to set 5v power to {new_state} on an unsupported BrainBox.") def get_5v_acc_power(self): if self._version >= 10: return bool(self._bus.read_byte_data(_GG_I2C_ADDR, _GG_ENABLE_5V_ACC)) else: # for GG versions 5v power is always enabled - raise IOError(f"Attempted to get 5v power on an unsupported BrainBox.") - + print(f"WARN: Attempted to set 5v power to {new_state} on an unsupported BrainBox.") def set_user_led(self, on): self._bus.write_byte_data(_GG_I2C_ADDR, _GG_USER_LED, int(on)) diff --git a/robot/mock_picamera2.py b/robot/mock_picamera2.py index 6e81914..5ed8ba3 100644 --- a/robot/mock_picamera2.py +++ b/robot/mock_picamera2.py @@ -6,28 +6,33 @@ class Picamera2: camera_properties = {"Model": "Mock Picamera2"} def __init__(self): - pass + print("mock_picamera2.Picamera2.__init__()") def configure(self, _): - pass + print("mock_picamera2.Picamera2.configure(_)") def start(self): + print("mock_picamera2.Picamera2.start()") pass def stop(self): + print("mock_picamera2.Picamera2.stop()") pass def close(self): + print("mock_picamera2.Picamera2.close()") pass def capture_array(self): + print("mock_picamera2.Picamera2.capture_array(): None") return None def create_still_configuration(self, _): + print("mock_picamera2.Picamera2.create_still_configuration(_): {}") return {} @staticmethod def set_logging(_): - pass + print("mock_picamera2.Picamera2.set_logging(_)") print("<<< WARN: using mock picamera2 >>>") diff --git a/robot/reset.py b/robot/reset.py index a8f907a..89701ec 100644 --- a/robot/reset.py +++ b/robot/reset.py @@ -19,7 +19,8 @@ def reset(): Used by Shepherd when the Stop button is pressed. """ bus = SMBus(1) - version = gg.GreenGiantInternal(bus).get_version() + internal = gg.GreenGiantInternal(bus) + version = internal.get_version() if version < 10: c.CytronBoard(1).stop() @@ -31,10 +32,8 @@ def reset(): gg.GreenGiantGPIOPinList(bus, version, 5, gg._GG_GPIO_GPIO_BASE, gg._GG_GPIO_PWM_BASE).off() # probably should wrap this all up in a .off() - internal = gg.GreenGiantInternal(bus) internal.enable_motors(False) - #internal.set_motor_power(False) - internal.set_12v_acc_power(False) # Not sure, should this be controlled by user? + internal.set_12v_acc_power(False) internal.set_5v_acc_power(False) internal.set_user_led(False) From bdb4cc1fc3565d059fd08ac6eb5f4d1836db833b Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sat, 25 Apr 2026 20:09:15 +0100 Subject: [PATCH 19/25] feat: write b64 encoded images to hopper image pipe --- pyproject.toml | 17 ----------------- robot/__init__.py | 2 -- robot/game_config/__init__.py | 3 --- robot/vision.py | 11 +++++------ robot/wrapper.py | 18 ++++-------------- setup.py | 9 --------- 6 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 376132a..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[project] -name = "robot" -version = "2024.1" -readme = "README.md" -requires-python = ">=3.6" -dependencies = [ - "smbus2>=0.4.2", - "opencv-python-headless~=3.4.0", - "scipy>=1.9.1", - "numpy<2.0.0", - "hopper @ git+https://github.com/systemetric/hopper.git", -] - -[dependency-groups] -dev = [ - "fake-rpi>=0.7.1", -] diff --git a/robot/__init__.py b/robot/__init__.py index c6db421..6a4cb6f 100644 --- a/robot/__init__.py +++ b/robot/__init__.py @@ -34,7 +34,6 @@ MARKER_TYPE, TARGET_TYPE, TEAM, - SECTOR ) @@ -59,6 +58,5 @@ "MARKER_TYPE", "TARGET_TYPE", "TEAM", - "SECTOR", "RoboConUSBCamera" ) diff --git a/robot/game_config/__init__.py b/robot/game_config/__init__.py index 60f778c..f97ba60 100644 --- a/robot/game_config/__init__.py +++ b/robot/game_config/__init__.py @@ -17,11 +17,8 @@ BLUE = (255, 0, 0) # Blue WHITE = (255, 255, 255) # White -SECTOR = TEAM # 2026 ONLY, ALIAS `TEAM` AS `SECTOR` - __all__ = ( "TEAM", - "SECTOR", "TARGET_TYPE", "MARKER", "TARGET_MARKER", diff --git a/robot/vision.py b/robot/vision.py index 7c17654..5945d94 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -2,6 +2,7 @@ postprocessing on the data to make it accessible to the user """ import abc +import base64 import logging import os import threading @@ -20,11 +21,6 @@ import robot.apriltags3 as AT - -# TODO put all of the paths together -IMAGE_TO_SHEPHERD_PATH = "/home/pi/shepherd/shepherd/static/image.jpg" - - class Marker(): """A class to automatically pull the dis and bear_y out of the detection""" @@ -322,6 +318,7 @@ class PostProcessor(threading.Thread): def __init__(self, owner, zone, + image_pipe, bounding_box_thickness=5, bounding_box=True, usb_stick=False, @@ -332,6 +329,7 @@ def __init__(self, self._owner = owner self.zone = zone + self._image_pipe = image_pipe self._bounding_box_thickness = bounding_box_thickness self._bounding_box = bounding_box self._usb_stick = usb_stick @@ -416,7 +414,8 @@ def run(self): if self._bounding_box: frame = self._draw_bounding_box(frame, detections) if self._save: - cv2.imwrite(IMAGE_TO_SHEPHERD_PATH, frame) + encoded_img = base64.b64encode(cv2.imencode(".png", frame)[1]) + b'\n' + self._image_pipe.write(encoded_img) if self._usb_stick: self._write_to_usb(capture, detections) if self._send_to_sheep: diff --git a/robot/wrapper.py b/robot/wrapper.py index f141e4b..be699c3 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -75,11 +75,10 @@ def __init__(self, self._warnings = [] # Initialize a RcMuxClient and open the start pipe + self._image_pipe = HopperPipe(HopperPipeType.OUT, "robot", "camera") self._start_pipe = HopperPipe(HopperPipeType.OUT, "robot", "robot/control") self._start_json_reader = JsonReader(self._start_pipe) - self._parse_cmdline() - setup_logging(logging_level) # check if copy stat file exists and read it if it does then delete it @@ -145,7 +144,7 @@ def subsystem_init(self, camera, start_enable_12v, start_enable_5v): raise ValueError("camera must inherit from vision.Camera") self.res = self.camera.res - self._vision = vision.Vision(self.zone, camera=self.camera) + self._vision = vision.Vision(self.zone, camera=self.camera, image_pipe=self._image_pipe) def report_hardware_status(self): """Print out a nice log message at the start of each robot init with @@ -240,17 +239,6 @@ def stop(self): self.enable_12v = False self.motors.stop() - def _parse_cmdline(self): - """Parse the command line arguments""" - parser = optparse.OptionParser() - - parser.add_option("--usbkey", type="string", dest="usbkey", - help="The path of the (non-volatile) user USB key") - - (options, _) = parser.parse_args() - - self.usbkey = options.usbkey - def _wait_start_blink(self): """Blink status LED until start is pressed""" v = False @@ -308,6 +296,8 @@ def see(self) -> vision.Detections: def __del__(self): """Frees hardware resources held by the vision object""" logging.warning("Destroying robot object") + self._start_pipe.close() + self._image_pipe.close() # If vision never was initialled this creates confusing errors # so check that it is initialled first if hasattr(self, "_vision"): diff --git a/setup.py b/setup.py index 97868b6..4434e55 100644 --- a/setup.py +++ b/setup.py @@ -5,15 +5,6 @@ version="2024.1", packages=find_packages(), - install_requires=[ - "smbus2>=0.4.2", - "fake-rpi>=0.7.1", - "opencv-python-headless~=3.4.0", - "scipy>=1.9.1", - "numpy<2.0.0", - "hopper @ git+https://github.com/systemetric/hopper.git", - ], - author="Skyler Grey", author_email="skyler3665@gmail.com", ) From eb843c02fd962514c5a05e37efe12f50197b387d Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sun, 26 Apr 2026 13:43:46 +0100 Subject: [PATCH 20/25] fix(vision): pass `image_pipe` to `PostProcessor` --- robot/vision.py | 3 ++- robot/wrapper.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/robot/vision.py b/robot/vision.py index 5945d94..9f231d9 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -428,6 +428,7 @@ class Vision(): def __init__(self, zone, + image_pipe, at_path=_AT_PATH, max_queue_size=4, camera=None): @@ -451,7 +452,7 @@ def __init__(self, self.camera = camera self.frames_to_postprocess = queue.Queue(max_queue_size) - self.post_processor = PostProcessor(self, zone=self.zone) + self.post_processor = PostProcessor(self, zone=self.zone, image_pipe=image_pipe) def stop(self): """Cleanup to prevent leaking hardware resource""" diff --git a/robot/wrapper.py b/robot/wrapper.py index be699c3..8fb405b 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -296,8 +296,17 @@ def see(self) -> vision.Detections: def __del__(self): """Frees hardware resources held by the vision object""" logging.warning("Destroying robot object") - self._start_pipe.close() - self._image_pipe.close() + + try: + self._start_pipe.close() + except: + pass + + try: + self._image_pipe.close() + except: + pass + # If vision never was initialled this creates confusing errors # so check that it is initialled first if hasattr(self, "_vision"): From 6ec55843af29c2bcabf0ef19725f767e91398a6e Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sun, 26 Apr 2026 13:51:19 +0100 Subject: [PATCH 21/25] fix: actually open hopper pipes before using --- robot/wrapper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/robot/wrapper.py b/robot/wrapper.py index 8fb405b..e03d8ca 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -77,6 +77,10 @@ def __init__(self, # Initialize a RcMuxClient and open the start pipe self._image_pipe = HopperPipe(HopperPipeType.OUT, "robot", "camera") self._start_pipe = HopperPipe(HopperPipeType.OUT, "robot", "robot/control") + + self._image_pipe.open() + self._start_pipe.open() + self._start_json_reader = JsonReader(self._start_pipe) setup_logging(logging_level) From db908e6cfb127d825e2bb93f51765d71c112d324 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Sun, 26 Apr 2026 23:22:35 +0100 Subject: [PATCH 22/25] fix: open image pipe in input mode --- robot/wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robot/wrapper.py b/robot/wrapper.py index e03d8ca..1425172 100644 --- a/robot/wrapper.py +++ b/robot/wrapper.py @@ -75,7 +75,7 @@ def __init__(self, self._warnings = [] # Initialize a RcMuxClient and open the start pipe - self._image_pipe = HopperPipe(HopperPipeType.OUT, "robot", "camera") + self._image_pipe = HopperPipe(HopperPipeType.IN, "robot", "camera") self._start_pipe = HopperPipe(HopperPipeType.OUT, "robot", "robot/control") self._image_pipe.open() From b6a23469bfe99cd83f5b03dcdd2898af77f2be9a Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Wed, 29 Apr 2026 21:11:43 +0100 Subject: [PATCH 23/25] fix(vision): use JPEG images, not PNG --- robot/vision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robot/vision.py b/robot/vision.py index 9f231d9..c601a2a 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -414,7 +414,7 @@ def run(self): if self._bounding_box: frame = self._draw_bounding_box(frame, detections) if self._save: - encoded_img = base64.b64encode(cv2.imencode(".png", frame)[1]) + b'\n' + encoded_img = base64.b64encode(cv2.imencode(".jpg", frame)[1]) + b'\n' self._image_pipe.write(encoded_img) if self._usb_stick: self._write_to_usb(capture, detections) From 3723073ba02c725ff8e890f485d61647990d3f9e Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Wed, 29 Apr 2026 21:26:23 +0100 Subject: [PATCH 24/25] fix(vision): use `logging.ERROR` instead of `ERROR` - removes deprecation warning on Robot intitialisation --- robot/vision.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robot/vision.py b/robot/vision.py index c601a2a..da819f5 100755 --- a/robot/vision.py +++ b/robot/vision.py @@ -160,7 +160,7 @@ class RoboConPiCamera(Camera): def __init__(self, start_res=None, focal_lengths=None): os.environ["LIBCAMERA_LOG_LEVELS"] = "3" - picamera2.Picamera2.set_logging(picamera2.Picamera2.ERROR) + picamera2.Picamera2.set_logging(logging.ERROR) self._pi_camera = picamera2.Picamera2() # should test if the camera exists here, and give a nice warning self.camera_model = self._pi_camera.camera_properties['Model'] @@ -197,7 +197,7 @@ def __init__(self, start_res=None, focal_lengths=None): else: print("unknown camera: " + self._pi_camera.camera_properties) - self._pi_camera.set_logging(picamera2.Picamera2.ERROR) + self._pi_camera.set_logging(logging.ERROR) self._pi_camera_resolution = start_res # we store this - WHY? self._camera_config = self._pi_camera.create_still_configuration( main={"size": start_res, "format": 'RGB888'}) From ea2c70d3b861ee919520e2500406b8919960cfcf Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Wed, 6 May 2026 08:31:51 +0100 Subject: [PATCH 25/25] module restructuring --- robocon/__init__.py | 18 + robocon/brain/__init__.py | 25 ++ {robot => robocon/brain}/cytron.py | 0 {robot => robocon/brain}/greengiant.py | 0 robocon/brain/io.py | 129 +++++++ {robot => robocon/brain}/reset.py | 4 +- .../game_config => robocon/game}/__init__.py | 0 .../game_config => robocon/game}/markers.py | 0 .../game}/startup_poems.py | 0 .../game_config => robocon/game}/targets.py | 0 {robot/game_config => robocon/game}/teams.py | 0 robocon/vision/__init__.py | 25 ++ {robot => robocon/vision}/apriltags3.py | 3 +- robocon/vision/camera.py | 86 +++++ {robot => robocon/vision}/mock_picamera2.py | 0 {robot => robocon/vision}/vision.py | 5 +- robot/__init__.py | 62 ---- robot/log.py | 15 - robot/wrapper.py | 318 ------------------ setup.py | 2 +- 20 files changed, 289 insertions(+), 403 deletions(-) create mode 100644 robocon/__init__.py create mode 100644 robocon/brain/__init__.py rename {robot => robocon/brain}/cytron.py (100%) rename {robot => robocon/brain}/greengiant.py (100%) create mode 100644 robocon/brain/io.py rename {robot => robocon/brain}/reset.py (96%) rename {robot/game_config => robocon/game}/__init__.py (100%) rename {robot/game_config => robocon/game}/markers.py (100%) rename {robot/game_config => robocon/game}/startup_poems.py (100%) rename {robot/game_config => robocon/game}/targets.py (100%) rename {robot/game_config => robocon/game}/teams.py (100%) create mode 100644 robocon/vision/__init__.py rename {robot => robocon/vision}/apriltags3.py (99%) mode change 100755 => 100644 create mode 100644 robocon/vision/camera.py rename {robot => robocon/vision}/mock_picamera2.py (100%) rename {robot => robocon/vision}/vision.py (99%) mode change 100755 => 100644 delete mode 100644 robot/__init__.py delete mode 100644 robot/log.py delete mode 100644 robot/wrapper.py diff --git a/robocon/__init__.py b/robocon/__init__.py new file mode 100644 index 0000000..bcd30a0 --- /dev/null +++ b/robocon/__init__.py @@ -0,0 +1,18 @@ +from robocon.game import ( + TEAM, + TARGET_TYPE, + MARKER, + TARGET_MARKER, + MARKER_TYPE, + BASE_MARKER, + ARENA_MARKER) + +__all__ = ( + "TEAM", + "TARGET_TYPE", + "MARKER", + "TARGET_MARKER", + "MARKER_TYPE", + "BASE_MARKER", + "ARENA_MARKER", +) diff --git a/robocon/brain/__init__.py b/robocon/brain/__init__.py new file mode 100644 index 0000000..052af96 --- /dev/null +++ b/robocon/brain/__init__.py @@ -0,0 +1,25 @@ +import importlib.util + +has_rpi = importlib.util.find_spec("RPi") is not None + +if not has_rpi: + import sys + import fake_rpi + sys.modules["RPi"] = fake_rpi.RPi + sys.modules["RPi.GPIO"] = fake_rpi.RPi.GPIO + sys.modules["smbus2"] = fake_rpi.smbus + +import sys + +# greengiant imports for users +from robocon.brain.greengiant import OUTPUT, INPUT, INPUT_ANALOG, INPUT_PULLUP, PWM_SERVO +from robocon.brain.io import IO + +__all__ = ( + "IO", + "OUTPUT", + "INPUT", + "INPUT_ANALOG", + "INPUT_PULLUP", + "PWM_SERVO", +) diff --git a/robot/cytron.py b/robocon/brain/cytron.py similarity index 100% rename from robot/cytron.py rename to robocon/brain/cytron.py diff --git a/robot/greengiant.py b/robocon/brain/greengiant.py similarity index 100% rename from robot/greengiant.py rename to robocon/brain/greengiant.py diff --git a/robocon/brain/io.py b/robocon/brain/io.py new file mode 100644 index 0000000..9cdc37c --- /dev/null +++ b/robocon/brain/io.py @@ -0,0 +1,129 @@ +import sys +import logging +from smbus2 import SMBus + +from robocon.brain.cytron import CytronBoard +from robocon.brain.greengiant import ( + GreenGiantInternal, + GreenGiantGPIOPinList, + GreenGiantMotors, + _GG_SERVO_PWM_BASE, + _GG_GPIO_PWM_BASE, + _GG_GPIO_GPIO_BASE, + _GG_SERVO_GPIO_BASE) + +_logger = logging.getLogger("robot_io") + +def setup_logging(level): + """Display the just the message when logging events + Sets the logging level to `level`""" + _logger.setLevel(level) + + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(level) + + fmt = logging.Formatter("%(message)s") + handler.setFormatter(fmt) + + _logger.addHandler(handler) + +class IO(): + _initialised = False + + def __init__(self, max_motor_voltage=6, enable_12v=True, enable_5v=True, log_level=logging.INFO): + self._max_motor_voltage = max_motor_voltage + + self._initialised = False + self._warnings = [] + + setup_logging(log_level) + + if type(self)._initialised: + raise RuntimeError("IO object acquires hardware locks for its" + " sole use and so can only be used once.") + + self.bus = SMBus(1) + self._green_giant = GreenGiantInternal(self.bus) + self._gg_version = self._green_giant.get_version() + if self._gg_version >= 10: + # enable power rails + self._green_giant.set_motor_power(True) + self.enable_12v = enable_12v + self.enable_5v = enable_5v + self._adc_max = 5 + # configure User IO Ports + self.servos = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_SERVO_GPIO_BASE, _GG_SERVO_PWM_BASE) + self.gpio = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_GPIO_GPIO_BASE, _GG_GPIO_PWM_BASE) + # configure motor drivers + self.motors = GreenGiantMotors(self.bus, self._max_motor_voltage) + ## thinks, perhaps this should be inherrent to using the motors and + ## open load detection can be in there? + self.motors.enable_motors(True) + else: + # power rails + self._green_giant.set_motor_power(True) + self._adc_max = self._green_giant.get_fvr_reading() + # user IO + self.servos = GreenGiantGPIOPinList(self.bus, self._gg_version, None, None, _GG_SERVO_PWM_BASE) + self.gpio = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_GPIO_GPIO_BASE , None) + # configure motor drivers + self.motors = CytronBoard(self._max_motor_voltage) + + type(self)._initialised = True + + @property + def enable_motors(self): + """Return if motors are currently enabled + + For the GG board this will be the state of the 12v line, which we cannot query, + so return what it was set to. + + For the PiLow series the Motors have both a power control and a enable. Generally + the Power should not be switched on and off, just the enable bits. The power may + be tripped in extreme circumstances. I guess that here we want to report any + reason for the motors not working, which includes power and enable + + """ + if self._gg_version < 10: + return self._green_giant.enable_12v + else: + return self._green_giant.get_motorpwr() and self._green_giant.get_enable() + + @enable_motors.setter + def enable_motors(self, on): + """An nice alias for set_12v""" + if self._version < 10: + return self._green_giant.enable_motors(on) + + @property + def enable_12v(self): + return self._green_giant.get_12v_acc_power() + + @enable_12v.setter + def enable_12v(self, on): + self._green_giant.set_12v_acc_power(on) + + @property + def enable_5v(self): + return self._green_giant.get_5v_acc_power() + + @enable_5v.setter + def enable_5v(self, on): + self._green_giant.set_5v_acc_power(on) + + def stop(self): + """Stops the robot and cuts power to the motors. + + does not touch the servos position. + """ + self.enable_12v = False + self.motors.stop() + + def set_user_led(self, val=True): + self._green_giant.set_user_led(val) + + def __del__(self): + """Frees hardware resources held by the vision object""" + logging.warning("Destroying robot object") + type(self)._initialised = False + diff --git a/robot/reset.py b/robocon/brain/reset.py similarity index 96% rename from robot/reset.py rename to robocon/brain/reset.py index 89701ec..ff64492 100644 --- a/robot/reset.py +++ b/robocon/brain/reset.py @@ -10,8 +10,8 @@ https://stackoverflow.com/a/45799209/5006710 """ from smbus2 import SMBus -import robot.cytron as c -import robot.greengiant as gg +import .cytron as c +import .greengiant as gg def reset(): diff --git a/robot/game_config/__init__.py b/robocon/game/__init__.py similarity index 100% rename from robot/game_config/__init__.py rename to robocon/game/__init__.py diff --git a/robot/game_config/markers.py b/robocon/game/markers.py similarity index 100% rename from robot/game_config/markers.py rename to robocon/game/markers.py diff --git a/robot/game_config/startup_poems.py b/robocon/game/startup_poems.py similarity index 100% rename from robot/game_config/startup_poems.py rename to robocon/game/startup_poems.py diff --git a/robot/game_config/targets.py b/robocon/game/targets.py similarity index 100% rename from robot/game_config/targets.py rename to robocon/game/targets.py diff --git a/robot/game_config/teams.py b/robocon/game/teams.py similarity index 100% rename from robot/game_config/teams.py rename to robocon/game/teams.py diff --git a/robocon/vision/__init__.py b/robocon/vision/__init__.py new file mode 100644 index 0000000..251192f --- /dev/null +++ b/robocon/vision/__init__.py @@ -0,0 +1,25 @@ +import importlib.util + +has_picamera2 = importlib.util.find_spec("picamera2") is not None + +if not has_picamera2: + import sys + import robocon.vision.mock_picamera2 as mock_picamera2 + sys.modules["picamera2"] = mock_picamera2 + +import sys + +from robocon.vision.camera import Camera, NoCameraPresent +from robocon.vision.vision import RoboConUSBCamera + +MINIUM_VERSION = (3, 6) +if sys.version_info <= MINIUM_VERSION: + raise ImportError( + "Expected python {} but instead got {}".format(MINIUM_VERSION, sys.version_info) + ) + +__all__ = ( + "Camera", + "NoCameraPresent", + "RoboConUSBCamera", +) diff --git a/robot/apriltags3.py b/robocon/vision/apriltags3.py old mode 100755 new mode 100644 similarity index 99% rename from robot/apriltags3.py rename to robocon/vision/apriltags3.py index 99db0c2..d0f657c --- a/robot/apriltags3.py +++ b/robocon/vision/apriltags3.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Python wrapper for C version of apriltags. This program creates two classes that are used to detect apriltags and extract information from them. Using this module, you can identify all apriltags visible in an @@ -21,7 +20,7 @@ import numpy as np import scipy.spatial.transform as transform -from .game_config import MARKER, MARKER_TYPE +from robocon.game import MARKER, MARKER_TYPE ###################################################################### diff --git a/robocon/vision/camera.py b/robocon/vision/camera.py new file mode 100644 index 0000000..7d53f31 --- /dev/null +++ b/robocon/vision/camera.py @@ -0,0 +1,86 @@ +""" +The module containing the `Robot` class + +Mainly provides init routine for the brain and binds attributes of the `Robot` +class to their respecitve classes +""" +import json +import sys +import optparse +import os +import logging +import time +import threading +import random +import typing + +from datetime import datetime +from robocon.game import TEAM, POEM_ON_STARTUP +from . import vision + +from hopper import HopperPipe, HopperPipeType, JsonReader + +_logger = logging.getLogger("robot_vision") + +def setup_logging(level): + """Display the just the message when logging events + Sets the logging level to `level`""" + _logger.setLevel(level) + + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(level) + + fmt = logging.Formatter("%(message)s") + handler.setFormatter(fmt) + + _logger.addHandler(handler) + + +class NoCameraPresent(Exception): + """Camera not connected.""" + + def __str__(self): + return "No camera found." + +class Camera(): + _initialised = False + + def __init__(self, camera=None, log_level=logging.INFO): + self._initialised = False + self._warnings = [] + + self._image_pipe = HopperPipe(HopperPipeType.IN, "robot", "camera") + self._image_pipe.open() + + setup_logging(log_level) + + if type(self)._initialised: + raise RuntimeError("Camera object can only be initialised once") + + self.camera = vision.RoboConPiCamera() if camera is None else camera() + if not isinstance(self.camera, vision.Camera): + raise ValueError("camera must inherit from vision.Camera") + self.res = self.camera.res + + self._vision = vision.Vision(self.zone, camera=self.camera, image_pipe=self._image_pipe) + + type(self)._initialised = True + + def see(self) -> vision.Detections: + """Take a photo, detect markers in scene, attach RoboCon specific + properties""" + return self._vision.detect_markers() + + def __del__(self): + """Frees hardware resources held by the vision object""" + try: + self._image_pipe.close() + except: + pass + + # If vision never was initialised this creates confusing errors + # so check that it is initialised first + if hasattr(self, "_vision"): + self._vision.stop() + type(self)._initialised = False + diff --git a/robot/mock_picamera2.py b/robocon/vision/mock_picamera2.py similarity index 100% rename from robot/mock_picamera2.py rename to robocon/vision/mock_picamera2.py diff --git a/robot/vision.py b/robocon/vision/vision.py old mode 100755 new mode 100644 similarity index 99% rename from robot/vision.py rename to robocon/vision/vision.py index da819f5..7bc6f5a --- a/robot/vision.py +++ b/robocon/vision/vision.py @@ -12,14 +12,13 @@ from datetime import datetime from typing import NamedTuple, Any - -from .game_config import MARKER, WHITE, BASE_MARKER as MarkerInfo +from robocon.game import MARKER, WHITE, BASE_MARKER as MarkerInfo import cv2 import numpy as np import picamera2 -import robot.apriltags3 as AT +import robocon.vision.apriltags3 as AT class Marker(): """A class to automatically pull the dis and bear_y out of the detection""" diff --git a/robot/__init__.py b/robot/__init__.py deleted file mode 100644 index 6a4cb6f..0000000 --- a/robot/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/python3 -"""The robot module, provides an python interface to the RoboCon hardware and -April tags a marker recognition system. Also performs convince functions for use -by shepherd""" - -import importlib.util - -has_picamera2 = importlib.util.find_spec("picamera2") is not None - -if not has_picamera2: - import sys - import fake_rpi - import robot.mock_picamera2 as mock_picamera2 - - sys.modules["RPi"] = fake_rpi.RPi - sys.modules["RPi.GPIO"] = fake_rpi.RPi.GPIO - sys.modules["picamera2"] = mock_picamera2 - sys.modules["smbus2"] = fake_rpi.smbus - -import sys - -# This log import configures our logging for us, but we don't want to -# provide it as part of this package. -import robot.log - -from robot.wrapper import Robot, NoCameraPresent -from robot.greengiant import OUTPUT, INPUT, INPUT_ANALOG, INPUT_PULLUP, PWM_SERVO -from robot.vision import RoboConUSBCamera -from robot.game_config import ( - MARKER, - BASE_MARKER, - ARENA_MARKER, - TARGET_MARKER, - MARKER_TYPE, - TARGET_TYPE, - TEAM, -) - - -MINIUM_VERSION = (3, 6) -if sys.version_info <= MINIUM_VERSION: - raise ImportError( - "Expected python {} but instead got {}".format(MINIUM_VERSION, sys.version_info) - ) - -__all__ = ( - "Robot", - "NoCameraPresent", - "OUTPUT", - "INPUT", - "INPUT_ANALOG", - "INPUT_PULLUP", - "PWM_SERVO", - "MARKER", - "BASE_MARKER", - "ARENA_MARKER", - "TARGET_MARKER", - "MARKER_TYPE", - "TARGET_TYPE", - "TEAM", - "RoboConUSBCamera" -) diff --git a/robot/log.py b/robot/log.py deleted file mode 100644 index 3d9d2e2..0000000 --- a/robot/log.py +++ /dev/null @@ -1,15 +0,0 @@ -# pylint: skip-file -# TODO is this dead code? -import logging - -# Python 2.6's logging doesn't have NullHandler - - -class NullHandler(logging.Handler): - def emit(self, record): - pass - - -# Default to sending our log messages nowhere -logger = logging.getLogger("sr") -logger.addHandler(NullHandler()) diff --git a/robot/wrapper.py b/robot/wrapper.py deleted file mode 100644 index 1425172..0000000 --- a/robot/wrapper.py +++ /dev/null @@ -1,318 +0,0 @@ -""" -The module containing the `Robot` class - -Mainly provides init routine for the brain and binds attributes of the `Robot` -class to their respecitve classes -""" -import json -import sys -import optparse -import os -import logging -import time -import threading -import random -import typing - -from datetime import datetime -from smbus2 import SMBus - -from .cytron import CytronBoard -from .greengiant import GreenGiantInternal, GreenGiantGPIOPinList, GreenGiantMotors, _GG_SERVO_PWM_BASE, _GG_GPIO_PWM_BASE, _GG_GPIO_GPIO_BASE, _GG_SERVO_GPIO_BASE -from .game_config import TEAM, POEM_ON_STARTUP -from . import vision - -from hopper import HopperPipe, HopperPipeType, JsonReader - -_logger = logging.getLogger("robot") - -# path to file with status of USB program copy, -# if this exists it is because the code on the robot has been copied from the robotusb -# this boot cycle. This is to highlight weird behaviour in the arena -COPY_STAT_FILE = "/tmp/usb_file_uploaded" - -def setup_logging(level): - """Display the just the message when logging events - Sets the logging level to `level`""" - _logger.setLevel(level) - - handler = logging.StreamHandler(sys.stdout) - handler.setLevel(level) - - fmt = logging.Formatter("%(message)s") - handler.setFormatter(fmt) - - _logger.addHandler(handler) - - -class NoCameraPresent(Exception): - """Camera not connected.""" - - def __str__(self): - return "No camera found." - - -class Robot(): - """Class for initialising and accessing robot hardware""" - - _initialised = False - - def __init__(self, - wait_for_start=True, - camera=None, - max_motor_voltage=6, - logging_level=logging.INFO, - start_enable_12v = True, - start_enable_5v = True, - ): - - self.zone = TEAM.RED - self.mode = "competition" - self._max_motor_voltage = max_motor_voltage - - self._initialised = False - self._start_pressed = False - self._warnings = [] - - # Initialize a RcMuxClient and open the start pipe - self._image_pipe = HopperPipe(HopperPipeType.IN, "robot", "camera") - self._start_pipe = HopperPipe(HopperPipeType.OUT, "robot", "robot/control") - - self._image_pipe.open() - self._start_pipe.open() - - self._start_json_reader = JsonReader(self._start_pipe) - - setup_logging(logging_level) - - # check if copy stat file exists and read it if it does then delete it - try: - with open(COPY_STAT_FILE, "r") as f: - _logger.info("Robot code copied %s from USB\n", f.read().strip()) - os.remove(COPY_STAT_FILE) - except IOError: - pass - - self.subsystem_init(camera, start_enable_12v, start_enable_5v) - self.report_hardware_status() - type(self)._initialised = True - - # Allows for the robot object to be set up and mutated before being set - # up. Dangerous as it means the start info might not get loaded - # depending on user code. - if wait_for_start is True: - start_data = self.wait_start() - self.zone = start_data['zone'] - self.mode = start_data['mode'] - else: - _logger.warning("Robot initalized but usercode running before" - "`robot.wait_start`. Robot will not wait for the " - "start button until `robot.wait_start` is called.") - - def subsystem_init(self, camera, start_enable_12v, start_enable_5v): - """Allows for initalisation of subsystems after instansating `Robot()` - Can only be called once""" - if type(self)._initialised: - raise RuntimeError("Robot object is acquires hardware locks for its" - " sole use and so can only be used once.") - - self.bus = SMBus(1) - self._green_giant = GreenGiantInternal(self.bus) - self._gg_version = self._green_giant.get_version() - if self._gg_version >= 10: - # enable power rails - self._green_giant.set_motor_power(True) - self.enable_12v = start_enable_12v - self.enable_5v = start_enable_5v - self._adc_max = 5 - # configure User IO Ports - self.servos = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_SERVO_GPIO_BASE, _GG_SERVO_PWM_BASE) - self.gpio = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_GPIO_GPIO_BASE, _GG_GPIO_PWM_BASE) - # configure motor drivers - self.motors = GreenGiantMotors(self.bus, self._max_motor_voltage) - ## thinks, perhaps this should be inherrent to using the motors and - ## open load detection can be in there? - self.motors.enable_motors(True) - else: - # power rails - self._green_giant.set_motor_power(True) - self._adc_max = self._green_giant.get_fvr_reading() - # user IO - self.servos = GreenGiantGPIOPinList(self.bus, self._gg_version, None, None, _GG_SERVO_PWM_BASE) - self.gpio = GreenGiantGPIOPinList(self.bus, self._gg_version, self._adc_max, _GG_GPIO_GPIO_BASE , None) - # configure motor drivers - self.motors = CytronBoard(self._max_motor_voltage) - - self.camera = vision.RoboConPiCamera() if camera is None else camera() - if not isinstance(self.camera, vision.Camera): - raise ValueError("camera must inherit from vision.Camera") - self.res = self.camera.res - - self._vision = vision.Vision(self.zone, camera=self.camera, image_pipe=self._image_pipe) - - def report_hardware_status(self): - """Print out a nice log message at the start of each robot init with - the hardware status""" - - battery_voltage = self._green_giant.get_battery_voltage() - battery_str = "Battery Voltage: %.2fv" % battery_voltage - # GG cannot read voltages above 12.2v - if battery_voltage > 12.2: - battery_str = "Battery Voltage: > 12.2v" - elif battery_voltage < 11.5: - self._warnings.append("Battery voltage below 11.5v, consider changing for a charged battery") - - if self._gg_version < 3: - self._warnings.append( - "Green Giant version not 3 but instead {}".format(self._gg_version)) - - camera_type_str = "Camera: {}".format( - self.camera.__class__.__name__) - - # Adds a secret every now and again! - POEM_ON_STARTUP.on_startup(_logger,random) - - # print report of hardware - _logger.info("------HARDWARE REPORT------") - #_logger.info("Time: %s", datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - # no RTC on new boards, perhaps use a "run number" increment instead? - _logger.info("Patch Version: ") - _logger.info(battery_str) - #_logger.info("ADC Max: %.2fv", self._adc_max) - _logger.info("Robocon Board: Yes (v%d)", self._gg_version) - if self._gg_version <= 3: - _logger.info("Motor Driver: Cytron Board") - else: - _logger.info("Motor Driver: PiLow Cover") - # check and report open load here, warning race condition, is battery power on long enough? - # Thinks that we just motor enable/disable and leave motor power always on? - _logger.info(camera_type_str) - _logger.info("---------------------------") - - for warning in self._warnings: - _logger.warning("WARNING: %s", warning) - - if not self._warnings: - _logger.info("Hardware looks good") - - @property - def enable_motors(self): - """Return if motors are currently enabled - - For the GG board this will be the state of the 12v line, which we cannot query, - so return what it was set to. - - For the PiLow series the Motors have both a power control and a enable. Generally - the Power should not be switched on and off, just the enable bits. The power may - be tripped in extreme circumstances. I guess that here we want to report any - reason for the motors not working, which includes power and enable - - """ - if self._gg_version < 10: - return self._green_giant.enable_12v - else: - return self._green_giant.get_motorpwr() and self._green_giant.get_enable() - - @enable_motors.setter - def enable_motors(self, on): - """An nice alias for set_12v""" - if self._version < 10: - return self._green_giant.enable_motors(on) - - @property - def enable_12v(self): - return self._green_giant.get_12v_acc_power() - - @enable_12v.setter - def enable_12v(self, on): - self._green_giant.set_12v_acc_power(on) - - @property - def enable_5v(self): - return self._green_giant.get_5v_acc_power() - - @enable_5v.setter - def enable_5v(self, on): - self._green_giant.set_5v_acc_power(on) - - def stop(self): - """Stops the robot and cuts power to the motors. - - does not touch the servos position. - """ - self.enable_12v = False - self.motors.stop() - - def _wait_start_blink(self): - """Blink status LED until start is pressed""" - v = False - while not self._start_pressed: - time.sleep(0.2) - self._green_giant.set_user_led(v) - v = not v - if self._gg_version < 10: - # on GG keep main LED on to show the device is running - self._green_giant.set_user_led(True) - else: - # for PiLow the board has its own Power LED, so this can be used by users - self._green_giant.set_user_led(False) - - def _get_start_info(self): - """Get the start infomation from the named pipe""" - - # This call blocks until the start info is read - settings = self._start_json_reader.read() - - assert "zone" in settings, "zone must be in startup info" - if settings["zone"] not in range(4): - raise ValueError( - "zone must be in range 0-3 inclusive -- value of %i is invalid" - % settings["zone"]) - settings["zone"] = TEAM[f"T{settings['zone']}"] - - self._start_pressed = True - - return settings - - def wait_start(self): - """Wait for the start signal to happen""" - - blink_thread = threading.Thread(target=self._wait_start_blink) - blink_thread.start() - - _logger.info("\nWaiting for start signal...") - - # This blocks till we get start info - start_info = self._get_start_info() - - _logger.info("Robot started!\n") - - return start_info - - def set_user_led(self, val=True): - self._green_giant.set_user_led(val) - - def see(self) -> vision.Detections: - """Take a photo, detect markers in sene, attach RoboCon specific - properties""" - return self._vision.detect_markers() - - def __del__(self): - """Frees hardware resources held by the vision object""" - logging.warning("Destroying robot object") - - try: - self._start_pipe.close() - except: - pass - - try: - self._image_pipe.close() - except: - pass - - # If vision never was initialled this creates confusing errors - # so check that it is initialled first - if hasattr(self, "_vision"): - self._vision.stop() - type(self)._initialised = False diff --git a/setup.py b/setup.py index 4434e55..10c7853 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup( - name="robot", + name="robocon", version="2024.1", packages=find_packages(),