RUN_TO_POSITION and RUN_USING_ENCODER in FakeMotor¶
FakeMotor runs the two closed-loop modes the way a REV Control Hub or Expansion Hub does: the SDK side
exactly as the FTC SDK 12.0.0 source does it, and the firmware side as a labelled approximation. This page
says which is which, and what to measure on a real hub to check the approximations.
Using it¶
Nothing special. Robot code that works on the robot works here:
arm.setTargetPosition(178); // target first...
arm.setMode(DcMotor.RunMode.RUN_TO_POSITION); // ...then the mode...
arm.setPower(0.8); // ...then the speed cap
while (arm.isBusy()) { /* ... */ }
The one thing the sim needs from you is the load. A FakeMotor knows nothing about the arm or slide on
it, so gravity has to be told, every tick, as a share of the motor's stall torque in the physical encoder
direction:
// Zero ticks = arm horizontal, positive lifts it. Holding it level takes 30% of stall torque.
double angle = arm.getPhysicalPositionTicks() / TICKS_PER_RADIAN;
arm.setExternalTorqueFraction(-0.3 * Math.cos(angle));
arm.update(deltaTime);
A vertical slide is a constant, for example setExternalTorqueFraction(-0.15). getAppliedPhysicalPower()
tells you what the hub is really putting on the windings; getPower() only reports what the robot code sent,
which in RUN_TO_POSITION is the speed cap.
Mechanism1DofPlant reads getPower(), so it can only be driven open loop (RUN_WITHOUT_ENCODER). For a
mechanism in RUN_TO_POSITION or RUN_USING_ENCODER, put its load on the motor as above.
Exact: the SDK's own behaviour¶
Read off the SDK 12.0.0 sources (DcMotorImpl, DcMotorImplEx, LynxDcMotorController, the Lynx command
classes). Tests pin every one; none should ever be loosened.
| Behaviour | What happens |
|---|---|
| Missing target | Entering RUN_TO_POSITION before any setTargetPosition throws TargetPositionNotSetException: "Failed to enable motor. You must set a target position before switching to RUN_TO_POSITION mode". The hub refuses the channel enable that follows the mode change. The mode is recorded first, so getMode() then reports RUN_TO_POSITION, with the channel off. Every later setPower, setVelocity or setMotorEnable throws the same until a target is sent. |
| Order | setTargetPosition is accepted in any mode. setMode(RUN_TO_POSITION) re-sends the last power, so the motor starts moving at once, capped by the power already set. |
| Power | In RUN_TO_POSITION setPower(p) sends abs(p), and getPower() reports abs(p). The hub turns it into a speed cap of abs(p) * maxTicksPerSecond, where maxTicksPerSecond is the configured motor type's getAchieveableMaxTicksPerSecondRounded(). In RUN_USING_ENCODER setPower(p) sends (int)(direction * p * maxTicksPerSecond) as the velocity target. |
isBusy() |
False in every mode but RUN_TO_POSITION. In RUN_TO_POSITION it is !atTarget, the hub's answer for the target and tolerance it was sent. Being "not busy" does not stop the motor: the hub keeps holding the target. |
| Tolerance | Default 5 ticks. setTargetPositionTolerance only stores it on the handle. It reaches the hub with the next setTargetPosition, so changing it alone changes nothing. A value outside [0, 65535] makes that setTargetPosition throw IllegalArgumentException("illegal tolerance: N"). |
| Direction | REVERSE negates the target, the power and a velocity command when they are sent, and the position and velocity when they are read. setDirection sends nothing, so flipping it on a running motor changes the readings, not the motion. |
| Encoder reset | STOP_AND_RESET_ENCODER stops the shaft and zeroes the count. The last power is kept and re-sent when the mode changes back. |
| Leaving RUN_TO_POSITION | The mode change re-sends the cached power, which is abs(p) with no direction applied. An arm switched straight to RUN_USING_ENCODER runs at that power in the positive raw direction. |
setVelocity |
Switches to RUN_USING_ENCODER unless already in it or RUN_TO_POSITION, then sends round(direction * v) clipped to +/-32767 ticks/s. The AngleUnit overload converts through the configured motor type's counts per revolution. |
getVelocity |
Whole ticks per second, because the hub reports an int. The AngleUnit overload converts that whole number: ticks / CPR * 360 degrees. |
| PIDF | setPIDFCoefficients on a mode that is not a PID mode throws IllegalArgumentException. RUN_TO_POSITION with a non-zero I, D or F under the PIDF algorithm throws UnsupportedOperationException ("setting of pidf coefficents not supported: ...", the SDK's spelling) and changes nothing; LegacyPID is exempt. The hub stores each gain as round(abs(c) * 65536) * sign(c), so what comes back is quantized to 1/65536. getPIDFCoefficients and getPIDCoefficients accept exactly RUN_USING_ENCODER and RUN_TO_POSITION. resetDeviceConfigurationForOpMode puts the default gains back. |
| Structure | The position loop feeds the velocity loop ("our fw runs a position loop, that then runs a velocity loop", LynxDcMotorController). |
Approximation: the firmware¶
The hub firmware is closed source. These numbers are the fake's best reading and are labelled
APPROXIMATION in HubMotorControlLoop. Tests of them carry headroom.
| Approximation | Value in the fake | Why this value |
|---|---|---|
| Default velocity gains | P 10, I 3, D 0, F 0 (PIDF) | What teams read back from getPIDFCoefficients(RUN_USING_ENCODER). goBILDA motor types in the SDK declare none, so these come from the firmware. |
| Default position gain | P 10 (PIDF) | The SDK's own Core Hex motor type declares position P = 10. Unconfirmed for other motors on current firmware. |
| Gain units | Output 32767 = full power; velocity error in ticks/s; position P gives ticks/s of velocity target per tick of error. | The FTC tuning rule F = 32767 / maxTicksPerSecond only works in these units. |
| Loop rate | One pass every 10 ms (100 Hz). The I term adds I * error once per pass. |
Not published. Chosen so the default gains settle a velocity step in a few hundred ms with light overshoot. |
| Integral wind-up | The I term is clamped to full scale, and cleared on every mode change, a disabled channel and an encoder reset. | Not published. |
| Velocity measurement | The loop sees the modelled shaft speed exactly; getVelocity() rounds it to the nearest whole tick/s. |
The real hub measures counts over a window, with some lag and noise. |
| At-target test | abs(target - count) <= tolerance, position only, no speed condition, so a motor passing through the target at speed reads not busy for that moment. |
The hub's test is not published, including whether the boundary is inclusive. |
| Target memory | A target, once sent, is kept across mode changes and encoder resets; nothing clears it. | Whether the firmware forgets its target on a reset is not published. |
| Not modelled | Battery voltage. The SDK's 500 ms power and enable caches: the fake sends every command and every enable, so where a robot repeating the same power within 500 ms sends nothing, and would throw TargetPositionNotSetException one call later, the fake throws at once. Bulk-caching isBusy() outside RUN_TO_POSITION (the fake says false). FLOAT vs BRAKE. |
What follows from these, and should be read as approximate: how long a move takes, how far it overshoots,
how much an arm sags before the loop catches it, and whether a sluggish mechanism rings. With the default
gains a sluggish motor rings in RUN_TO_POSITION: at maxAccel 6 per second it takes over a second to
settle, and at 4 it is still ringing after 8 s.
The real-hub check¶
One motor with an encoder, on a hub with current firmware, logging at the bulk-read rate. For each item,
compare the robot's numbers with the same commands run through FakeMotor with that motor's free speed and
maxAccel.
- Default gains. Print
getPIDFCoefficients(RUN_USING_ENCODER)andgetPIDFCoefficients(RUN_TO_POSITION)on a freshly powered hub, for a goBILDA motor type. Expect 10/3/0/0 and 10/0/0/0. Anything else replacesdefaultVelocityCoefficients()ordefaultPositionCoefficients(). - Velocity step. Unloaded,
setVelocity(half of free speed)from rest. Measure the time to reach 98%, the peak overshoot and the steady error. This checks the gain units and the loop rate together. The fake reaches 98% in about 0.3 s with about 10% overshoot formaxAccel12. - Feedforward units. Set P = I = D = 0 and F = 32767 / free speed, then
setVelocity(half). If the motor settles at half speed, the output units are right. - Position step. Unloaded,
setTargetPosition(2000), RUN_TO_POSITION, power 1.0. Log position andisBusy()every loop. Measure the time to first not-busy, the peak overshoot in ticks, and whetherisBusy()flickers as the motor passes through the target. Repeat at power 0.3 and check the peak speed is close to 0.3 of free speed. - Holding under load. An arm held horizontal in RUN_TO_POSITION. Measure the steady error and how the current compares with holding it open loop. Push it off by hand and time the return.
- Tolerance boundary. In RUN_TO_POSITION at power 0 the hub holds the motor still, so set the target
to exactly the current position + 5 (tolerance 5) and read
isBusy(). False means the boundary is inclusive, as the fake assumes. - Target after reset. Reach a target,
STOP_AND_RESET_ENCODER, then RUN_TO_POSITION with no newsetTargetPosition. If it throwsTargetPositionNotSetException, the firmware forgets its target on a reset, andhubTargetPositionSetmust be cleared there.
Record the date, firmware version and motor for each result next to the constant it confirms or replaces.