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}