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