001package jmri.jmrix.bachrus.speedmatcher.speedStepScale; 002 003import jmri.DccThrottle; 004import jmri.jmrix.bachrus.Speed; 005 006/** 007 * This is a speed step scale speed matcher which will speed match a locomotive 008 * such that its speed in mph/kph will be equal to its speed step in 128 speed 009 * step mode. This uses the complex speed table, and the locomotive's speed will 010 * plateau at either its actual top speed or the set max speed, whichever is 011 * lower. 012 * 013 * @author Todd Wegter Copyright (C) 2024 014 */ 015public class SpeedStepScaleSpeedTableSpeedMatcher extends SpeedStepScaleSpeedMatcher { 016 017 //<editor-fold defaultstate="collapsed" desc="Constants"> 018 private final int INITIAL_STEP1 = 1; 019 private final int INITIAL_STEP28 = 255; 020 private final int INITIAL_TRIM = 128; 021 022 private final int TOP_SPEED_STEP_MAX = 255; 023 //</editor-fold> 024 025 //<editor-fold defaultstate="collapsed" desc="Enums"> 026 protected enum SpeedMatcherState { 027 IDLE, 028 WAIT_FOR_THROTTLE, 029 INIT_THROTTLE, 030 INIT_ACCEL, 031 INIT_DECEL, 032 INIT_SPEED_TABLE, 033 INIT_FORWARD_TRIM, 034 INIT_REVERSE_TRIM, 035 POST_INIT, 036 FORWARD_WARM_UP, 037 READ_MAX_SPEED, 038 FORWARD_SPEED_MATCH_STEP28, 039 SET_UPPER_SPEED_STEPS, 040 FORWARD_SPEED_MATCH, 041 POST_SPEED_MATCH, 042 REVERSE_WARM_UP, 043 REVERSE_SPEED_MATCH_TRIM, 044 COMPLETE, 045 USER_STOPPED, 046 CLEAN_UP, 047 } 048 //</editor-fold> 049 050 //<editor-fold defaultstate="collapsed" desc="Instance Variables"> 051 private SpeedTableStep initSpeedTableStep; 052 private int initSpeedTableStepValue; 053 private SpeedTableStep speedMatchSpeedTableStep; 054 private int lowestMaxSpeedStep; 055 056 private int step28CVValue = INITIAL_STEP28; 057 private float speedStepTargetSpeedKPH; 058 059 private int speedMatchCVValue = INITIAL_STEP28; 060 private int lastSpeedMatchCVValue = INITIAL_STEP28; 061 private int lastSpeedTableStepCVValue = INITIAL_STEP28; 062 063 private int reverseTrimValue = INITIAL_TRIM; 064 private int lastReverseTrimValue = INITIAL_TRIM; 065 private int reverseTrimSpeedStep; 066 private float reverseTrimSpeedKPH; 067 068 private SpeedMatcherState speedMatcherState = SpeedMatcherState.IDLE; 069 //</editor-fold> 070 071 /** 072 * Constructs the SpeedStepScaleSpeedTableSpeedMatcher from a 073 * SpeedStepScaleSpeedMatcherConfig 074 * 075 * @param config SpeedStepScaleSpeedMatcherConfig 076 */ 077 public SpeedStepScaleSpeedTableSpeedMatcher(SpeedStepScaleSpeedMatcherConfig config) { 078 super(config); 079 } 080 081 //<editor-fold defaultstate="collapsed" desc="SpeedMatcherOverrides"> 082 /** 083 * Starts the speed matching process 084 * 085 * @return true if speed matching started successfully, false otherwise 086 */ 087 @Override 088 public boolean startSpeedMatcher() { 089 if (!validate()) { 090 return false; 091 } 092 093 //reset instance variables 094 speedMatchCVValue = INITIAL_STEP28; 095 lastSpeedMatchCVValue = INITIAL_STEP28; 096 lastSpeedTableStepCVValue = INITIAL_STEP28; 097 reverseTrimValue = INITIAL_TRIM; 098 lastReverseTrimValue = INITIAL_TRIM; 099 measuredMaxSpeedKPH = 0; 100 speedMatchMaxSpeedKPH = 0; 101 102 speedMatcherState = SpeedMatcherState.WAIT_FOR_THROTTLE; 103 104 actualMaxSpeedField.setText(String.format("___")); 105 106 if (!initializeAndStartSpeedMatcher(e -> speedMatchTimeout())) { 107 cleanUpSpeedMatcher(); 108 return false; 109 } 110 111 startStopButton.setText(Bundle.getMessage("SpeedMatchStopBtn")); 112 113 return true; 114 } 115 116 /** 117 * Stops the speed matching process 118 */ 119 @Override 120 public void stopSpeedMatcher() { 121 if (!isSpeedMatcherIdle()) { 122 logger.info("Speed matching manually stopped"); 123 userStop(); 124 } else { 125 cleanUpSpeedMatcher(); 126 } 127 } 128 129 /** 130 * Indicates if the speed matcher is idle (not currently speed matching) 131 * 132 * @return true if idle, false otherwise 133 */ 134 @Override 135 public boolean isSpeedMatcherIdle() { 136 return speedMatcherState == SpeedMatcherState.IDLE; 137 } 138 139 /** 140 * Cleans up the speed matcher when speed matching is stopped or is finished 141 */ 142 @Override 143 protected void cleanUpSpeedMatcher() { 144 speedMatcherState = SpeedMatcherState.IDLE; 145 super.cleanUpSpeedMatcher(); 146 } 147 //</editor-fold> 148 149 //<editor-fold defaultstate="collapsed" desc="Speed Matcher State"> 150 /** 151 * Main speed matching timeout handler. This is the state machine that 152 * effectively does the speed matching process. 153 */ 154 private synchronized void speedMatchTimeout() { 155 switch (speedMatcherState) { 156 case WAIT_FOR_THROTTLE: 157 cleanUpSpeedMatcher(); 158 logger.error("Timeout waiting for throttle"); 159 statusLabel.setText(Bundle.getMessage("StatusTimeout")); 160 break; 161 162 case INIT_THROTTLE: 163 //set throttle to 0 for init 164 setThrottle(true, 0); 165 initNextSpeedMatcherState(SpeedMatcherState.INIT_ACCEL); 166 break; 167 168 case INIT_ACCEL: 169 //set acceleration momentum to 0 (CV 3) 170 if (programmerState == ProgrammerState.IDLE) { 171 writeMomentumAccel(INITIAL_MOMENTUM); 172 initNextSpeedMatcherState(SpeedMatcherState.INIT_DECEL); 173 } 174 break; 175 176 case INIT_DECEL: 177 //set deceleration mementum to 0 (CV 4) 178 if (programmerState == ProgrammerState.IDLE) { 179 writeMomentumDecel(INITIAL_MOMENTUM); 180 initNextSpeedMatcherState(SpeedMatcherState.INIT_SPEED_TABLE); 181 } 182 break; 183 184 case INIT_SPEED_TABLE: 185 //initialize every speed table step to 1 except speed table step 28 = 255 186 if (programmerState == ProgrammerState.IDLE) { 187 if (stepDuration == 0) { 188 initSpeedTableStepValue = INITIAL_STEP1; 189 initSpeedTableStep = SpeedTableStep.STEP1; 190 stepDuration = 1; 191 } 192 193 if (initSpeedTableStep == SpeedTableStep.STEP28) { 194 initSpeedTableStepValue = INITIAL_STEP28; 195 } 196 197 writeSpeedTableStep(initSpeedTableStep, initSpeedTableStepValue); 198 199 initSpeedTableStep = initSpeedTableStep.getNext(); 200 if (initSpeedTableStep == null) { 201 initNextSpeedMatcherState(SpeedMatcherState.INIT_FORWARD_TRIM); 202 } 203 } 204 break; 205 206 case INIT_FORWARD_TRIM: 207 //set forward trim to 128 (CV 66) 208 if (programmerState == ProgrammerState.IDLE) { 209 writeForwardTrim(INITIAL_TRIM); 210 initNextSpeedMatcherState(SpeedMatcherState.INIT_REVERSE_TRIM); 211 } 212 break; 213 214 case INIT_REVERSE_TRIM: 215 //set reverse trim to 128 (CV 95) 216 if (programmerState == ProgrammerState.IDLE) { 217 writeReverseTrim(INITIAL_TRIM); 218 initNextSpeedMatcherState(SpeedMatcherState.POST_INIT); 219 } 220 break; 221 222 case POST_INIT: { 223 statusLabel.setText(Bundle.getMessage("StatRestoreThrottle")); 224 225 //un-brick Digitrax decoders 226 setThrottle(false, 0); 227 setThrottle(true, 0); 228 229 SpeedMatcherState nextState; 230 if (warmUpForwardSeconds > 0) { 231 nextState = SpeedMatcherState.FORWARD_WARM_UP; 232 } else { 233 nextState = SpeedMatcherState.READ_MAX_SPEED; 234 } 235 initNextSpeedMatcherState(nextState); 236 break; 237 } 238 239 case FORWARD_WARM_UP: 240 //Run 4 minutes at high speed forward 241 statusLabel.setText(Bundle.getMessage("StatForwardWarmUp", warmUpForwardSeconds - stepDuration)); 242 243 if (stepDuration >= warmUpForwardSeconds) { 244 initNextSpeedMatcherState(SpeedMatcherState.READ_MAX_SPEED); 245 } else { 246 if (stepDuration == 0) { 247 setSpeedMatchStateTimerDuration(5000); 248 setThrottle(true, 28); 249 } 250 stepDuration += 5; 251 } 252 break; 253 254 case READ_MAX_SPEED: 255 //Run 10 second at high speed forward and record the speed 256 if (stepDuration == 0) { 257 statusLabel.setText("Recording locomotive's maximum speed"); 258 setSpeedMatchStateTimerDuration(10000); 259 setThrottle(true, 28); 260 stepDuration = 1; 261 } else { 262 measuredMaxSpeedKPH = currentSpeedKPH; 263 264 String statusMessage = String.format("Measured maximum speed = %.1f KPH (%.1f MPH)", measuredMaxSpeedKPH, Speed.kphToMph(measuredMaxSpeedKPH)); 265 logger.info(statusMessage); 266 267 if (measuredMaxSpeedKPH > targetMaxSpeedKPH) { 268 speedMatchMaxSpeedKPH = targetMaxSpeedKPH; 269 initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_STEP28); 270 } else { 271 //skip speed matching step 28 if max speed is less than target 272 speedMatchMaxSpeedKPH = measuredMaxSpeedKPH; 273 initNextSpeedMatcherState(SpeedMatcherState.SET_UPPER_SPEED_STEPS); 274 } 275 276 //set TOP_SPEED_STEP_MIN to the lowest speed step that will be set to the max speed 277 float speedMatchMaxSpeed = speedUnit == Speed.Unit.MPH ? Speed.kphToMph(speedMatchMaxSpeedKPH) : speedMatchMaxSpeedKPH; 278 lowestMaxSpeedStep = getLowestMaxSpeedStep(speedMatchMaxSpeed); 279 280 actualMaxSpeedField.setText(String.format("%.1f", speedMatchMaxSpeed)); 281 } 282 break; 283 284 case FORWARD_SPEED_MATCH_STEP28: 285 //Use PID Controller to adjust speed table step 28 to max speed 286 if (programmerState == ProgrammerState.IDLE) { 287 if (stepDuration == 0) { 288 speedMatchSpeedTableStep = SpeedTableStep.STEP28; 289 } 290 speedMatchSpeedStepInner(TOP_SPEED_STEP_MAX, lowestMaxSpeedStep, SpeedMatcherState.SET_UPPER_SPEED_STEPS, true); 291 step28CVValue = speedMatchCVValue; 292 } 293 break; 294 295 case SET_UPPER_SPEED_STEPS: 296 //Set Speed table steps 27 through lowestMaxSpeedStep to step28CVValue 297 if (programmerState == ProgrammerState.IDLE) { 298 if (stepDuration == 0) { 299 speedMatchSpeedTableStep = SpeedTableStep.STEP27; 300 stepDuration = 1; 301 } 302 303 writeSpeedTableStep(speedMatchSpeedTableStep, step28CVValue); 304 305 speedMatchSpeedTableStep = speedMatchSpeedTableStep.getPrevious(); 306 307 if (speedMatchSpeedTableStep.getSpeedStep() < lowestMaxSpeedStep) { 308 initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH); 309 } 310 } 311 break; 312 313 case FORWARD_SPEED_MATCH: 314 //Use PID Controller to adjust table speed steps lowestMaxSpeedStep through 1 to the appropriate speed 315 if (programmerState == ProgrammerState.IDLE) { 316 speedMatchSpeedStepInner(lastSpeedTableStepCVValue, speedMatchSpeedTableStep.getSpeedStep(), SpeedMatcherState.POST_SPEED_MATCH); 317 } 318 break; 319 320 case POST_SPEED_MATCH: { 321 statusLabel.setText(Bundle.getMessage("StatRestoreThrottle")); 322 323 //un-brick Digitrax decoders 324 setThrottle(false, 0); 325 setThrottle(true, 0); 326 327 SpeedMatcherState nextState; 328 if (trimReverseSpeed) { 329 if (warmUpReverseSeconds > 0) { 330 nextState = SpeedMatcherState.REVERSE_WARM_UP; 331 } else { 332 nextState = SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM; 333 } 334 } else { 335 nextState = SpeedMatcherState.COMPLETE; 336 } 337 initNextSpeedMatcherState(nextState); 338 break; 339 } 340 341 case REVERSE_WARM_UP: 342 //Run specified reverse warm up time at high speed in reverse 343 statusLabel.setText(Bundle.getMessage("StatReverseWarmUp", warmUpReverseSeconds - stepDuration)); 344 345 if (stepDuration >= warmUpReverseSeconds) { 346 initNextSpeedMatcherState(SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM); 347 } else { 348 if (stepDuration == 0) { 349 setSpeedMatchStateTimerDuration(5000); 350 setThrottle(false, 28); 351 } 352 stepDuration += 5; 353 } 354 355 break; 356 357 case REVERSE_SPEED_MATCH_TRIM: 358 //Use PID controller logic to adjust reverse trim until high speed reverse speed matches forward 359 if (programmerState == ProgrammerState.IDLE) { 360 if (stepDuration == 0) { 361 statusLabel.setText(Bundle.getMessage("StatSettingReverseTrim")); 362 reverseTrimSpeedStep = Math.max(lowestMaxSpeedStep - 2, 1); 363 reverseTrimSpeedKPH = getSpeedStepScaleSpeedInKPH(reverseTrimSpeedStep); 364 setThrottle(false, reverseTrimSpeedStep); 365 setSpeedMatchStateTimerDuration(8000); 366 stepDuration = 1; 367 } else { 368 setSpeedMatchError(reverseTrimSpeedKPH); 369 370 if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) { 371 initNextSpeedMatcherState(SpeedMatcherState.COMPLETE); 372 } else { 373 reverseTrimValue = getNextSpeedMatchValue(lastReverseTrimValue, REVERSE_TRIM_MAX, REVERSE_TRIM_MIN); 374 375 if (((lastReverseTrimValue == REVERSE_TRIM_MAX) || (lastReverseTrimValue == REVERSE_TRIM_MIN)) && (reverseTrimValue == lastReverseTrimValue)) { 376 statusLabel.setText(Bundle.getMessage("StatSetReverseTrimFail")); 377 logger.info("Unable to trim reverse to match forward"); 378 abort(); 379 break; 380 } 381 382 lastReverseTrimValue = reverseTrimValue; 383 writeReverseTrim(reverseTrimValue); 384 } 385 } 386 } 387 break; 388 389 case COMPLETE: 390 if (programmerState == ProgrammerState.IDLE) { 391 statusLabel.setText(Bundle.getMessage("StatSpeedMatchComplete")); 392 setThrottle(true, 0); 393 initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP); 394 } 395 break; 396 397 case USER_STOPPED: 398 if (programmerState == ProgrammerState.IDLE) { 399 statusLabel.setText(Bundle.getMessage("StatUserStoppedSpeedMatch")); 400 setThrottle(true, 0); 401 initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP); 402 } 403 break; 404 405 case CLEAN_UP: 406 //wrap it up 407 if (programmerState == ProgrammerState.IDLE) { 408 cleanUpSpeedMatcher(); 409 } 410 break; 411 412 default: 413 cleanUpSpeedMatcher(); 414 logger.error("Unexpected speed match timeout"); 415 break; 416 } 417 418 if (speedMatcherState != SpeedMatcherState.IDLE) { 419 startSpeedMatchStateTimer(); 420 } 421 } 422 //</editor-fold> 423 424 //<editor-fold defaultstate="collapsed" desc="ThrottleListener Overrides"> 425 /** 426 * Called when a throttle is found 427 * 428 * @param t the requested DccThrottle 429 */ 430 @Override 431 public void notifyThrottleFound(DccThrottle t) { 432 super.notifyThrottleFound(t); 433 434 if (speedMatcherState == SpeedMatcherState.WAIT_FOR_THROTTLE) { 435 logger.info("Starting speed matching"); 436 // using speed matching timer to trigger each phase of speed matching 437 initNextSpeedMatcherState(SpeedMatcherState.INIT_THROTTLE); 438 startSpeedMatchStateTimer(); 439 } else { 440 cleanUpSpeedMatcher(); 441 } 442 } 443 //</editor-fold> 444 445 //<editor-fold defaultstate="collapsed" desc="Helper Functions"> 446 /** 447 * Helper function for speed matching the current speedMatchSpeedTableStep 448 * 449 * @param maxCVValue the maximum allowable value for the CV 450 * @param minCVValue the minimum allowable value for the CV 451 * @param nextState the SpeedMatcherState to advance to if speed matching 452 * is complete 453 */ 454 private void speedMatchSpeedStepInner(int maxCVValue, int minCVValue, SpeedMatcherState nextState) { 455 speedMatchSpeedStepInner(maxCVValue, minCVValue, nextState, false); 456 } 457 458 /** 459 * Helper function for speed matching the current speedMatchSpeedTableStep 460 * 461 * @param maxCVValue the maximum allowable value for the CV 462 * @param minCVValue the minimum allowable value for the CV 463 * @param nextState the SpeedMatcherState to advance to if speed 464 * matching is complete 465 * @param forceNextState set to true to force speedMatcherState to the next 466 * state when speed matching the current 467 * speedMatchSpeedTableStep is complete 468 */ 469 private void speedMatchSpeedStepInner(int maxCVValue, int minCVValue, SpeedMatcherState nextState, boolean forceNextState) { 470 if (stepDuration == 0) { 471 speedStepTargetSpeedKPH = getSpeedStepScaleSpeedInKPH(speedMatchSpeedTableStep.getSpeedStep()); 472 473 statusLabel.setText(Bundle.getMessage("StatSettingSpeed", speedMatchSpeedTableStep.getCV() + " (Speed Step " + String.valueOf(speedMatchSpeedTableStep.getSpeedStep()) + ")")); 474 logger.info("Setting CV {} (speed step {}) to {} KPH ({} MPH)", speedMatchSpeedTableStep.getCV(), speedMatchSpeedTableStep.getSpeedStep(), String.valueOf(speedStepTargetSpeedKPH), String.valueOf(Speed.kphToMph(speedStepTargetSpeedKPH))); 475 476 setThrottle(true, speedMatchSpeedTableStep.getSpeedStep()); 477 478 writeSpeedTableStep(speedMatchSpeedTableStep, speedMatchCVValue); 479 480 setSpeedMatchStateTimerDuration(8000); 481 stepDuration = 1; 482 } else { 483 setSpeedMatchError(speedStepTargetSpeedKPH); 484 485 if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) { 486 lastSpeedTableStepCVValue = speedMatchCVValue; 487 488 if (forceNextState) { 489 initNextSpeedMatcherState(nextState); 490 return; 491 } 492 493 speedMatchSpeedTableStep = speedMatchSpeedTableStep.getPrevious(); 494 495 if (speedMatchSpeedTableStep != null) { 496 initNextSpeedMatcherState(speedMatcherState); 497 } else { 498 initNextSpeedMatcherState(nextState); 499 } 500 } else { 501 speedMatchCVValue = getNextSpeedMatchValue(lastSpeedMatchCVValue, maxCVValue, minCVValue); 502 503 if (((speedMatchCVValue == maxCVValue) || (speedMatchCVValue == minCVValue)) && (speedMatchCVValue == lastSpeedMatchCVValue)) { 504 statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", speedMatchSpeedTableStep.getCV() + " (Speed Step " + String.valueOf(speedMatchSpeedTableStep.getSpeedStep()) + ")")); 505 logger.info("Unable to achieve desired speed for CV {} (Speed Step {})", speedMatchSpeedTableStep.getCV(), String.valueOf(speedMatchSpeedTableStep.getSpeedStep())); 506 abort(); 507 return; 508 } 509 510 lastSpeedMatchCVValue = speedMatchCVValue; 511 writeSpeedTableStep(speedMatchSpeedTableStep, speedMatchCVValue); 512 } 513 } 514 } 515 516 /** 517 * Aborts the speed matching process programmatically 518 */ 519 private void abort() { 520 initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP); 521 } 522 523 /** 524 * Stops the speed matching process due to user input 525 */ 526 private void userStop() { 527 initNextSpeedMatcherState(SpeedMatcherState.USER_STOPPED); 528 } 529 530 /** 531 * Sets up the speed match state by clearing the speed match error, clearing 532 * the step duration, setting the timer duration, and setting the next state 533 * 534 * @param nextState next SpeedMatcherState to set 535 */ 536 protected void initNextSpeedMatcherState(SpeedMatcherState nextState) { 537 resetSpeedMatchError(); 538 stepDuration = 0; 539 speedMatcherState = nextState; 540 setSpeedMatchStateTimerDuration(1800); 541 } 542 //</editor-fold> 543 544 //debugging logger 545 private final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SpeedStepScaleSpeedTableSpeedMatcher.class); 546}