How Professional Teams Handle Hardware Development
Here's a common pattern in robotics development — and it's not unique to FTC:
- Hardware and software progress in parallel. Mechanical is designing. Electrical is planning. Software needs to start, but the physical robot isn't ready yet.
- Integration happens late. When hardware finally arrives, software needs to validate their logic works with real components — while also helping debug any hardware issues.
- Hardware access is limited. Multiple programmers, one robot. Testing slots need to be coordinated.
- Testing cycles are slower on real hardware. Build. Deploy. Walk to the robot. Press Start. Observe. Walk back. Iterate.
This isn't a problem to solve — it's the nature of hardware/software projects in satellites, medical devices, autonomous vehicles, and industrial automation. Professional engineers handle this with a proven technique called mock interfaces.
Mock interfaces let you develop and test software logic independently of hardware availability. When hardware arrives, you integrate with confidence because your logic has already been validated.
Understanding Mock Interfaces
What's a Mock Interface?
A mock interface is a software simulation of hardware that responds to commands the same way from your code's perspective, but doesn't require the actual physical device.
Think of it like a flight simulator. Pilots train for hundreds of hours in simulators before flying real aircraft. The simulator responds to controls realistically — but provides a safe environment for learning and testing procedures.
Your mock motor responds to setPower() just like a real motor would — but you can run it on your laptop, test instantly, and validate logic without needing the physical robot.
When you use mock interfaces, you can:
- Start development immediately — parallel progress with hardware design
- Test in seconds — rapid iteration without deployment cycles
- Isolate issues — if tests pass but robot fails, focus on hardware integration
- Work independently — everyone can develop and test without competing for robot access
Building a Simple Mock Motor
Let's build the simplest possible mock: a motor. A motor has two basic behaviors:
- You can set its power (−1.0 to +1.0)
- It moves based on that power over time
Here's the complete mock motor — notice it has zero FTC imports:
MockMotor.java
package org.firstinspires.ftc.teamcode.subsystems;
/**
* A simulated motor for testing.
* No FTC SDK required — runs on any computer.
*/
public class MockMotor {
private double power = 0.0;
private double position = 0.0;
private double speed = 1.0; // radians per second at full power
/**
* Set motor power.
* @param power Power from -1.0 (full reverse) to +1.0 (full forward)
*/
public void setPower(double power) {
// Clamp to valid range, just like real motor
this.power = Math.max(-1.0, Math.min(1.0, power));
}
/**
* Get current power setting.
*/
public double getPower() {
return power;
}
/**
* Simulate time passing. Call this every loop iteration.
* @param dt Time step in seconds (typically 0.02 for 50Hz)
*/
public void update(double dt) {
position += power * speed * dt;
}
/**
* Get simulated position.
*/
public double getPosition() {
return position;
}
}
That's it. 30 lines of code. No Android SDK, no FTC libraries, no hardware. This class runs anywhere Java runs — including your laptop.
Connecting to Real Hardware
Now we create a class that extends MockMotor but talks to real hardware. This is the "adapter" that bridges your mock interface to the physical motor:
FtcMotor.java
package org.firstinspires.ftc.teamcode.subsystems;
import com.qualcomm.robotcore.hardware.DcMotor;
/**
* Adapter that wraps a real FTC motor.
* Extends MockMotor so your code can use either interchangeably.
*/
public class FtcMotor extends MockMotor {
private final DcMotor realMotor;
/**
* Create adapter for a real FTC motor.
* @param motor The actual motor from hardwareMap
*/
public FtcMotor(DcMotor motor) {
super();
this.realMotor = motor;
// Configure the real motor
motor.setZeroPowerBehavior(DcMotor.ZeroPowerBehavior.BRAKE);
motor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
}
@Override
public void setPower(double power) {
super.setPower(power); // Track in parent
realMotor.setPower(power); // Send to real hardware
}
}
The Power of Inheritance
Because FtcMotor extends MockMotor, any code that uses a MockMotor will automatically work with an FtcMotor.
Your controller doesn't need to know which one it has. It just calls motor.setPower(0.5) and the right thing happens — whether that's updating a simulation variable or sending voltage to a real motor.
This is called the Liskov Substitution Principle — one of the most important ideas in software engineering.
What Runs Where
Here's the key insight: most of your code runs on your PC. Only a small amount needs the actual robot.
- MockMotor.java ✓ Tested
- MockPotentiometer.java ✓ Tested
- ChuteController.java ✓ Tested
- Chute.java ✓ Tested
- ChuteTest.java ✓ Tested
- FtcMotor.java Hardware
- FtcPotentiometer.java Hardware
- ChuteOpMode.java OpMode
When tests pass on your PC, you've validated your logic is correct. The remaining work on the robot focuses on:
- Verifying hardware is wired correctly
- Confirming hardware configuration in the app
- Checking motors spin in the expected direction
These hardware integration tasks are much easier to debug when you've already proven the logic works!
Full Example: The Chute Controller
Let's look at a real subsystem that uses this pattern: a chute controller that moves a linear actuator to specific positions using a motor and potentiometer.
The Challenge
A potentiometer reads position, but it wraps around after a full rotation. If your actuator extends through 3.5 rotations, the pot voltage goes:
0 → 2π → 0 → 2π → 0 → 2π → π
Your code needs to "unwrap" this to track the true position: 7π radians.
This is exactly the kind of logic that's challenging to debug on hardware — but straightforward to validate with mocks.
The MockPotentiometer
First, we need a mock potentiometer that simulates the wraparound behavior:
MockPotentiometer.java
public class MockPotentiometer {
private double position = 0.0;
private final double wrapAmount; // Typically 2π
public MockPotentiometer(double wrapAmount) {
this.wrapAmount = wrapAmount;
}
/**
* Get voltage reading (wraps around like real pot).
*/
public double getVoltage() {
double wrapped = position % wrapAmount;
if (wrapped < 0) wrapped += wrapAmount;
return wrapped;
}
/**
* Update position (called by simulation).
*/
public void updatePosition(double delta) {
this.position += delta;
}
}
The Controller Logic
Now the controller that handles homing and position tracking. Notice it only uses MockMotor and MockPotentiometer — it never imports anything from FTC:
ChuteController.java
public class ChuteController {
private final MockMotor motor;
private final MockPotentiometer pot;
private boolean homed = false;
private double homeVoltage = 0.0;
private double unwrappedVoltage = 0.0;
private double lastVoltage = 0.0;
public ChuteController(MockMotor motor, MockPotentiometer pot) {
this.motor = motor;
this.pot = pot;
}
public void update(double dt) {
motor.update(dt);
double voltage = pot.getVoltage();
// Detect wraparound and accumulate true position
double delta = voltage - lastVoltage;
if (Math.abs(delta) > Math.PI) {
// Wrap detected!
if (delta < 0) {
unwrappedVoltage += (delta + 2 * Math.PI);
} else {
unwrappedVoltage += (delta - 2 * Math.PI);
}
} else {
unwrappedVoltage += delta;
}
lastVoltage = voltage;
}
public double getPosition() {
return unwrappedVoltage - homeVoltage;
}
}
The build.gradle.kts Magic
Here's the key configuration: telling Gradle which files to exclude when building on your PC.
build.gradle.kts
plugins {
java
}
dependencies {
// Testing runs on PC — no FTC SDK needed
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// THE KEY: Exclude FTC files from PC compilation
tasks.withType<JavaCompile> {
exclude("**/FtcMotor.java")
exclude("**/FtcPotentiometer.java")
exclude("**/opmodes/**")
}
tasks.test {
useJUnitPlatform()
}
The exclude() lines tell Gradle: "When compiling on a PC, skip these files." Without this, Gradle would try to compile FtcMotor.java, fail because DcMotor doesn't exist on your PC, and the build would stop.
Writing Unit Tests
Now the payoff: tests that run on your PC in under 2 seconds.
ChuteTest.java
public class ChuteTest {
private static final double DT = 0.02; // 50Hz, like real robot
@Test
public void testPotWrapsCorrectly() {
MockPotentiometer pot = new MockPotentiometer(2 * Math.PI);
pot.setPosition(0.0);
assertEquals(0.0, pot.getVoltage(), 0.01);
pot.setPosition(Math.PI);
assertEquals(Math.PI, pot.getVoltage(), 0.01);
// At 2π, should wrap back to 0
pot.setPosition(2 * Math.PI);
assertEquals(0.0, pot.getVoltage(), 0.01);
}
@Test
public void testTracksPositionThroughMultipleWraps() {
Chute chute = new Chute(25.0); // Allow 25 radians
// Home the chute
chute.home();
for (int i = 0; i < 200; i++) {
chute.update(DT);
if (chute.isHomed()) break;
}
// Move to 3.5 rotations (7π radians)
double target = 3.5 * 2 * Math.PI;
chute.setTargetPosition(target);
for (int i = 0; i < 2000; i++) {
chute.update(DT);
if (chute.isAtTarget()) break;
}
// Should track through all the wraps correctly
assertTrue(chute.getPosition() > 3.0 * 2 * Math.PI);
assertTrue(Math.abs(chute.getPosition() - target) < 1.0);
}
}
Run them:
$ ./gradlew test
> Task :test
ChuteTest > testPotWrapsCorrectly() PASSED
ChuteTest > testTracksPositionThroughMultipleWraps() PASSED
BUILD SUCCESSFUL in 1s
One second. No robot. No USB cable. No deployment cycle. You just validated your wraparound logic works through 3.5 full rotations.
Putting It All Together
Create Mock Classes
Build simple classes (MockMotor, MockPotentiometer) that simulate hardware behavior with zero FTC dependencies. These run anywhere Java runs.
Write Your Logic Against Mocks
Your controller (ChuteController) only uses mock classes. It doesn't know or care about real hardware — it just implements the logic.
Create Hardware Adapters
Build adapter classes (FtcMotor, FtcPotentiometer) that extend your mocks and wrap real hardware.
Configure Gradle to Exclude
Tell Gradle to skip the hardware adapters and OpModes when building on your PC. They only compile when deployed to Android.
Validate Everything on Your PC
Write unit tests that simulate time passing. Catch logic bugs at your desk, not during robot testing sessions.
Integrate with Confidence
When tests pass, your logic is validated. The remaining work focuses on hardware integration — wiring, configuration, and motor directions. Much clearer debugging!
The complete chute-controller project is available on nxgit.dev. Clone it, run the tests, and use it as a template for your own subsystems.