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