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