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);
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);
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(8000);
357                        stepDuration = 1;
358                    } else {
359                        setSpeedMatchError(targetVStartSpeedKPH);
360
361                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
362                            initNextSpeedMatcherState(SpeedMatcherState.RE_INIT_SPEED_TABLE);
363                        } else {
364                            vStart = getNextSpeedMatchValue(lastVStart, vStartMax, VSTART_MIN);
365
366                            if (((lastVStart == vStartMax) || (lastVStart == VSTART_MIN)) && (vStart == lastVStart)) {
367                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", SpeedMatcherCV.VSTART.getName()));
368                                logger.info("Unable to achieve desired speed for CV {}", SpeedMatcherCV.VSTART.getName());
369                                abort();
370                                break;
371                            }
372
373                            lastVStart = vStart;
374                            writeVStart(vStart);
375                        }
376                    }
377                }
378                break;
379
380            case RE_INIT_SPEED_TABLE:
381                //Set Speed table steps 27 through lowestMaxSpeedStep to TOP_SPEED_STEP_MAX 
382                //and the remaining steps through step 2 to 1
383                if (programmerState == ProgrammerState.IDLE) {
384                    if (stepDuration == 0) {
385                        initSpeedTableStepValue = TOP_SPEED_STEP_MAX;
386                        initSpeedTableStep = SpeedTableStep.STEP27;
387                        stepDuration = 1;
388                    }
389
390                    writeSpeedTableStep(initSpeedTableStep, initSpeedTableStepValue);
391                    
392                    if (initSpeedTableStep.getSpeedStep() == speedMatchMaxSpeedStep) {
393                        initSpeedTableStep = initSpeedTableStep.getPrevious();
394                        speedMatchSpeedTableStep = initSpeedTableStep;
395                        initSpeedTableStepValue = INITIAL_STEP2;
396                    }
397                    else {
398                        initSpeedTableStep = initSpeedTableStep.getPrevious();
399                    }
400
401                    if (initSpeedTableStep.getSpeedStep() < 2) {
402                        initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH);
403                    }
404                }
405                break;
406
407            case FORWARD_SPEED_MATCH:
408                //Use PID Controller to adjust table speed steps lowestMaxSpeedStep through 2 to the appropriate speed
409                if (programmerState == ProgrammerState.IDLE) {
410                    speedMatchSpeedStepInner(lastSpeedTableStepCVValue, speedMatchSpeedTableStep.getSpeedStep(), SpeedMatcherState.POST_SPEED_MATCH);
411                }
412                break;
413
414            case POST_SPEED_MATCH: {
415                statusLabel.setText(Bundle.getMessage("StatRestoreThrottle"));
416
417                //un-brick Digitrax decoders
418                setThrottle(false, 0);
419                setThrottle(true, 0);
420
421                SpeedMatcherState nextState;
422                if (trimReverseSpeed) {
423                    if (warmUpReverseSeconds > 0) {
424                        nextState = SpeedMatcherState.REVERSE_WARM_UP;
425                    } else {
426                        nextState = SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM;
427                    }
428                } else {
429                    nextState = SpeedMatcherState.COMPLETE;
430                }
431                initNextSpeedMatcherState(nextState);
432                break;
433            }
434
435            case REVERSE_WARM_UP:
436                //Run specified reverse warm up time at high speed in reverse
437                statusLabel.setText(Bundle.getMessage("StatReverseWarmUp", warmUpReverseSeconds - stepDuration));
438
439                if (stepDuration >= warmUpReverseSeconds) {
440                    initNextSpeedMatcherState(SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM);
441                } else {
442                    if (stepDuration == 0) {
443                        setSpeedMatchStateTimerDuration(5000);
444                        setThrottle(false, 28);
445                    }
446                    stepDuration += 5;
447                }
448
449                break;
450
451            case REVERSE_SPEED_MATCH_TRIM:
452                //Use PID controller logic to adjust reverse trim until high speed reverse speed matches forward
453                if (programmerState == ProgrammerState.IDLE) {
454                    if (stepDuration == 0) {
455                        statusLabel.setText(Bundle.getMessage("StatSettingReverseTrim"));
456                        setThrottle(false, speedMatchMaxSpeedStep);
457                        setSpeedMatchStateTimerDuration(8000);
458                        stepDuration = 1;
459                    } else {
460                        setSpeedMatchError(speedMatchMaxSpeedKPH);
461
462                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
463                            initNextSpeedMatcherState(SpeedMatcherState.COMPLETE);
464                        } else {
465                            reverseTrimValue = getNextSpeedMatchValue(lastReverseTrimValue, REVERSE_TRIM_MAX, REVERSE_TRIM_MIN);
466
467                            if (((lastReverseTrimValue == REVERSE_TRIM_MAX) || (lastReverseTrimValue == REVERSE_TRIM_MIN)) && (reverseTrimValue == lastReverseTrimValue)) {
468                                statusLabel.setText(Bundle.getMessage("StatSetReverseTrimFail"));
469                                logger.info("Unable to trim reverse to match forward");
470                                abort();
471                                break;
472                            }
473
474                            lastReverseTrimValue = reverseTrimValue;
475                            writeReverseTrim(reverseTrimValue);
476                        }
477                    }
478                }
479                break;
480
481            case COMPLETE:
482                if (programmerState == ProgrammerState.IDLE) {
483                    statusLabel.setText(Bundle.getMessage("StatSpeedMatchComplete"));
484                    setThrottle(true, 0);
485                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
486                }
487                break;
488
489            case USER_STOPPED:
490                if (programmerState == ProgrammerState.IDLE) {
491                    statusLabel.setText(Bundle.getMessage("StatUserStoppedSpeedMatch"));
492                    setThrottle(true, 0);
493                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
494                }
495                break;
496
497            case CLEAN_UP:
498                //wrap it up
499                if (programmerState == ProgrammerState.IDLE) {
500                    cleanUpSpeedMatcher();
501                }
502                break;
503
504            default:
505                cleanUpSpeedMatcher();
506                logger.error("Unexpected speed match timeout");
507                break;
508        }
509
510        if (speedMatcherState != SpeedMatcherState.IDLE) {
511            startSpeedMatchStateTimer();
512        }
513    }
514    //</editor-fold>
515
516    //<editor-fold defaultstate="collapsed" desc="ThrottleListener Overrides">
517    /**
518     * Called when a throttle is found
519     *
520     * @param t the requested DccThrottle
521     */
522    @Override
523    public void notifyThrottleFound(DccThrottle t) {
524        super.notifyThrottleFound(t);
525
526        if (speedMatcherState == SpeedMatcherState.WAIT_FOR_THROTTLE) {
527            logger.info("Starting speed matching");
528            // using speed matching timer to trigger each phase of speed matching            
529            initNextSpeedMatcherState(SpeedMatcherState.INIT_THROTTLE);
530            startSpeedMatchStateTimer();
531        } else {
532            cleanUpSpeedMatcher();
533        }
534    }
535    //</editor-fold>
536
537    //<editor-fold defaultstate="collapsed" desc="Helper Functions">
538    /**
539     * Helper function for speed matching the current speedMatchSpeedTableStep
540     *
541     * @param maxCVValue the maximum allowable value for the CV
542     * @param minCVValue the minimum allowable value for the CV
543     * @param nextState  the SpeedMatcherState to advance to if speed matching
544     *                   is complete
545     */
546    private void speedMatchSpeedStepInner(int maxCVValue, int minCVValue, SpeedMatcherState nextState) {
547        if (stepDuration == 0) {
548            speedStepTargetSpeedKPH = getSpeedStepScaleSpeedInKPH(speedMatchSpeedTableStep.getSpeedStep());
549
550            statusLabel.setText(Bundle.getMessage("StatSettingSpeed", speedMatchSpeedTableStep.getCV() + " (Speed Step " + String.valueOf(speedMatchSpeedTableStep.getSpeedStep()) + ")"));
551            logger.info("Setting CV {} (speed step {}) to {} KPH ({} MPH)", speedMatchSpeedTableStep.getCV(), speedMatchSpeedTableStep.getSpeedStep(), String.valueOf(speedStepTargetSpeedKPH), String.valueOf(Speed.kphToMph(speedStepTargetSpeedKPH)));
552
553            setThrottle(true, speedMatchSpeedTableStep.getSpeedStep());
554
555            writeSpeedTableStep(speedMatchSpeedTableStep, speedMatchCVValue);
556
557            setSpeedMatchStateTimerDuration(8000);
558            stepDuration = 1;
559        } else {
560            setSpeedMatchError(speedStepTargetSpeedKPH);
561
562            if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
563                lastSpeedTableStepCVValue = speedMatchCVValue;
564
565                speedMatchSpeedTableStep = speedMatchSpeedTableStep.getPrevious();
566
567                if (speedMatchSpeedTableStep != SpeedTableStep.STEP1) {
568                    initNextSpeedMatcherState(speedMatcherState);
569                } else {
570                    initNextSpeedMatcherState(nextState);
571                }
572            } else {
573                speedMatchCVValue = getNextSpeedMatchValue(lastSpeedMatchCVValue, maxCVValue, minCVValue);
574
575                if (((speedMatchCVValue == maxCVValue) || (speedMatchCVValue == minCVValue)) && (speedMatchCVValue == lastSpeedMatchCVValue)) {
576                    statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", speedMatchSpeedTableStep.getCV() + " (Speed Step " + String.valueOf(speedMatchSpeedTableStep.getSpeedStep()) + ")"));
577                    logger.info("Unable to achieve desired speed for CV {} (Speed Step {})", speedMatchSpeedTableStep.getCV(), String.valueOf(speedMatchSpeedTableStep.getSpeedStep()));
578                    abort();
579                    return;
580                }
581
582                lastSpeedMatchCVValue = speedMatchCVValue;
583                writeSpeedTableStep(speedMatchSpeedTableStep, speedMatchCVValue);
584            }
585        }
586    }
587
588    /**
589     * Aborts the speed matching process programmatically
590     */
591    private void abort() {
592        initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
593    }
594
595    /**
596     * Stops the speed matching process due to user input
597     */
598    private void userStop() {
599        initNextSpeedMatcherState(SpeedMatcherState.USER_STOPPED);
600    }
601
602    /**
603     * Sets up the speed match state by clearing the speed match error, clearing
604     * the step duration, setting the timer duration, and setting the next state
605     *
606     * @param nextState - next SpeedMatcherState to set
607     */
608    protected void initNextSpeedMatcherState(SpeedMatcherState nextState) {
609        resetSpeedMatchError();
610        stepDuration = 0;
611        speedMatcherState = nextState;
612        setSpeedMatchStateTimerDuration(1800);
613    }
614    //</editor-fold>
615
616    //debugging logger
617    private final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SpeedStepScaleESUTableSpeedMatcher.class);
618}