Your logic file will be the brains of the operation, the man in the chair for your subsystem. It’s going to receive and interpret requests from superstructure and translate those requests into action.

Your logic file will be a class named after your subsystem. It will be constructed in the initialization of the RobotContainer and accepts an IO object as a value in its constructor. The class should inhert from either ControlledByStateMachine if it is controlled by superstructure or SubsystemBase if not.

ControlledByStateMachine is a custom abstract class designed to ensure that all classes controlled by our state machine contain a loop method. This loop method prevents classes being ran twice, once by the CommandScheduler and once by the state machine.

Your logic file class will take in an instance of your IO, which represents the plant being controlled.

class Subsystem(val io: SubsystemIO) : ControlledByStateMachine() { /* */ }

/*
 In the RobotContainer, your subsystem will be instantiated as follows:
 val subsystem = Subsystem(SubsystemIOTalon)
 Notice how an instance of the IO is passed and not the IO interface itself.
 Any class which extends SubsystemIO is a valid argument for the constructor.
*/

The first thing you should do in your file is to start writing the state machine for the class. First, you create the requests, which unfortunately are held in a different file, namely superstructure/Request.kt. It will look like this:

sealed interface Request {
	/* ... */
	
	sealed interface SubsystemRequest : Request {
		class Idle() : SubsystemRequest
		class OpenLoop(val voltage: ElectricalPotential) : SubsystemRequest
		
		/* If neccessary */
		// position may be of type Length or Angle
		class TargetPosition(val position: Length) : SubsystemRequest
		// velocity may be of type LinearVelocity or AngularVelocity
		class TargetVelocity(val velocity: LinearVelocity) : SubsystemRequest
	}
}

Next is the states, which is done back in our subsystem’s logic file. This must be done in a companion objectinside the class, as it is static and can be accessed by other classes in the project, namely the superstructure. You must also create functions for checking state machine states and requests.:

companion object {
	enum class SubsystemState {
		UNINITIALIZED,
		IDLE,
		OPEN_LOOP,
		/* If neccessary */
		TARGETING_POSITION,
		TARGETING_VELOCITY
	}
	
	inline fun fromRequestToState(request: SubsystemRequest): SubsystemState {
		return when (request) {
			is SubsystemRequest.IDLE -> SubsystemState.IDLE
			is SubsystemRequest.OpenLoop -> SubsystemState.OPEN_LOOP
			/* If neccessary */
			is SubsystemRequest.TargetPosition -> SubsystemState.TARGETING_POSITION
			is SubsystemRequest.TargetVelocity -> SubsystemState.TARGETING_VELOCITY
		}
	}
}

The next part of the file is creating the fields of the class. This includes things that other classes need to know about your subsystem, such as it’s current information, and fields internal to the class, e.g. target values for the plant. Make sure you know how Kotlin getters and setters work, as these are very valuable for a class’s public fields. A getter allows a variable to check custom logic every time it’s value is retrieved, and a setter runs custom logic whenever a variable is assigned. You must have a variable which stores the inputs and outputs of your plant, called inputs.

val inputs = SubsystemIO.SubsystemInputs()

/*
 These are private set to ensure that only the logic file is changing these.
 Other files who want to change these values must do so via requests.
*/
var targetVoltage = 0.0.volts
	private set

/* If necessary */
var targetPosition = 0.0.inches // can be Length or Angle
	private set
var targetVelocity = 0.0.inches.perSecond // can be LinearVelocity or AngularVelocity
	private set

var currentState: SubsystemState = SubsystemState.UNINITIALIZED
var currentRequest: SubsystemRequest = SubsystemRequest.Idle()
	set(value) {
		/* 
		 This setter runs everytime Subsystem.currentRequest is assigned a value.
		 We will use this setter to adjust target fields when necessary.
		*/
		when (value) {
			// value.property is the property passed into the request
			is SubsystemRequest.OpenLoop -> targetVoltage = value.voltage
			/* If necessary */
			is SubsystemRequest.TargetPosition -> targetPosition = value.position
			is SubsystemRequest.TargetVelocity -> targetVelocity = value.velocity
		}
	}
	
	/* Necessary for subsystems with closed-loop control */
	// or isAtTargetedVelocity, same logic but with Velocity units
	val isAtTargetedPosition: Boolean
		get() = return (inputs.subsystemPosition - targetPosition) <= SubsystemConstants.POSITION_TOLERANCE

If you subsystem has closed-loop control, you have to configure your feedback and feedforward values on initialization of your subsystem. Your arguments may change depending on what is needed (and what you have written) in your IO.

init {
	if (RobotBase.isReal()) {
		io.configurePID(
			SubsystemConstants.PID.REAL_KP,
			SubsystemConstants.PID.REAL_KI,
			SubsystemConstants.PID.REAL_KD,
		)
		io.configureFF(
			SubsystemConstants.PID.REAL_KS,
			SubsystemConstants.PID.REAL_KG,
			SubsystemConstants.PID.REAL_KV,
			SubsystemConstants.PID.REAL_KA
		)
	} else {
		io.configurePID(
			SubsystemConstants.PID.SIM_KP,
			SubsystemConstants.PID.SIM_KI,
			SubsystemConstants.PID.SIM_KD,
		)
		io.configureFF(
			SubsystemConstants.PID.SIM_KS,
			SubsystemConstants.PID.SIM_KG,
			SubsystemConstants.PID.SIM_KV,
			SubsystemConstants.PID.SIM_KA
		)
	}
}

We’re so close… final thing to do is the loop function. For subsystems extending SubsystemBase, this will be your periodic(), and for subsystems extending ControlledByStateMachine, this will be your onLoop().

First, you must update your inputs from your IO to ensure that you are using the most recent values supplied by your plant. Then, you need to log information about your logic file (we love logs ❤️‍🩹). Then, you update your current state based on the current request. Below is my example.

override fun onLoop() {
	io.updateInputs(inputs)
	
	CustomLogger.processInputs("Subsystem", inputs)
	
	CustomLogger.recordOutput("Subsystem/currentState", currentState)
	CustomLogger.recordOutput("Subsystem/currentRequest", currentRequest.javaClass.simpleName)
	
	CustomLogger.recordOutput("Subsystem/targetVoltage", targetVoltage.inVolts)
	/* If necessary */
	CustomLogger.recordOutput("Subsystem/targetPositionInches", targetPosition.inInches) // or whatever unit is appropriate
	CustomLogger.recordOutput("Subsystem/targetVelocityInchesPerSecond", targetVelocity.inInchesPerSecond) // or whatever unit is appropriate
	
	var nextState = currentState
	
	when (currentState) {
		SubsystemState.UNINITIALIZED -> {
			nextState = fromRequestToState(currentRequest)
		}
		SubsystemState.IDLE -> {
			/* IDLE logic, below is example */
			io.setVoltage(SubsystemConstants.IDLE_VOLTAGE)
			nextState = fromRequestToState(currentRequest)
		}
		SubsystemState.OPEN_LOOP -> {
			io.setVoltage(targetVoltage)
			nextState = fromRequestToState(currentRequest)
		}
		/* If necessary */
		SubsystemState.TARGETING_POSITION -> {
			io.setPosition(targetPosition)
			nextState = fromRequestToState(currentRequest)
		}
		SubsystemState.TARGETING_VELOCITY -> {
			io.setVelocity(targetVelocity)
			nextState = fromRequestToState(currentRequest)
		}
	}
}

And alas, it’s complete! This likely means that you have completed writing your subsystem, and for that I congratulate you. 🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳

For a fully complete subsystem with everything in here (plus a lot more complicated stuff which may or may not be explained in other documentation), you can take a look at 2026 shooter’s logic file.

https://github.com/team4099/Rebuilt-2026/blob/master/src/main/kotlin/com/team4099/robot2026/subsystems/superstructure/shooter/Shooter.kt