|
| 1 | +# EasyCRT Absolute Encoder Configuration Guide |
| 2 | + |
| 3 | +This guide shows how to configure the EasyCRT solver (`EasyCRT` + `EasyCRTConfig`) |
| 4 | + |
| 5 | +## How the solver works |
| 6 | +- You supply two absolute enocder angles (wrapping to [0,1) is taken care of by the config) plus their rotations-per-mechanism-rotation ratios. |
| 7 | +- The solver enumerates every mechanism angle that fits encoder 1 within the allowed mechanism range, predicts what encoder 2 should read, and measures modular error. |
| 8 | +- The best match inside your tolerance becomes the mechanism angle; near ties become `AMBIGUOUS`; no in-range match becomes `NO_SOLUTION`. |
| 9 | +- The iteration count stays reasonable. Typical solves are tens of iterations. Log `getLastIterations()` to view this. The gear recommender also filters gear pairs whose theoretical iterations exceed your limit. |
| 10 | + |
| 11 | +## Required inputs |
| 12 | +- Provide two `Supplier<Angle>` values (encoder 1 and encoder 2). If a supplier returns `null`, it is treated as `NaN` and the solve will fail. |
| 13 | +- All other settings are configured via `EasyCRTConfig` before passing it to `EasyCRT`. |
| 14 | + |
| 15 | +## Ratio / gearing setup |
| 16 | +Pick ONE path per encoder to define rotations per mechanism rotation. |
| 17 | + |
| 18 | +- **Direct ratio**: `withEncoderRatios(enc1RotPerMechRot, enc2RotPerMechRot)` when you already computed the ratios. Example: |
| 19 | + ```java |
| 20 | + easyCrt.withEncoderRatios(/* enc1 */ 50.0, /* enc2 */ 18.3333); |
| 21 | + ``` |
| 22 | +- **Shared drive gear**: `withCommonDriveGear(commonRatio, driveGearTeeth, encoder1PinionTeeth, encoder2PinionTeeth)` |
| 23 | + - `commonRatio` = mechanism rotations : shared drive gear rotations (the gear that meshes with both encoder pinions). |
| 24 | + - Seeds prime tooth counts, the common scale `k`, and stage 1 gear teeth + stage 2 ratio for gear recommendations. |
| 25 | + - Example: turret gearbox is 12:50 -> 10:110; encoders use 30T and 31T pinions driven by the 50t gear. `commonRatio` is 110.0 / 10.0 = 11.0; shared drive gear is 50T: |
| 26 | + ```java |
| 27 | + easyCrt.withCommonDriveGear(/* commonRatio */ 11.0, |
| 28 | + /* driveGearTeeth */ 50, |
| 29 | + /* encoder1Pinion */ 30, |
| 30 | + /* encoder2Pinion */ 31); |
| 31 | + ``` |
| 32 | +- **Gear chain (simple mesh sequence)**: `withAbsoluteEncoder1Gearing(teeth...)` / `withAbsoluteEncoder2Gearing(teeth...)` |
| 33 | + - Teeth listed from mechanism-side gear to encoder pinion. Example: |
| 34 | + ```java |
| 35 | + easyCrt.withAbsoluteEncoder1Gearing(50, 20, 40); // 50 drives 20, 20 drives 40 |
| 36 | + easyCrt.withAbsoluteEncoder2Gearing(60, 20); // 60 drives 20 |
| 37 | + ``` |
| 38 | +- **Explicit stages (driver, driven pairs)**: `withAbsoluteEncoder1GearingStages(driver1, driven1, driver2, driven2, ...)` / `withAbsoluteEncoder2GearingStages(...)` |
| 39 | + - Use for compound or same-shaft trains. Example: |
| 40 | + ```java |
| 41 | + easyCrt.withAbsoluteEncoder1GearingStages(12, 36, 18, 60); // 12->36, then 18->60 |
| 42 | + easyCrt.withAbsoluteEncoder2GearingStages(12, 60); // single stage 12->60 |
| 43 | + ``` |
| 44 | +- **Inversion**: Configure both in one place (preferred). We reccomend that offsets be applied in the config to avoid confusion, but if your vendor's encoder supports on-device offsets, that is fine, however DO NOT set them in both places: |
| 45 | + ```java |
| 46 | + easyCrt.withAbsoluteEncoderInversions(/* enc1 inverted */ false, |
| 47 | + /* enc2 inverted */ true); |
| 48 | + ``` |
| 49 | + Leave device-side inversion at defaults; set inversion here as the single source of truth. The CTRE CANCoder configurator in YAMS resets device inversion to a known baseline, so you will not carry stale on-device inversion. |
| 50 | +
|
| 51 | +Sanity checks: |
| 52 | +- Ratios must be finite and non-zero; tooth counts must be positive. |
| 53 | +- If neither direct ratios nor gearing are provided, the configuration throws an error. |
| 54 | +- When using chains or stages, verify the order (mechanism -> ... -> encoder). Reversed order yields the wrong ratio. |
| 55 | +
|
| 56 | +## Offsets and limits |
| 57 | +- **Offsets before wrap**: `withAbsoluteEncoderOffsets(enc1Offset, enc2Offset)` (in rotations) shifts raw readings prior to wrapping into `[0, 1)`. Set offsets here after mechanical zeroing so both encoders read as close to 0.0 rotations as possible at your reference pose. Keep device-side offsets at defaults; the CTRE CANCoder configurator resets device offsets, so the config holds the offsets. Similar to inversion, set this where you want but DO NOT do it twice. |
| 58 | +- **Motion limits**: `withMechanismRange(minAngle, maxAngle)` (rotations). The solver enumerates only within this window; narrower windows shrink the candidate set. |
| 59 | +- **Match tolerance**: `withMatchTolerance(tolerance)` (rotations of encoder 2). Start near the expected backlash mapped through encoder 2’s ratio; too small -> `NO_SOLUTION`, too large -> risk of `AMBIGUOUS`. If you always preload the turret (or mechanism) to one side of its backlash before solving, effective backlash drops and you can run a smaller tolerance. Monitor `getLastErrorRotations()` while tuning. |
| 60 | +
|
| 61 | +## Coverage (unique range) and primes |
| 62 | +- When you provide prime tooth counts and a common scale, `getUniqueCoverage()` returns the mechanism rotations you can uniquely represent. |
| 63 | +- Provided automatically when you use `withCommonDriveGear` (it seeds `encoder1PrimeTeeth`, `encoder2PrimeTeeth`, and `commonScaleK = commonRatio * driveGearTeeth`). |
| 64 | +- Formula: `coverageRot = lcm(encoder1PrimeTeeth, encoder2PrimeTeeth) / commonScaleK`. |
| 65 | +- `coverageSatisfiesRange()` checks whether the computed coverage is greater than or equal to your configured `maxMechanismAngle`. Use this as a guardrail when choosing pinion teeth. |
| 66 | +- Tip: pick pinion teeth that are coprime to maximize coverage; otherwise `lcm` shrinks and you may not cover your travel. |
| 67 | +
|
| 68 | +## Gear recommender workflow |
| 69 | +Goal: find the smallest coprime-ish gear pair that yields enough unique coverage with acceptable solve iterations. |
| 70 | +- Prereqs (often auto-set by `withCommonDriveGear`): |
| 71 | + - Stage 1 gear teeth (the shared drive gear for both encoders). |
| 72 | + - Stage 2 ratio (mechanism : drive gear). |
| 73 | +- If you do NOT use `withCommonDriveGear`, you can still seed the recommender manually with `withCrtGearRecommendationInputs(stage1GearTeeth, stage2Ratio)` before calling `withCrtGearRecommendationConstraints(...)`. |
| 74 | +- Configure constraints: `withCrtGearRecommendationConstraints(coverageMargin, minTeeth, maxTeeth, maxIterations)` and (if needed) `withCrtGearRecommendationInputs(stage1GearTeeth, stage2Ratio)`. |
| 75 | + - Runs only in simulation to avoid extra CPU on-robot. |
| 76 | + - `coverageMargin` multiplies your `maxMechanismAngle` to enforce slack (for example, 1.2 adds 20 percent headroom). |
| 77 | + - `maxIterations` rejects pairs that would require too many candidate checks per solve (the real count is visible via `getLastIterations()`). |
| 78 | +- Call `getRecommendedCrtGearPair()` in sim; it returns `Optional<CrtGearPair>` with: |
| 79 | + - `gearA`, `gearB`: tooth counts. |
| 80 | + - `coverage`: resulting unique mechanism coverage (rotations). |
| 81 | + - `lcm`, `gcd`, and `theoreticalIterations`. |
| 82 | +- Use the returned pinions as `encoder1PinionTeeth` / `encoder2PinionTeeth` in `withCommonDriveGear` (or your own chain/stage builders) and re-check `coverageSatisfiesRange()`. |
| 83 | +
|
| 84 | +## Practical gearing guidance |
| 85 | +- Ratio equals the total reduction to the mechanism. Ensure backlash mapped to encoder 2 does not exceed your tolerance. |
| 86 | +- Chain or belt stages: include every stage when you compute ratios or describe the chain; missing a stage collapses your coverage and can cause ambiguity. |
| 87 | +- Prime tooth choice: pick small, coprime pinions (for example, 19T + 21T) for high coverage with minimal size. Avoid sharing factors (for example, 20T + 30T) unless coverage math still clears your travel. |
| 88 | +
|
| 89 | +## Calibration and bring-up |
| 90 | +1) Mechanically zero the mechanism; log both absolute readings (raw rotations in [0, 1)). |
| 91 | +2) Set offsets so that the zero pose reads the desired mechanism zero after wrap (set them in the config). |
| 92 | +3) Set motion range to the actual usable travel (include soft stops if you have them). |
| 93 | +4) Compute ratios via one of the gearing helpers; confirm `coverageSatisfiesRange()` if using prime-aware inputs. |
| 94 | +5) Choose an initial `matchTolerance` based on backlash mapped to encoder 2: `backlash_mech_rot * ratio2`. If you preload to one side, you can shrink this. |
| 95 | +6) Jog through the full travel; watch `getLastStatus()` and `getLastErrorRotations()`; ensure no `AMBIGUOUS` near boundary regions. |
| 96 | +7) Power-cycle in multiple poses and confirm reconstructed angles stay consistent. |
| 97 | +
|
| 98 | +## Seeding a motor controller with EasyCRT |
| 99 | +Use the solved mechanism angle to initialize your motor controller's encoder so it knows its absolute position after boot: |
| 100 | +```java |
| 101 | +var easyCrtConfig = ...; // build EasyCRTConfig as shown above |
| 102 | +var easyCrtSolver = new EasyCRT(easyCrtConfig); |
| 103 | +//If the mechanism is configured using YAMS, use setEncoderPosition |
| 104 | +easyCrtSolver.getAngleOptional().ifPresent(mechAngle -> { |
| 105 | + motor.setEncoderPosition(mechAngle); |
| 106 | +}); |
| 107 | +``` |
| 108 | +Call this once at startup (after sensors are ready) before running closed-loop control. |
| 109 | + |
| 110 | +## Troubleshooting: Common pitfalls and how to avoid them |
| 111 | +- Wrong ratio sign: mechanism turns positive but angle decreases -> set encoder inversion |
| 112 | +- Missing a gear stage: forgetting chain or belt stages yields too-small ratios; candidates overlap -> `AMBIGUOUS`. |
| 113 | +- Non-coprime pinions: shared factors shrink coverage; `coverageSatisfiesRange()` becomes false. Pick coprime counts or widen the coverage margin. |
| 114 | +- Zeroing at a wrap edge: offsets that place the parked pose near 0 or 1 can cause wrap flips from noise. If your zero offset lands within a few hundredths of 0 or 1 rotations, physically re-phase the encoder (pull the pinion or magnet, rotate a tooth or two, and remesh) so your zero sits closer to mid-window, then reset offsets in the config. |
| 115 | +- Tolerance too tight: backlash or magnet noise bigger than tolerance -> `NO_SOLUTION`. Increase tolerance or preload the mechanism to one side of backlash. |
| 116 | +- Tolerance too loose: two candidates inside tolerance -> `AMBIGUOUS`. Reduce tolerance or widen range margins so only one candidate survives. |
| 117 | +- Double inversion or offset: applying inversion or offset both on device and in config produces mirrored or shifted angles. Keep device at defaults; do inversion and offsets in the config. The CTRE CANCoder configurator resets device settings, preventing stale on-device values. |
| 118 | +- Skipping power-cycle tests: CRT must reconstruct after reboot; always validate cold-start behavior. |
| 119 | + |
| 120 | +## Example configuration (encoders driven through the mechanism reduction) |
| 121 | +```java |
| 122 | +// Suppose: mechanism : drive gear = 12:1, drive gear = 50T, encoders use 19T and 23T pinions. |
| 123 | +var easyCrt = |
| 124 | + new EasyCRTConfig(enc1Supplier, enc2Supplier) |
| 125 | + .withCommonDriveGear( |
| 126 | + /* commonRatio (mech:drive) */ 12.0, |
| 127 | + /* driveGearTeeth */ 50, |
| 128 | + /* encoder1Pinion */ 19, |
| 129 | + /* encoder2Pinion */ 23) |
| 130 | + .withAbsoluteEncoderOffsets(Rotations.of(0.0), Rotations.of(0.0)) // set after mechanical zero |
| 131 | + .withMechanismRange(Rotations.of(-1.0), Rotations.of(2.0)) // -360 deg to +720 deg |
| 132 | + .withMatchTolerance(Rotations.of(0.06)) // ~1.08 deg at encoder2 for the example ratio |
| 133 | + .withAbsoluteEncoderInversions(false, false) |
| 134 | + .withCrtGearRecommendationConstraints( |
| 135 | + /* coverageMargin */ 1.2, |
| 136 | + /* minTeeth */ 15, |
| 137 | + /* maxTeeth */ 45, |
| 138 | + /* maxIterations */ 30); |
| 139 | + |
| 140 | +// you can inspect: |
| 141 | +easyCrt.getUniqueCoverage(); // Optional<Angle> coverage from prime counts and common scale |
| 142 | +easyCrt.coverageSatisfiesRange(); // Does coverage exceed maxMechanismAngle? |
| 143 | +easyCrt.getRecommendedCrtGearPair(); // Suggested pair within constraints |
| 144 | + |
| 145 | +// Create the solver: |
| 146 | +var easyCrtSolver = new EasyCRT(easyCrt); |
| 147 | +``` |
| 148 | + |
| 149 | +## More gearing examples |
| 150 | +- **Encoders using explicit stage definitions** (no CRT inputs set by gearing; seed the recommender manually): |
| 151 | + ```java |
| 152 | + var easyCrt = |
| 153 | + new EasyCRTConfig(enc1, enc2) |
| 154 | + .withAbsoluteEncoder1GearingStages(14, 50, 16, 60) // compound to encoder 1 |
| 155 | + .withAbsoluteEncoder2GearingStages(12, 36, 18, 54) // compound to encoder 2 |
| 156 | + .withMechanismRange(Rotations.of(0.0), Rotations.of(2.0)) |
| 157 | + .withAbsoluteEncoderOffsets(Rotations.of(0.01), Rotations.of(0.0)) // align to hard stop |
| 158 | + .withAbsoluteEncoderInversions(false, true) |
| 159 | + // Seed recommender since common drive inputs were not provided above |
| 160 | + .withCrtGearRecommendationInputs( |
| 161 | + /* stage1GearTeeth (shared driver) */ 48, |
| 162 | + /* stage2Ratio (mech:drive) */ 10.0) |
| 163 | + .withCrtGearRecommendationConstraints( |
| 164 | + /* coverageMargin */ 1.15, |
| 165 | + /* minTeeth */ 15, |
| 166 | + /* maxTeeth */ 55, |
| 167 | + /* maxIterations */ 30); |
| 168 | + |
| 169 | + easyCrt.getRecommendedCrtGearPair(); |
| 170 | + ``` |
| 171 | + |
| 172 | +- **Encoders using chain helpers** (no CRT inputs; coverage helpers empty): |
| 173 | + ```java |
| 174 | + var easyCrt = |
| 175 | + new EasyCRTConfig(enc1, enc2) |
| 176 | + .withAbsoluteEncoder1Gearing(72, 24) // 72 drives 24 |
| 177 | + .withAbsoluteEncoder2Gearing(50, 20, 40) // 50 drives 20, 20 drives 40 |
| 178 | + .withMechanismRange(Rotations.of(0.0), Rotations.of(1.2)) |
| 179 | + .withAbsoluteEncoderInversions(false, false); |
| 180 | + ``` |
| 181 | + |
| 182 | +## Validation checklist |
| 183 | +- Observe `getLastStatus()` while commanding moves; it should report `OK` across travel. |
| 184 | +- Ensure `getLastErrorRotations()` stays below your tolerance with margin. |
| 185 | +- Jog across theoretical wrap boundaries; verify no sudden +/-1 rotation jumps in mechanism space. |
| 186 | +- Run multiple power cycles in varied poses and confirm the resolved angle stays within your tolerance budget. |
0 commit comments