Skip to main content

Lesson 9.5: Challenge — Sync a Dual-Servo Gripper with Offset Tuning


Technical Context

Mechanical misalignment or "slop" in servo splines means two physically identical servos may reach their effective grip position at slightly different software values. Applying position offsets to each servo independently compensates for this, ensuring the gripper holds a game element securely without one side stalling against its physical stop while the other is still moving.


How to Tune Two Servos So They Work Together

This challenge combines setDirection() from Lesson 9.3 with else if chaining from Unit 5, and adds the concept of per-servo offsets. Rather than commanding both servos to the exact same value, you define named constants for each state (OPEN, CLOSED, READY) and apply a small offset to whichever servo needs mechanical compensation.

Using else if chaining instead of multiple independent if blocks is critical here — it prevents servo jitter. If two if blocks both evaluate to true in the same loop cycle (for example, if two buttons are held simultaneously), the servo would receive two conflicting position commands within a single 50Hz update, causing rapid mechanical oscillation that can strip servo gears.

A single else if chain guarantees the servo receives exactly one position command per loop cycle.


Annotated Code

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.Servo;

@TeleOp(name="Dual_Servo_Demo")
public class DualServoDemo extends OpMode {

private Servo leftClaw;
private Servo rightClaw;

// Named constants for each gripper state
// Right servo has a +0.05 offset to compensate for mechanical slop
private static final double CLOSED_LEFT = 1.0;
private static final double CLOSED_RIGHT = 0.95; // offset applied here
private static final double OPEN_LEFT = 0.0;
private static final double OPEN_RIGHT = 0.0;
private static final double READY_POS = 0.5;

@Override
public void init() {
leftClaw = hardwareMap.get(Servo.class, "left_claw");
rightClaw = hardwareMap.get(Servo.class, "right_claw");

rightClaw.setDirection(Servo.Direction.REVERSE);

leftClaw.setPosition(READY_POS);
rightClaw.setPosition(READY_POS);
telemetry.addData("Gripper", "Ready at center");
}

@Override
public void loop() {
// Mutually exclusive states — exactly one command sent per cycle
if (gamepad1.a) {
leftClaw.setPosition(CLOSED_LEFT);
rightClaw.setPosition(CLOSED_RIGHT);
telemetry.addData("Gripper", "Closed");
} else if (gamepad1.b) {
leftClaw.setPosition(OPEN_LEFT);
rightClaw.setPosition(OPEN_RIGHT);
telemetry.addData("Gripper", "Open");
} else {
leftClaw.setPosition(READY_POS);
rightClaw.setPosition(READY_POS);
telemetry.addData("Gripper", "Ready");
}
}
}

Fill-in-the-Blank Practice

  1. Using else if statements instead of multiple independent if statements ensures the servo receives only __________ position command(s) per loop cycle.
  2. Providing a __________ state in the else clause ensures the servo returns to a predictable position when no buttons are pressed.
  3. Servos may exhibit __________ if they receive multiple conflicting setPosition() commands within the same 50Hz loop update.
Show answers
  1. one
  2. default (or rest / ready)
  3. jitter (rapid mechanical oscillation)

Template Challenge

Robot Scenario: Synchronize two servos — "servoL" and "servoR". Apply a 0.05 offset to servoR to compensate for mechanical slop. When gamepad1.x is pressed, servoL moves to 0.1 and servoR moves to 0.85 (mirrored close with offset). Otherwise both rest at 0.5.

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.Servo;

@TeleOp(name="Dual_Sync_Challenge")
public class DualSync extends OpMode {

private Servo servoL;
private Servo servoR;

// INSERT CODE HERE: Define static final constants for:
// CLOSE_L = 0.1, CLOSE_R = 0.85, REST = 0.5

@Override
public void init() {
servoL = hardwareMap.get(Servo.class, "servoL");
servoR = hardwareMap.get(Servo.class, "servoR");

// INSERT CODE HERE: Reverse servoR direction
// INSERT CODE HERE: Set both to REST position initially
}

@Override
public void loop() {
// INSERT CODE HERE: If X pressed, command CLOSE positions
// Otherwise, command REST positions for both servos
}
}
Show answer
private static final double CLOSE_L = 0.1;
private static final double CLOSE_R = 0.85;
private static final double REST = 0.5;

@Override
public void init() {
servoL = hardwareMap.get(Servo.class, "servoL");
servoR = hardwareMap.get(Servo.class, "servoR");

servoR.setDirection(Servo.Direction.REVERSE);

servoL.setPosition(REST);
servoR.setPosition(REST);
telemetry.addData("Servos", "Initialized at rest");
}

@Override
public void loop() {
if (gamepad1.x) {
servoL.setPosition(CLOSE_L);
servoR.setPosition(CLOSE_R);
telemetry.addData("Gripper", "Closed");
} else {
servoL.setPosition(REST);
servoR.setPosition(REST);
telemetry.addData("Gripper", "Rest");
}
}

Ready to move on?

Sign in with Google to save your progress with Telemark, or continue without saving.