Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

A subsystem owns a mechanism

A subsystem is the part of a command-based robot program responsible for a mechanism and its shared resources. Our Intake subsystem is the single place that writes the intake’s output and knows how to read its piece detector. Keep the program structure diagram nearby as you connect these responsibilities to Python methods.

Separate decisions from requests

The operator might request collection, autonomous might request collection, and a release action might request reverse. They should all use the same intake rules. Put those rules behind methods such as set_output, has_piece, and stop. Callers ask for an action instead of reaching through and changing io.output.

The subsystem is not the whole robot and it is not a task with a finish condition. It persists for the life of the program. A command, introduced next, describes one behavior using that subsystem.

Build on the framework

intake_subsystem.py
import commands2


class Intake(commands2.Subsystem):
    def __init__(self, io):
        super().__init__()
        self.io = io

    def set_output(self, request):
        if self.has_piece() and request > 0:
            self.io.set_output(0.0)
        else:
            self.io.set_output(request)

    def has_piece(self):
        return self.io.detected

    def stop(self):
        self.io.set_output(0.0)

commands2.Subsystem is the base class supplied by RobotPy’s command framework. We inherit its framework behavior while adding our mechanism methods. super().__init__() runs the base class initializer so the framework can register the subsystem. It belongs before our own setup; don’t omit it because the code seems to work on one run.

The io parameter is an object given to us by the robot. self.io = io stores that same object; it does not make a new simulator. This is composition again: the subsystem has an input/output adapter. Tests can supply the same small simulator without constructing a whole application.

has_piece() returns a boolean to the caller. set_output blocks forward collection when detected, while reverse remains available. The adapter already handles normalized limits and disabled outputs. stop() requests zero. Each layer has a stated responsibility; callers don’t need to duplicate all of those rules.

Refactor

Copy your known periodic starting point with python tools/start_exercise.py intake-subsystem. In student-work/intake-subsystem, create intake_subsystem.py and implement the subsystem. Keep your earlier periodic assignment intact for comparison. Use import commands2 and the supplied IntakeSim; you do not need vendor libraries for this exercise.

Create test_intake.py beside it. Pytest is the check runner installed with the course: it calls functions whose names begin with test_ and reports failed assertions. Start with this check:

test_intake_contract.py
"""A first subsystem check, also included directly in the beginner lesson."""
from intake_subsystem import Intake
from examples.support.intake_io import IntakeSim


def test_forward_request():
    io = IntakeSim()
    io.enabled = True
    intake = Intake(io)
    intake.set_output(0.5)
    assert io.output == 0.5

Run python -m pytest student-work/intake-subsystem/test_intake.py -q from the Chiron root. The project’s test configuration makes shared helpers importable. Add checks for detected forward, reverse, stop, and independent instances. These calls test the subsystem’s contract before involving scheduling. The final project adds a fresh scheduler and simulated time for command checks.

Explain why other behaviors should use the subsystem instead of writing directly to the adapter. Then move on to commands.