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