001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj2.command.sysid;
006
007import static edu.wpi.first.units.Units.Second;
008import static edu.wpi.first.units.Units.Seconds;
009import static edu.wpi.first.units.Units.Volts;
010import static java.util.Map.entry;
011
012import edu.wpi.first.units.VoltageUnit;
013import edu.wpi.first.units.measure.Time;
014import edu.wpi.first.units.measure.Velocity;
015import edu.wpi.first.units.measure.Voltage;
016import edu.wpi.first.wpilibj.Timer;
017import edu.wpi.first.wpilibj.sysid.SysIdRoutineLog;
018import edu.wpi.first.wpilibj2.command.Command;
019import edu.wpi.first.wpilibj2.command.Subsystem;
020import java.util.Map;
021import java.util.function.Consumer;
022
023/**
024 * A SysId characterization routine for a single mechanism. Mechanisms may have multiple motors.
025 *
026 * <p>A single subsystem may have multiple mechanisms, but mechanisms should not share test
027 * routines. Each complete test of a mechanism should have its own SysIdRoutine instance, since the
028 * log name of the recorded data is determined by the mechanism name.
029 *
030 * <p>The test state (e.g. "quasistatic-forward") is logged once per iteration during test
031 * execution, and once with state "none" when a test ends. Motor frames are logged every iteration
032 * during test execution.
033 *
034 * <p>Timestamps are not coordinated across data, so motor frames and test state tags may be
035 * recorded on different log frames. Because frame alignment is not guaranteed, SysId parses the log
036 * by using the test state flag to determine the timestamp range for each section of the test, and
037 * then extracts the motor frames within the valid timestamp ranges. If a given test was run
038 * multiple times in a single logfile, the user will need to select which of the tests to use for
039 * the fit in the analysis tool.
040 */
041public class SysIdRoutine extends SysIdRoutineLog {
042  private final Config m_config;
043  private final Mechanism m_mechanism;
044  private final Consumer<State> m_recordState;
045
046  /**
047   * Create a new SysId characterization routine.
048   *
049   * @param config Hardware-independent parameters for the SysId routine.
050   * @param mechanism Hardware interface for the SysId routine.
051   */
052  public SysIdRoutine(Config config, Mechanism mechanism) {
053    super(mechanism.m_name);
054    m_config = config;
055    m_mechanism = mechanism;
056    m_recordState = config.m_recordState != null ? config.m_recordState : this::recordState;
057  }
058
059  /** Hardware-independent configuration for a SysId test routine. */
060  public static class Config {
061    /** The voltage ramp rate used for quasistatic test routines. */
062    public final Velocity<VoltageUnit> m_rampRate;
063
064    /** The step voltage output used for dynamic test routines. */
065    public final Voltage m_stepVoltage;
066
067    /** Safety timeout for the test routine commands. */
068    public final Time m_timeout;
069
070    /** Optional handle for recording test state in a third-party logging solution. */
071    public final Consumer<State> m_recordState;
072
073    /**
074     * Create a new configuration for a SysId test routine.
075     *
076     * @param rampRate The voltage ramp rate used for quasistatic test routines. Defaults to 1 volt
077     *     per second if left null.
078     * @param stepVoltage The step voltage output used for dynamic test routines. Defaults to 7
079     *     volts if left null.
080     * @param timeout Safety timeout for the test routine commands. Defaults to 10 seconds if left
081     *     null.
082     * @param recordState Optional handle for recording test state in a third-party logging
083     *     solution. If provided, the test routine state will be passed to this callback instead of
084     *     logged in WPILog.
085     */
086    public Config(
087        Velocity<VoltageUnit> rampRate,
088        Voltage stepVoltage,
089        Time timeout,
090        Consumer<State> recordState) {
091      m_rampRate = rampRate != null ? rampRate : Volts.of(1).per(Second);
092      m_stepVoltage = stepVoltage != null ? stepVoltage : Volts.of(7);
093      m_timeout = timeout != null ? timeout : Seconds.of(10);
094      m_recordState = recordState;
095    }
096
097    /**
098     * Create a new configuration for a SysId test routine.
099     *
100     * @param rampRate The voltage ramp rate used for quasistatic test routines. Defaults to 1 volt
101     *     per second if left null.
102     * @param stepVoltage The step voltage output used for dynamic test routines. Defaults to 7
103     *     volts if left null.
104     * @param timeout Safety timeout for the test routine commands. Defaults to 10 seconds if left
105     *     null.
106     */
107    public Config(Velocity<VoltageUnit> rampRate, Voltage stepVoltage, Time timeout) {
108      this(rampRate, stepVoltage, timeout, null);
109    }
110
111    /**
112     * Create a default configuration for a SysId test routine with all default settings.
113     *
114     * <p>rampRate: 1 volt/sec
115     *
116     * <p>stepVoltage: 7 volts
117     *
118     * <p>timeout: 10 seconds
119     */
120    public Config() {
121      this(null, null, null, null);
122    }
123  }
124
125  /**
126   * A mechanism to be characterized by a SysId routine. Defines callbacks needed for the SysId test
127   * routine to control and record data from the mechanism.
128   */
129  public static class Mechanism {
130    /** Sends the SysId-specified drive signal to the mechanism motors during test routines. */
131    public final Consumer<? super Voltage> m_drive;
132
133    /**
134     * Returns measured data (voltages, positions, velocities) of the mechanism motors during test
135     * routines.
136     */
137    public final Consumer<SysIdRoutineLog> m_log;
138
139    /** The subsystem containing the motor(s) that is (or are) being characterized. */
140    public final Subsystem m_subsystem;
141
142    /** The name of the mechanism being tested. */
143    public final String m_name;
144
145    /**
146     * Create a new mechanism specification for a SysId routine.
147     *
148     * @param drive Sends the SysId-specified drive signal to the mechanism motors during test
149     *     routines.
150     * @param log Returns measured data of the mechanism motors during test routines. To return
151     *     data, call `motor(string motorName)` on the supplied `SysIdRoutineLog` instance, and then
152     *     call one or more of the chainable logging handles (e.g. `voltage`) on the returned
153     *     `MotorLog`. Multiple motors can be logged in a single callback by calling `motor`
154     *     multiple times.
155     * @param subsystem The subsystem containing the motor(s) that is (or are) being characterized.
156     *     Will be declared as a requirement for the returned test commands.
157     * @param name The name of the mechanism being tested. Will be appended to the log entry title
158     *     for the routine's test state, e.g. "sysid-test-state-mechanism". Defaults to the name of
159     *     the subsystem if left null.
160     */
161    public Mechanism(
162        Consumer<Voltage> drive, Consumer<SysIdRoutineLog> log, Subsystem subsystem, String name) {
163      m_drive = drive;
164      m_log = log != null ? log : l -> {};
165      m_subsystem = subsystem;
166      m_name = name != null ? name : subsystem.getName();
167    }
168
169    /**
170     * Create a new mechanism specification for a SysId routine. Defaults the mechanism name to the
171     * subsystem name.
172     *
173     * @param drive Sends the SysId-specified drive signal to the mechanism motors during test
174     *     routines.
175     * @param log Returns measured data of the mechanism motors during test routines. To return
176     *     data, call `motor(string motorName)` on the supplied `SysIdRoutineLog` instance, and then
177     *     call one or more of the chainable logging handles (e.g. `voltage`) on the returned
178     *     `MotorLog`. Multiple motors can be logged in a single callback by calling `motor`
179     *     multiple times.
180     * @param subsystem The subsystem containing the motor(s) that is (or are) being characterized.
181     *     Will be declared as a requirement for the returned test commands. The subsystem's `name`
182     *     will be appended to the log entry title for the routine's test state, e.g.
183     *     "sysid-test-state-subsystem".
184     */
185    public Mechanism(Consumer<Voltage> drive, Consumer<SysIdRoutineLog> log, Subsystem subsystem) {
186      this(drive, log, subsystem, null);
187    }
188  }
189
190  /** Motor direction for a SysId test. */
191  public enum Direction {
192    /** Forward. */
193    kForward,
194    /** Reverse. */
195    kReverse
196  }
197
198  /**
199   * Returns a command to run a quasistatic test in the specified direction.
200   *
201   * <p>The command will call the `drive` and `log` callbacks supplied at routine construction once
202   * per iteration. Upon command end or interruption, the `drive` callback is called with a value of
203   * 0 volts.
204   *
205   * @param direction The direction in which to run the test.
206   * @return A command to run the test.
207   */
208  public Command quasistatic(Direction direction) {
209    State state;
210    if (direction == Direction.kForward) {
211      state = State.kQuasistaticForward;
212    } else { // if (direction == Direction.kReverse) {
213      state = State.kQuasistaticReverse;
214    }
215
216    double outputSign = direction == Direction.kForward ? 1.0 : -1.0;
217
218    Timer timer = new Timer();
219    return m_mechanism
220        .m_subsystem
221        .runOnce(timer::restart)
222        .andThen(
223            m_mechanism.m_subsystem.run(
224                () -> {
225                  m_mechanism.m_drive.accept(
226                      (Voltage) m_config.m_rampRate.times(Seconds.of(timer.get() * outputSign)));
227                  m_mechanism.m_log.accept(this);
228                  m_recordState.accept(state);
229                }))
230        .finallyDo(
231            () -> {
232              m_mechanism.m_drive.accept(Volts.of(0));
233              m_recordState.accept(State.kNone);
234              timer.stop();
235            })
236        .withName("sysid-" + state.toString() + "-" + m_mechanism.m_name)
237        .withTimeout(m_config.m_timeout.in(Seconds));
238  }
239
240  /**
241   * Returns a command to run a dynamic test in the specified direction.
242   *
243   * <p>The command will call the `drive` and `log` callbacks supplied at routine construction once
244   * per iteration. Upon command end or interruption, the `drive` callback is called with a value of
245   * 0 volts.
246   *
247   * @param direction The direction in which to run the test.
248   * @return A command to run the test.
249   */
250  public Command dynamic(Direction direction) {
251    double outputSign = direction == Direction.kForward ? 1.0 : -1.0;
252    State state =
253        Map.ofEntries(
254                entry(Direction.kForward, State.kDynamicForward),
255                entry(Direction.kReverse, State.kDynamicReverse))
256            .get(direction);
257    Voltage[] output = {Volts.zero()};
258
259    return m_mechanism
260        .m_subsystem
261        .runOnce(() -> output[0] = m_config.m_stepVoltage.times(outputSign))
262        .andThen(
263            m_mechanism.m_subsystem.run(
264                () -> {
265                  m_mechanism.m_drive.accept(output[0]);
266                  m_mechanism.m_log.accept(this);
267                  m_recordState.accept(state);
268                }))
269        .finallyDo(
270            () -> {
271              m_mechanism.m_drive.accept(Volts.of(0));
272              m_recordState.accept(State.kNone);
273            })
274        .withName("sysid-" + state.toString() + "-" + m_mechanism.m_name)
275        .withTimeout(m_config.m_timeout.in(Seconds));
276  }
277}