001package jmri.jmrix.bachrus.speedmatcher.basic;
002
003import jmri.DccThrottle;
004import jmri.jmrix.bachrus.Speed;
005
006/**
007 * This is a simple speed matcher which will speed match a locomotive to a given
008 * start and top speed using ESU's complex speed table. Speed steps 1, 10, 19,
009 * and 28 will be set according to values interpolated linearly between the
010 * given start and to speeds. Values for the remaining CVs will interpolated
011 * between these 4 CVs. This is done to reduce the time the speed match takes
012 * and to increase likelihood of success.
013 *
014 * @author Todd Wegter Copyright (C) 2024
015 */
016public class BasicESUTableSpeedMatcher extends BasicSpeedMatcher {
017
018    //<editor-fold defaultstate="collapsed" desc="Constants">
019    private final int INITIAL_VSTART = 1;
020    private final int INITIAL_VHIGH = 255;
021    private final int INITIAL_TRIM = 128;
022    private final int STEP28_VALUE = 255;
023    private final int STEP1_VALUE = 1;
024
025    private final int VHIGH_MAX = 255;
026    private final int VHIGH_MIN = INITIAL_VSTART + 1;
027    private final int STEP19_MIN = 19;
028    private final int STEP10_MIN = 10;
029    private final int VSTART_MIN = 1;
030    //</editor-fold>
031
032    //<editor-fold defaultstate="collapsed" desc="Enums">
033    protected enum SpeedMatcherState {
034        IDLE,
035        WAIT_FOR_THROTTLE,
036        INIT_THROTTLE,
037        INIT_ACCEL,
038        INIT_DECEL,
039        INIT_VSTART,
040        INIT_VHIGH,
041        INIT_SPEED_TABLE,
042        INIT_FORWARD_TRIM,
043        INIT_REVERSE_TRIM,
044        POST_INIT,
045        FORWARD_WARM_UP,
046        FORWARD_SPEED_MATCH_VHIGH,
047        FORWARD_SPEED_MATCH_VSTART,
048        FORWARD_SPEED_MATCH_STEP19,
049        RE_INIT_SPEED_TABLE_MIDDLE_THIRD,
050        FORWARD_SPEED_MATCH_STEP10,
051        INTERPOLATE_SPEED_TABLE,
052        POST_INTERPOLATE,
053        REVERSE_WARM_UP,
054        REVERSE_SPEED_MATCH_TRIM,
055        COMPLETE,
056        USER_STOPPED,
057        CLEAN_UP,
058    }
059    //</editor-fold>
060
061    //<editor-fold defaultstate="collapsed" desc="Instance Variables">
062    private SpeedTableStep initSpeedTableStep;
063    private int initSpeedTableStepValue;
064
065    private SpeedTableStep interpolationSpeedTableStep;
066
067    private int speedMatchCVValue = INITIAL_VHIGH;
068    private int lastSpeedMatchCVValue = INITIAL_VHIGH;
069
070    private int reverseTrimValue = INITIAL_TRIM;
071    private int lastReverseTrimValue = INITIAL_TRIM;
072
073    private final float targetVHighSpeedKPH;
074    private final float targetStep19SpeedKPH;
075    private final float targetStep10SpeedKPH;
076    private final float targetVStartSpeedKPH;
077
078    private int vHigh = INITIAL_VHIGH;
079    private int lastVHigh = INITIAL_VHIGH;
080    private int step19CVValue;
081    private int step10CVValue;
082    private int vStart;
083    private int lastVStart = INITIAL_VSTART;
084    private int vStartMax;
085
086    private SpeedMatcherState speedMatcherState = SpeedMatcherState.IDLE;
087    //</editor-fold>
088
089    /**
090     * Constructs the BasicESUTableSpeedMatcher from a BasicSpeedMatcherConfig
091     * 
092     * @param config BasicSpeedMatcherConfig
093     */
094    public BasicESUTableSpeedMatcher(BasicSpeedMatcherConfig config) {
095        super(config);
096
097        targetVHighSpeedKPH = targetTopSpeedKPH;
098        targetStep19SpeedKPH = getSpeedForSpeedStep(SpeedTableStep.STEP19, targetStartSpeedKPH, targetTopSpeedKPH);
099        targetStep10SpeedKPH = getSpeedForSpeedStep(SpeedTableStep.STEP10, targetStartSpeedKPH, targetTopSpeedKPH);
100        targetVStartSpeedKPH = targetStartSpeedKPH;
101    }
102
103    //<editor-fold defaultstate="collapsed" desc="SpeedMatcher Overrides">
104    /**
105     * Starts the speed matching process
106     *
107     * @return true if speed matching started successfully, false otherwise
108     */
109    @Override
110    public boolean startSpeedMatcher() {
111        if (!validate()) {
112            return false;
113        }
114
115        //reset instance variables
116        speedMatchCVValue = INITIAL_VHIGH;
117        lastSpeedMatchCVValue = INITIAL_VHIGH;
118        reverseTrimValue = INITIAL_TRIM;
119        lastReverseTrimValue = INITIAL_TRIM;
120
121        speedMatcherState = SpeedMatcherState.WAIT_FOR_THROTTLE;
122
123        if (!initializeAndStartSpeedMatcher(e -> speedMatchTimeout())) {
124            cleanUpSpeedMatcher();
125            return false;
126        }
127
128        startStopButton.setText(Bundle.getMessage("SpeedMatchStopBtn"));
129
130        return true;
131    }
132
133    /**
134     * Stops the speed matching process
135     */
136    @Override
137    public void stopSpeedMatcher() {
138        if (!isSpeedMatcherIdle()) {
139            logger.info("Speed matching manually stopped");
140            userStop();
141        } else {
142            cleanUpSpeedMatcher();
143        }
144    }
145
146    /**
147     * Indicates if the speed matcher is idle (not currently speed matching)
148     *
149     * @return true if idle, false otherwise
150     */
151    @Override
152    public boolean isSpeedMatcherIdle() {
153        return speedMatcherState == SpeedMatcherState.IDLE;
154    }
155
156    /**
157     * Cleans up the speed matcher when speed matching is stopped or is finished
158     */
159    @Override
160    protected void cleanUpSpeedMatcher() {
161        speedMatcherState = SpeedMatcherState.IDLE;
162        super.cleanUpSpeedMatcher();
163    }
164    //</editor-fold>
165
166    //<editor-fold defaultstate="collapsed" desc="Speed Matcher State">
167    /**
168     * Main speed matching timeout handler. This is the state machine that
169     * effectively does the speed matching process.
170     */
171    private synchronized void speedMatchTimeout() {
172        switch (speedMatcherState) {
173            case WAIT_FOR_THROTTLE:
174                cleanUpSpeedMatcher();
175                logger.error("Timeout waiting for throttle");
176                statusLabel.setText(Bundle.getMessage("StatusTimeout"));
177                break;
178
179            case INIT_THROTTLE:
180                //set throttle to 0 for init
181                setThrottle(true, 0);
182                initNextSpeedMatcherState(SpeedMatcherState.INIT_ACCEL);
183                break;
184
185            case INIT_ACCEL:
186                //set acceleration momentum to 0 (CV 3)
187                if (programmerState == ProgrammerState.IDLE) {
188                    writeMomentumAccel(INITIAL_MOMENTUM);
189                    initNextSpeedMatcherState(SpeedMatcherState.INIT_DECEL);
190                }
191                break;
192
193            case INIT_DECEL:
194                //set deceleration mementum to 0 (CV 4)
195                if (programmerState == ProgrammerState.IDLE) {
196                    writeMomentumDecel(INITIAL_MOMENTUM);
197                    initNextSpeedMatcherState(SpeedMatcherState.INIT_VSTART);
198                }
199                break;
200
201            case INIT_VSTART:
202                //set vStart to 0 (CV 2)
203                if (programmerState == ProgrammerState.IDLE) {
204                    writeVStart(INITIAL_VSTART);
205                    initNextSpeedMatcherState(SpeedMatcherState.INIT_VHIGH);
206                }
207                break;
208
209            case INIT_VHIGH:
210                //set vHigh to 255 (CV 5)
211                if (programmerState == ProgrammerState.IDLE) {
212                    writeVHigh(INITIAL_VHIGH);
213                    initNextSpeedMatcherState(SpeedMatcherState.INIT_SPEED_TABLE);
214                }
215                break;
216
217            case INIT_SPEED_TABLE:
218                //initialize speed table steps
219                //don't need to set steps 1 or 28 since they are locked to 1 and
220                //255, respectively on ESU decoders
221                if (programmerState == ProgrammerState.IDLE) {
222                    if (stepDuration == 0) {
223                        initSpeedTableStepValue = INITIAL_VSTART;
224                        initSpeedTableStep = SpeedTableStep.STEP2;
225                        stepDuration = 1;
226                    }
227
228                    if (initSpeedTableStep.getSpeedStep() > SpeedTableStep.STEP18.getSpeedStep()) {
229                        initSpeedTableStepValue = INITIAL_VHIGH;
230                    }
231
232                    writeSpeedTableStep(initSpeedTableStep, initSpeedTableStepValue);
233
234                    initSpeedTableStep = initSpeedTableStep.getNext();
235                    if (initSpeedTableStep == SpeedTableStep.STEP28) {
236                        initNextSpeedMatcherState(SpeedMatcherState.INIT_FORWARD_TRIM);
237                    }
238                }
239                break;
240
241            case INIT_FORWARD_TRIM:
242                //set forward trim to 128 (CV 66)
243                if (programmerState == ProgrammerState.IDLE) {
244                    writeForwardTrim(INITIAL_TRIM);
245                    initNextSpeedMatcherState(SpeedMatcherState.INIT_REVERSE_TRIM);
246                }
247                break;
248
249            case INIT_REVERSE_TRIM:
250                //set reverse trim to 128 (CV 95)
251                if (programmerState == ProgrammerState.IDLE) {
252                    writeReverseTrim(INITIAL_TRIM);
253                    initNextSpeedMatcherState(SpeedMatcherState.POST_INIT);
254                }
255                break;
256
257            case POST_INIT: {
258                statusLabel.setText(Bundle.getMessage("StatRestoreThrottle"));
259
260                //un-brick Digitrax decoders
261                setThrottle(false, 0);
262                setThrottle(true, 0);
263
264                SpeedMatcherState nextState;
265                if (warmUpForwardSeconds > 0) {
266                    nextState = SpeedMatcherState.FORWARD_WARM_UP;
267                } else {
268                    nextState = SpeedMatcherState.FORWARD_SPEED_MATCH_VHIGH;
269                }
270                initNextSpeedMatcherState(nextState);
271                break;
272            }
273
274            case FORWARD_WARM_UP:
275                //Run 4 minutes at high speed forward
276                statusLabel.setText(Bundle.getMessage("StatForwardWarmUp", warmUpForwardSeconds - stepDuration));
277
278                if (stepDuration >= warmUpForwardSeconds) {
279                    initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_VHIGH);
280                } else {
281                    if (stepDuration == 0) {
282                        setSpeedMatchStateTimerDuration(5000);
283                        setThrottle(true, 28);
284                    }
285                    stepDuration += 5;
286                }
287                break;
288
289            case FORWARD_SPEED_MATCH_VHIGH:
290                //Use PID Controller to adjust vHigh (Speed Step 28) to the max speed
291                if (programmerState == ProgrammerState.IDLE) {
292                    if (stepDuration == 0) {
293                        statusLabel.setText(Bundle.getMessage("StatSettingSpeed", SpeedMatcherCV.VHIGH.getName()));
294                        logger.info("Setting CV {} to {} KPH ({} MPH)", SpeedMatcherCV.VHIGH.getName(), String.valueOf(targetVHighSpeedKPH), String.valueOf(Speed.kphToMph(targetVHighSpeedKPH)));
295                        setThrottle(true, 28);
296                        setSpeedMatchStateTimerDuration(8000);
297                        stepDuration = 1;
298                    } else {
299                        setSpeedMatchError(targetVHighSpeedKPH);
300
301                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
302                            initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_VSTART);
303                        } else {
304                            vHigh = getNextSpeedMatchValue(lastVHigh, VHIGH_MAX, VHIGH_MIN);
305
306                            if (((lastVHigh == VHIGH_MAX) || (lastVHigh == VHIGH_MIN)) && (vHigh == lastVHigh)) {
307                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", SpeedMatcherCV.VHIGH.getName()));
308                                logger.info("Unable to achieve desired speed for CV {}", SpeedMatcherCV.VHIGH.getName());
309                                abort();
310                                break;
311                            }
312
313                            lastVHigh = vHigh;
314                            writeVHigh(vHigh);
315                        }
316                    }
317                }
318                break;
319                
320            case FORWARD_SPEED_MATCH_VSTART:
321                //Use PID Controller to adjust vStart (Speed Step 1) to the min speed
322                if (programmerState == ProgrammerState.IDLE) {
323                    if (stepDuration == 0) {
324                        vStartMax = vHigh - 1;
325                        statusLabel.setText(Bundle.getMessage("StatSettingSpeed", SpeedMatcherCV.VSTART.getName()));
326                        logger.info("Setting CV {} to {} KPH ({} MPH)", SpeedMatcherCV.VSTART.getName(), String.valueOf(targetVStartSpeedKPH), String.valueOf(Speed.kphToMph(targetVStartSpeedKPH)));
327                        setThrottle(true, 1);
328                        setSpeedMatchStateTimerDuration(8000);
329                        stepDuration = 1;
330                    } else {
331                        setSpeedMatchError(targetVStartSpeedKPH);
332
333                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
334                            initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_STEP19);
335                        } else {
336                            vStart = getNextSpeedMatchValue(lastVStart, vStartMax, VSTART_MIN);
337
338                            if (((lastVStart == vStartMax) || (lastVStart == VSTART_MIN)) && (vStart == lastVStart)) {
339                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", SpeedMatcherCV.VSTART.getName()));
340                                logger.info("Unable to achieve desired speed for CV {}", SpeedMatcherCV.VSTART.getName());
341                                abort();
342                                break;
343                            }
344
345                            lastVStart = vStart;
346                            writeVStart(vStart);
347                        }
348                    }
349                }
350                break;
351
352            case FORWARD_SPEED_MATCH_STEP19:
353                //Use PID Controller to adjust Speed Step 19 to the interpolated speed
354                if (programmerState == ProgrammerState.IDLE) {
355                    if (stepDuration == 0) {
356                        lastSpeedMatchCVValue = STEP28_VALUE;
357                    }
358                    speedMatchSpeedTableStep(SpeedTableStep.STEP19, targetStep19SpeedKPH, INITIAL_VHIGH, STEP19_MIN, SpeedMatcherState.RE_INIT_SPEED_TABLE_MIDDLE_THIRD);
359                    step19CVValue = speedMatchCVValue;
360                }
361                break;
362
363            case RE_INIT_SPEED_TABLE_MIDDLE_THIRD:
364                //Re-initialize Speed Steps 10-18 based off value for Step 19
365                if (programmerState == ProgrammerState.IDLE) {
366                    if (stepDuration == 0) {
367                        initSpeedTableStep = SpeedTableStep.STEP18;
368                        stepDuration = 1;
369                    }
370
371                    writeSpeedTableStep(initSpeedTableStep, step19CVValue);
372
373                    if (initSpeedTableStep == SpeedTableStep.STEP10) {
374                        initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_STEP10);
375                    } else {
376                        initSpeedTableStep = initSpeedTableStep.getPrevious();
377                    }
378
379                }
380                break;
381
382            case FORWARD_SPEED_MATCH_STEP10:
383                //Use PID Controller to adjust Speed Step 10 to the interpolated speed
384                if (programmerState == ProgrammerState.IDLE) {
385                    if (stepDuration == 0) {
386                        lastSpeedMatchCVValue = step19CVValue;
387                    }
388                    speedMatchSpeedTableStep(SpeedTableStep.STEP10, targetStep10SpeedKPH, step19CVValue - 9, STEP10_MIN, SpeedMatcherState.INTERPOLATE_SPEED_TABLE);
389                    step10CVValue = speedMatchCVValue;
390                }
391                break;
392
393            case INTERPOLATE_SPEED_TABLE: {
394                //Interpolate the values of the intermediate speed steps
395                if (programmerState == ProgrammerState.IDLE) {
396                    if (stepDuration == 0) {
397                        setThrottle(true, 0);
398                        interpolationSpeedTableStep = SpeedTableStep.STEP27;
399                        stepDuration = 1;
400                    }
401
402                    int interpolatedSpeedStepCVValue = getInterpolatedSpeedTableCVValue(interpolationSpeedTableStep);
403                    writeSpeedTableStep(interpolationSpeedTableStep, interpolatedSpeedStepCVValue);
404
405                    do {
406                        interpolationSpeedTableStep = interpolationSpeedTableStep.getPrevious();
407                    } while (interpolationSpeedTableStep == SpeedTableStep.STEP19 || interpolationSpeedTableStep == SpeedTableStep.STEP10);
408
409                    if (interpolationSpeedTableStep == SpeedTableStep.STEP1) {
410                        initNextSpeedMatcherState(SpeedMatcherState.POST_INTERPOLATE);
411                    }
412
413                }
414                break;
415            }
416
417            case POST_INTERPOLATE: {
418                statusLabel.setText(Bundle.getMessage("StatRestoreThrottle"));
419
420                //un-brick Digitrax decoders
421                setThrottle(false, 0);
422                setThrottle(true, 0);
423
424                SpeedMatcherState nextState;
425                if (trimReverseSpeed) {
426                    if (warmUpReverseSeconds > 0) {
427                        nextState = SpeedMatcherState.REVERSE_WARM_UP;
428                    } else {
429                        nextState = SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM;
430                    }
431                } else {
432                    nextState = SpeedMatcherState.COMPLETE;
433                }
434                initNextSpeedMatcherState(nextState);
435                break;
436            }
437
438            case REVERSE_WARM_UP:
439                //Run specified reverse warm up time at high speed in reverse
440                statusLabel.setText(Bundle.getMessage("StatReverseWarmUp", warmUpReverseSeconds - stepDuration));
441
442                if (stepDuration >= warmUpReverseSeconds) {
443                    initNextSpeedMatcherState(SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM);
444                } else {
445                    if (stepDuration == 0) {
446                        setSpeedMatchStateTimerDuration(5000);
447                        setThrottle(false, 28);
448                    }
449                    stepDuration += 5;
450                }
451
452                break;
453
454            case REVERSE_SPEED_MATCH_TRIM:
455                //Use PID controller logic to adjust reverse trim until high speed reverse speed matches forward
456                if (programmerState == ProgrammerState.IDLE) {
457                    if (stepDuration == 0) {
458                        statusLabel.setText(Bundle.getMessage("StatSettingReverseTrim"));
459                        setThrottle(false, 28);
460                        setSpeedMatchStateTimerDuration(8000);
461                        stepDuration = 1;
462                    } else {
463                        setSpeedMatchError(targetTopSpeedKPH);
464
465                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
466                            initNextSpeedMatcherState(SpeedMatcherState.COMPLETE);
467                        } else {
468                            reverseTrimValue = getNextSpeedMatchValue(lastReverseTrimValue, REVERSE_TRIM_MAX, REVERSE_TRIM_MIN);
469
470                            if (((lastReverseTrimValue == REVERSE_TRIM_MAX) || (lastReverseTrimValue == REVERSE_TRIM_MIN)) && (reverseTrimValue == lastReverseTrimValue)) {
471                                statusLabel.setText(Bundle.getMessage("StatSetReverseTrimFail"));
472                                logger.info("Unable to trim reverse to match forward");
473                                abort();
474                                break;
475                            }
476
477                            lastReverseTrimValue = reverseTrimValue;
478                            writeReverseTrim(reverseTrimValue);
479                        }
480                    }
481                }
482                break;
483
484            case COMPLETE:
485                if (programmerState == ProgrammerState.IDLE) {
486                    statusLabel.setText(Bundle.getMessage("StatSpeedMatchComplete"));
487                    setThrottle(true, 0);
488                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
489                }
490                break;
491
492            case USER_STOPPED:
493                if (programmerState == ProgrammerState.IDLE) {
494                    statusLabel.setText(Bundle.getMessage("StatUserStoppedSpeedMatch"));
495                    setThrottle(true, 0);
496                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
497                }
498                break;
499
500            case CLEAN_UP:
501                //wrap it up
502                if (programmerState == ProgrammerState.IDLE) {
503                    cleanUpSpeedMatcher();
504                }
505                break;
506
507            default:
508                cleanUpSpeedMatcher();
509                logger.error("Unexpected speed match timeout");
510                break;
511        }
512
513        if (speedMatcherState != SpeedMatcherState.IDLE) {
514            startSpeedMatchStateTimer();
515        }
516    }
517    //</editor-fold>
518
519    //<editor-fold defaultstate="collapsed" desc="ThrottleListener Overrides">
520    /**
521     * Called when a throttle is found
522     *
523     * @param t the requested DccThrottle
524     */
525    @Override
526    public void notifyThrottleFound(DccThrottle t) {
527        super.notifyThrottleFound(t);
528
529        if (speedMatcherState == SpeedMatcherState.WAIT_FOR_THROTTLE) {
530            logger.info("Starting speed matching");
531            // using speed matching timer to trigger each phase of speed matching            
532            initNextSpeedMatcherState(SpeedMatcherState.INIT_THROTTLE);
533            startSpeedMatchStateTimer();
534        } else {
535            cleanUpSpeedMatcher();
536        }
537    }
538    //</editor-fold>
539
540    //<editor-fold defaultstate="collapsed" desc="Helper Functions">
541    /**
542     * Gets the interpolated CV value for the given speed step in the speed
543     * table
544     *
545     * @param speedStep the SpeedTableStep to get the speed for
546     * @return the target speed for the given speed step in KPH
547     */
548    private int getInterpolatedSpeedTableCVValue(SpeedTableStep speedStep) {
549        SpeedTableStep maxStep;
550        SpeedTableStep minStep;
551        int maxStepCVValue;
552        int minStepCVValue;
553
554        if (speedStep.getSpeedStep() >= SpeedTableStep.STEP19.getSpeedStep()) {
555            maxStep = SpeedTableStep.STEP28;
556            minStep = SpeedTableStep.STEP19;
557            maxStepCVValue = STEP28_VALUE;
558            minStepCVValue = step19CVValue;
559        } else if (speedStep.getSpeedStep() >= SpeedTableStep.STEP10.getSpeedStep()) {
560            maxStep = SpeedTableStep.STEP19;
561            minStep = SpeedTableStep.STEP10;
562            maxStepCVValue = step19CVValue;
563            minStepCVValue = step10CVValue;
564        } else {
565            maxStep = SpeedTableStep.STEP10;
566            minStep = SpeedTableStep.STEP1;
567            maxStepCVValue = step10CVValue;
568            minStepCVValue = STEP1_VALUE;
569        }
570
571        return Math.round(minStepCVValue + ((((float) (maxStepCVValue - minStepCVValue)) / (maxStep.getSpeedStep() - minStep.getSpeedStep())) * (speedStep.getSpeedStep() - minStep.getSpeedStep())));
572    }
573
574    /**
575     * Helper function for speed matching a given speed step
576     *
577     * @param speedStep      the SpeedTableStep to speed match
578     * @param targetSpeedKPH the target speed in KPH
579     * @param maxCVValue     the maximum allowable value for the CV
580     * @param minCVValue     the minimum allowable value for the CV
581     * @param nextState      the SpeedMatcherState to advance to if speed
582     *                       matching is complete
583     */
584    private void speedMatchSpeedTableStep(SpeedTableStep speedStep, float targetSpeedKPH, int maxCVValue, int minCVValue, SpeedMatcherState nextState) {
585        if (stepDuration == 0) {
586            statusLabel.setText(Bundle.getMessage("StatSettingSpeed", speedStep.getCV() + " (Speed Step " + String.valueOf(speedStep.getSpeedStep()) + ")"));
587            logger.info("Setting CV {} (speed step {}) to {} KPH ({} MPH)", speedStep.getCV(), speedStep.getSpeedStep(), String.valueOf(targetSpeedKPH), String.valueOf(Speed.kphToMph(targetSpeedKPH)));
588            setThrottle(true, speedStep.getSpeedStep());
589            setSpeedMatchStateTimerDuration(8000);
590            stepDuration = 1;
591        } else {
592            setSpeedMatchError(targetSpeedKPH);
593
594            if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
595                initNextSpeedMatcherState(nextState);
596            } else {
597                speedMatchCVValue = getNextSpeedMatchValue(lastSpeedMatchCVValue, maxCVValue, minCVValue);
598
599                if (((speedMatchCVValue == maxCVValue) || (speedMatchCVValue == minCVValue)) && (speedMatchCVValue == lastSpeedMatchCVValue)) {
600                    statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", speedStep.getCV() + " (Speed Step " + String.valueOf(speedStep.getSpeedStep()) + ")"));
601                    logger.info("Unable to achieve desired speed for CV {} (Speed Step {})", speedStep.getCV(), String.valueOf(speedStep.getSpeedStep()));
602                    abort();
603                    return;
604                }
605
606                lastSpeedMatchCVValue = speedMatchCVValue;
607                writeSpeedTableStep(speedStep, speedMatchCVValue);
608            }
609        }
610    }
611
612    /**
613     * Aborts the speed matching process programmatically
614     */
615    private void abort() {
616        initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
617    }
618
619    /**
620     * Stops the speed matching process due to user input
621     */
622    private void userStop() {
623        initNextSpeedMatcherState(SpeedMatcherState.USER_STOPPED);
624    }
625
626    /**
627     * Sets up the speed match state by clearing the speed match error, clearing
628     * the step duration, setting the timer duration, and setting the next state
629     *
630     * @param nextState - next SpeedMatcherState to set
631     */
632    protected void initNextSpeedMatcherState(SpeedMatcherState nextState) {
633        resetSpeedMatchError();
634        stepDuration = 0;
635        speedMatcherState = nextState;
636        setSpeedMatchStateTimerDuration(1800);
637    }
638    //</editor-fold>
639
640    //debugging logger
641    private final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(BasicESUTableSpeedMatcher.class);
642}