|
| 1 | +from dataclasses import dataclass |
| 2 | +from enum import Enum |
| 3 | + |
| 4 | +from wpilib.sysid import SysIdRoutineLog, State |
| 5 | +from ..command import Command |
| 6 | +from ..subsystem import Subsystem |
| 7 | +from wpilib import Timer |
| 8 | + |
| 9 | +from wpimath.units import seconds, volts |
| 10 | + |
| 11 | +from typing import Callable, Optional |
| 12 | + |
| 13 | + |
| 14 | +volts_per_second = float |
| 15 | + |
| 16 | + |
| 17 | +class SysIdRoutine(SysIdRoutineLog): |
| 18 | + """A SysId characterization routine for a single mechanism. Mechanisms may have multiple motors. |
| 19 | +
|
| 20 | + A single subsystem may have multiple mechanisms, but mechanisms should not share test |
| 21 | + routines. Each complete test of a mechanism should have its own SysIdRoutine instance, since the |
| 22 | + log name of the recorded data is determined by the mechanism name. |
| 23 | +
|
| 24 | + The test state (e.g. "quasistatic-forward") is logged once per iteration during test |
| 25 | + execution, and once with state "none" when a test ends. Motor frames are logged every iteration |
| 26 | + during test execution. |
| 27 | +
|
| 28 | + Timestamps are not coordinated across data, so motor frames and test state tags may be |
| 29 | + recorded on different log frames. Because frame alignment is not guaranteed, SysId parses the log |
| 30 | + by using the test state flag to determine the timestamp range for each section of the test, and |
| 31 | + then extracts the motor frames within the valid timestamp ranges. If a given test was run |
| 32 | + multiple times in a single logfile, the user will need to select which of the tests to use for |
| 33 | + the fit in the analysis tool. |
| 34 | + """ |
| 35 | + |
| 36 | + @dataclass |
| 37 | + class Config: |
| 38 | + """Hardware-independent configuration for a SysId test routine. |
| 39 | +
|
| 40 | + :param rampRate: The voltage ramp rate used for quasistatic test routines. Defaults to 1 volt |
| 41 | + per second if left null. |
| 42 | + :param stepVoltage: The step voltage output used for dynamic test routines. Defaults to 7 |
| 43 | + volts if left null. |
| 44 | + :param timeout: Safety timeout for the test routine commands. Defaults to 10 seconds if left |
| 45 | + null. |
| 46 | + :param recordState: Optional handle for recording test state in a third-party logging |
| 47 | + solution. If provided, the test routine state will be passed to this callback instead of |
| 48 | + logged in WPILog. |
| 49 | + """ |
| 50 | + |
| 51 | + rampRate: volts_per_second = 1.0 |
| 52 | + stepVoltage: volts = 7.0 |
| 53 | + timeout: seconds = 10.0 |
| 54 | + recordState: Optional[Callable[[State], None]] = None |
| 55 | + |
| 56 | + @dataclass |
| 57 | + class Mechanism: |
| 58 | + """A mechanism to be characterized by a SysId routine. |
| 59 | +
|
| 60 | + Defines callbacks needed for the SysId test routine to control |
| 61 | + and record data from the mechanism. |
| 62 | +
|
| 63 | + :param drive: Sends the SysId-specified drive signal to the mechanism motors during test |
| 64 | + routines. |
| 65 | + :param log: Returns measured data of the mechanism motors during test routines. To return |
| 66 | + data, call `motor(string motorName)` on the supplied `SysIdRoutineLog` instance, and then |
| 67 | + call one or more of the chainable logging handles (e.g. `voltage`) on the returned |
| 68 | + `MotorLog`. Multiple motors can be logged in a single callback by calling `motor` |
| 69 | + multiple times. |
| 70 | + :param subsystem: The subsystem containing the motor(s) that is (or are) being characterized. |
| 71 | + Will be declared as a requirement for the returned test commands. |
| 72 | + :param name: The name of the mechanism being tested. Will be appended to the log entry title |
| 73 | + for the routine's test state, e.g. "sysid-test-state-mechanism". Defaults to the name of |
| 74 | + the subsystem if left null. |
| 75 | + """ |
| 76 | + |
| 77 | + drive: Callable[[volts], None] |
| 78 | + log: Callable[[SysIdRoutineLog], None] |
| 79 | + subsystem: Subsystem |
| 80 | + name: Optional[str] = None |
| 81 | + |
| 82 | + def __post_init__(self): |
| 83 | + if self.name == None: |
| 84 | + self.name = self.subsystem.getName() |
| 85 | + |
| 86 | + class Direction(Enum): |
| 87 | + """Motor direction for a SysId test.""" |
| 88 | + |
| 89 | + kForward = 1 |
| 90 | + kReverse = -1 |
| 91 | + |
| 92 | + def __init__(self, config: Config, mechanism: Mechanism): |
| 93 | + """Create a new SysId characterization routine. |
| 94 | +
|
| 95 | + :param config: Hardware-independent parameters for the SysId routine. |
| 96 | + :param mechanism: Hardware interface for the SysId routine. |
| 97 | + """ |
| 98 | + super().__init__(mechanism.subsystem.getName()) |
| 99 | + self.config = config |
| 100 | + self.mechanism = mechanism |
| 101 | + self.outputVolts = 0.0 |
| 102 | + self.logState = config.recordState or self.recordState |
| 103 | + |
| 104 | + def quasistatic(self, direction: Direction) -> Command: |
| 105 | + """Returns a command to run a quasistatic test in the specified direction. |
| 106 | +
|
| 107 | + The command will call the `drive` and `log` callbacks supplied at routine construction once |
| 108 | + per iteration. Upon command end or interruption, the `drive` callback is called with a value of |
| 109 | + 0 volts. |
| 110 | +
|
| 111 | + :param direction: The direction in which to run the test. |
| 112 | +
|
| 113 | + :returns: A command to run the test. |
| 114 | + """ |
| 115 | + |
| 116 | + timer = Timer() |
| 117 | + if direction == self.Direction.kForward: |
| 118 | + state = State.kQuasistaticForward |
| 119 | + else: |
| 120 | + state = State.kQuasistaticReverse |
| 121 | + |
| 122 | + def execute(): |
| 123 | + self.outputVolts = direction.value * timer.get() * self.config.rampRate |
| 124 | + self.mechanism.drive(self.outputVolts) |
| 125 | + self.mechanism.log(self) |
| 126 | + self.logState(state) |
| 127 | + |
| 128 | + def end(interrupted: bool): |
| 129 | + self.mechanism.drive(0.0) |
| 130 | + self.logState(State.kNone) |
| 131 | + timer.stop() |
| 132 | + |
| 133 | + return ( |
| 134 | + self.mechanism.subsystem.runOnce(timer.start) |
| 135 | + .andThen(self.mechanism.subsystem.run(execute)) |
| 136 | + .finallyDo(end) |
| 137 | + .withName( |
| 138 | + f"sysid-{SysIdRoutineLog.stateEnumToString(state)}-{self.mechanism.name}" |
| 139 | + ) |
| 140 | + .withTimeout(self.config.timeout) |
| 141 | + ) |
| 142 | + |
| 143 | + def dynamic(self, direction: Direction) -> Command: |
| 144 | + """Returns a command to run a dynamic test in the specified direction. |
| 145 | +
|
| 146 | + The command will call the `drive` and `log` callbacks supplied at routine construction once |
| 147 | + per iteration. Upon command end or interruption, the `drive` callback is called with a value of |
| 148 | + 0 volts. |
| 149 | +
|
| 150 | + :param direction: The direction in which to run the test. |
| 151 | +
|
| 152 | + :returns: A command to run the test. |
| 153 | + """ |
| 154 | + |
| 155 | + if direction == self.Direction.kForward: |
| 156 | + state = State.kDynamicForward |
| 157 | + else: |
| 158 | + state = State.kDynamicReverse |
| 159 | + |
| 160 | + def command(): |
| 161 | + self.outputVolts = direction.value * self.config.stepVoltage |
| 162 | + |
| 163 | + def execute(): |
| 164 | + self.mechanism.drive(self.outputVolts) |
| 165 | + self.mechanism.log(self) |
| 166 | + self.logState(state) |
| 167 | + |
| 168 | + def end(interrupted: bool): |
| 169 | + self.mechanism.drive(0.0) |
| 170 | + self.logState(State.kNone) |
| 171 | + |
| 172 | + return ( |
| 173 | + self.mechanism.subsystem.runOnce(command) |
| 174 | + .andThen(self.mechanism.subsystem.run(execute)) |
| 175 | + .finallyDo(end) |
| 176 | + .withName( |
| 177 | + f"sysid-{SysIdRoutineLog.stateEnumToString(state)}-{self.mechanism.name}" |
| 178 | + ) |
| 179 | + .withTimeout(self.config.timeout) |
| 180 | + ) |
0 commit comments