Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions SimLoop/docs/packages/opmode.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ A `LinearOpMode`'s `runOpMode()` runs on its own thread in lockstep with the sim
recording telemetry. What it cannot swap is listed in `result.warnings()`.
- **Threads the OpMode starts run in real time beside the sim.** They are listed in `result.warnings()`.
`.failOnOpModeThreads(true)` fails the run instead.
- **The map always has a battery.** A map with no voltage sensor gets a 12 V `FakeVoltageSensor` named
`"Control Hub"` when the run starts, as a real robot's map holds its hubs' sensors, so
`hardwareMap.voltageSensor.iterator().next()` works. Register your own `FakeVoltageSensor` to set another
voltage; the runner then adds none.
- **A runner runs once.** Build a new runner and a new OpMode to run again, as the SDK builds a fresh
OpMode for every INIT.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.firstinspires.ftc.robotcore.internal.opmode.OpModeServices;
import org.horizon36596.simloop.config.SimRobotConfig;
import org.horizon36596.simloop.fakehardware.FakeHardwareMap;
import org.horizon36596.simloop.fakehardware.FakeVoltageSensor;
import org.horizon36596.simloop.sim.FakeTimer;
import org.horizon36596.simloop.sim.ScenarioRunner;
import org.horizon36596.simloop.simclock.SimClockLoader;
Expand Down Expand Up @@ -122,6 +123,12 @@ public final class OpModeRunner {
/** The default tick, in seconds: 50 Hz, about what a real FTC control loop manages. */
public static final double DEFAULT_TICK_SECONDS = 0.02;

/**
* The name the runner gives the battery it adds to a map that has no voltage sensor: a Control Hub's own
* voltage sensor is configured under the hub's name, which is {@code "Control Hub"} unless the team renamed it.
*/
public static final String DEFAULT_VOLTAGE_SENSOR_NAME = "Control Hub";

/**
* The default for {@link #stuckTimeoutMillis(int)}: how long, in milliseconds of real time, an OpMode
* may run within one tick before the run is failed as stuck.
Expand Down Expand Up @@ -497,6 +504,20 @@ private static int requireNotNegative(int ticks, String name) {
return ticks;
}

/**
* A real robot's map always holds its hubs' voltage sensors, and robot code that compensates for the battery
* reads one as {@code hardwareMap.voltageSensor.iterator().next()}, which throws
* {@code NoSuchElementException} on a map with none. So a map with no voltage sensor gets a
* {@link FakeVoltageSensor} at its nominal 12 V under {@link #DEFAULT_VOLTAGE_SENSOR_NAME} before the OpMode
* sees it. It stays in the map after the run. A map that has a voltage sensor of its own is left alone, so a
* test that wants another voltage registers one.
*/
private void addBatteryIfMissing() {
if (hardwareMap.voltageSensor.size() == 0) {
hardwareMap.register(DEFAULT_VOLTAGE_SENSOR_NAME, new FakeVoltageSensor());
}
}

/** One run's state: which tick it is on, the stepper driving the OpMode, and what the run noticed. */
private final class Run {
final int totalTicks = 1 + initLoopTicks + activeTicks + 1;
Expand All @@ -522,6 +543,7 @@ private final class Run {
? new LinearOpModeStepper((LinearOpMode) opMode, context, tickSeconds, stuckTimeoutMillis,
threads.group())
: new IterativeOpModeStepper(opMode, context, stuckTimeoutMillis, threads.group());
addBatteryIfMissing();
rewiring = context.inject(hardwareMap, telemetry, services());
simClockBridge = simClock != null ? new SimClockBridge(opModeName, stepper) : null;
if (simClock != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.horizon36596.simloop.opmode;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.hardware.VoltageSensor;

import org.horizon36596.simloop.fakehardware.FakeHardwareMap;
import org.horizon36596.simloop.fakehardware.FakeVoltageSensor;
import org.junit.jupiter.api.Test;

/**
* Pins the battery the runner adds: robot code that compensates for the battery reads
* {@code hardwareMap.voltageSensor.iterator().next()}, which a real robot's map always answers (its hubs' voltage
* sensors) and an empty fake map used to answer with {@code NoSuchElementException} at INIT.
*
* <p>Implementation tier: the 12 V and the name are the runner's documented defaults.
*/
class DefaultBatteryTest {

/** Reads the battery in init() the way voltage-compensated robot code does. */
static class ReadsTheBattery extends OpMode {
VoltageSensor battery;
double volts = Double.NaN;

@Override
public void init() {
battery = hardwareMap.voltageSensor.iterator().next();
volts = battery.getVoltage();
}

@Override
public void loop() {
}
}

@Test
void aMapWithNoVoltageSensorGetsATwelveVoltBatteryNamedForTheControlHub() {
FakeHardwareMap hardwareMap = new FakeHardwareMap();
ReadsTheBattery opMode = new ReadsTheBattery();

new OpModeRunner(opMode, hardwareMap).initLoopTicks(1).activeTicks(1).run();

assertEquals(12.0, opMode.volts, 0.0, "volts");
assertEquals(1, hardwareMap.voltageSensor.size(), "one battery, the runner's");
assertSame(opMode.battery, hardwareMap.voltageSensor.get(OpModeRunner.DEFAULT_VOLTAGE_SENSOR_NAME),
"under the Control Hub's name");
}

@Test
void aMapWithItsOwnVoltageSensorIsLeftAlone() {
FakeHardwareMap hardwareMap = new FakeHardwareMap();
FakeVoltageSensor low = new FakeVoltageSensor(11.2); // volts: a test of a tired battery
hardwareMap.register("Expansion Hub 2", low);
ReadsTheBattery opMode = new ReadsTheBattery();

new OpModeRunner(opMode, hardwareMap).initLoopTicks(1).activeTicks(1).run();

assertSame(low, opMode.battery, "the test's sensor, not one the runner made");
assertEquals(11.2, opMode.volts, 0.0, "volts");
assertEquals(1, hardwareMap.voltageSensor.size(), "the runner added none");
}
}