001package jmri.jmrix.bachrus.speedmatcher.basic;
002
003import jmri.DccThrottle;
004import jmri.jmrix.bachrus.Speed;
005
006/**
007 * This is a simple speed matcher which will speed match a locomotive to a given
008 * start and top speed using the VStart, VMid, and VHigh CVs.
009 *
010 * @author Todd Wegter Copyright (C) 2024
011 */
012public class BasicSimpleCVSpeedMatcher extends BasicSpeedMatcher {
013
014    //<editor-fold defaultstate="collapsed" desc="Constants">
015    private final int INITIAL_VSTART = 1;
016    private final int INITIAL_VMID = 2;
017    private final int INITIAL_VHIGH = 255;
018    private final int INITIAL_TRIM = 128;
019
020    private final int VHIGH_MAX = 255;
021    private final int VHIGH_MIN = INITIAL_VMID + 1;
022    private final int VMID_MIN = INITIAL_VSTART + 1;
023    private final int VSTART_MIN = 1;
024    //</editor-fold>
025
026    //<editor-fold defaultstate="collapsed" desc="Enums">
027    protected enum SpeedMatcherState {
028        IDLE,
029        WAIT_FOR_THROTTLE,
030        INIT_THROTTLE,
031        INIT_ACCEL,
032        INIT_DECEL,
033        INIT_VSTART,
034        INIT_VMID,
035        INIT_VHIGH,
036        INIT_FORWARD_TRIM,
037        INIT_REVERSE_TRIM,
038        POST_INIT,
039        FORWARD_WARM_UP,
040        FORWARD_SPEED_MATCH_VHIGH,
041        FORWARD_SPEED_MATCH_VMID,
042        FORWARD_SPEED_MATCH_VSTART,
043        REVERSE_WARM_UP,
044        REVERSE_SPEED_MATCH_TRIM,
045        COMPLETE,
046        USER_STOPPED,
047        CLEAN_UP,
048    }
049    //</editor-fold>
050
051    //<editor-fold defaultstate="collapsed" desc="Instance Variables">
052    private int vHigh = INITIAL_VHIGH;
053    private int lastVHigh = INITIAL_VHIGH;
054    private int vMid = INITIAL_VSTART;
055    private int lastVMid = INITIAL_VSTART;
056    private int vMidMax;
057    private int vStart = INITIAL_VSTART;
058    private int lastVStart = INITIAL_VSTART;
059    private int vStartMax;
060    private int reverseTrimValue = INITIAL_TRIM;
061    private int lastReverseTrimValue = INITIAL_TRIM;
062
063    private final float targetMidSpeedKPH;
064
065    private SpeedMatcherState speedMatcherState = SpeedMatcherState.IDLE;
066    //</editor-fold>
067
068    /**
069     * Constructs the BasicSimpleCVSpeedMatcher from a BasicSpeedMatcherConfig
070     *
071     * @param config BasicSpeedMatcherConfig
072     */
073    public BasicSimpleCVSpeedMatcher(BasicSpeedMatcherConfig config) {
074        super(config);
075
076        this.targetMidSpeedKPH = this.targetStartSpeedKPH + ((this.targetTopSpeedKPH - this.targetStartSpeedKPH) / 2);
077    }
078
079    //<editor-fold defaultstate="collapsed" desc="SpeedMatcher Overrides">
080    /**
081     * Starts the speed matching process
082     *
083     * @return true if speed matching started successfully, false otherwise
084     */
085    @Override
086    public boolean startSpeedMatcher() {
087        if (!validate()) {
088            return false;
089        }
090
091        //reset instance variables
092        vStart = INITIAL_VSTART;
093        lastVStart = INITIAL_VSTART;
094        vMid = INITIAL_VSTART;
095        lastVMid = INITIAL_VSTART;
096        vHigh = INITIAL_VHIGH;
097        lastVHigh = INITIAL_VHIGH;
098        reverseTrimValue = INITIAL_TRIM;
099        lastReverseTrimValue = INITIAL_TRIM;
100
101        speedMatcherState = SpeedMatcherState.WAIT_FOR_THROTTLE;
102
103        if (!initializeAndStartSpeedMatcher(e -> speedMatchTimeout())) {
104            cleanUpSpeedMatcher();
105            return false;
106        }
107
108        startStopButton.setText(Bundle.getMessage("SpeedMatchStopBtn"));
109
110        return true;
111    }
112
113    /**
114     * Stops the speed matching process
115     */
116    @Override
117    public void stopSpeedMatcher() {
118        if (!isSpeedMatcherIdle()) {
119            logger.info("Speed matching manually stopped");
120            userStop();
121        } else {
122            cleanUpSpeedMatcher();
123        }
124    }
125
126    /**
127     * Indicates if the speed matcher is idle (not currently speed matching)
128     *
129     * @return true if idle, false otherwise
130     */
131    @Override
132    public boolean isSpeedMatcherIdle() {
133        return speedMatcherState == SpeedMatcherState.IDLE;
134    }
135
136    /**
137     * Cleans up the speed matcher when speed matching is stopped or is finished
138     */
139    @Override
140    protected void cleanUpSpeedMatcher() {
141        speedMatcherState = SpeedMatcherState.IDLE;
142
143        super.cleanUpSpeedMatcher();
144    }
145    //</editor-fold>
146
147    //<editor-fold defaultstate="collapsed" desc="Speed Matcher State">
148    /**
149     * Main speed matching timeout handler. This is the state machine that
150     * effectively does the speed matching process.
151     */
152    private synchronized void speedMatchTimeout() {
153        switch (speedMatcherState) {
154            case WAIT_FOR_THROTTLE:
155                cleanUpSpeedMatcher();
156                logger.error("Timeout waiting for throttle");
157                statusLabel.setText(Bundle.getMessage("StatusTimeout"));
158                break;
159
160            case INIT_THROTTLE:
161                //set throttle to 0 for init
162                setThrottle(true, 0);
163                initNextSpeedMatcherState(SpeedMatcherState.INIT_ACCEL);
164                break;
165
166            case INIT_ACCEL:
167                //set acceleration momentum to 0 (CV 3)
168                if (programmerState == ProgrammerState.IDLE) {
169                    writeMomentumAccel(INITIAL_MOMENTUM);
170                    initNextSpeedMatcherState(SpeedMatcherState.INIT_DECEL);
171                }
172                break;
173
174            case INIT_DECEL:
175                //set deceleration mementum to 0 (CV 4)
176                if (programmerState == ProgrammerState.IDLE) {
177                    writeMomentumDecel(INITIAL_MOMENTUM);
178                    initNextSpeedMatcherState(SpeedMatcherState.INIT_VSTART);
179                }
180                break;
181
182            case INIT_VSTART:
183                //set vStart to 0 (CV 2)
184                if (programmerState == ProgrammerState.IDLE) {
185                    writeVStart(INITIAL_VSTART);
186                    initNextSpeedMatcherState(SpeedMatcherState.INIT_VMID);
187                }
188                break;
189
190            case INIT_VMID:
191                //set vMid to 1 (CV 6)
192                if (programmerState == ProgrammerState.IDLE) {
193                    writeVMid(INITIAL_VMID);
194                    initNextSpeedMatcherState(SpeedMatcherState.INIT_VHIGH);
195                }
196                break;
197
198            case INIT_VHIGH:
199                //set vHigh to 255 (CV 5)
200                if (programmerState == ProgrammerState.IDLE) {
201                    writeVHigh(INITIAL_VHIGH);
202                    initNextSpeedMatcherState(SpeedMatcherState.INIT_FORWARD_TRIM);
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.FORWARD_SPEED_MATCH_VHIGH;
234                }
235                initNextSpeedMatcherState(nextState, 30);
236                break;
237            }
238
239            case FORWARD_WARM_UP:
240                //Run specified forward warm up time at high speed forward
241                statusLabel.setText(Bundle.getMessage("StatForwardWarmUp", warmUpForwardSeconds - stepDuration));
242
243                if (stepDuration >= warmUpForwardSeconds) {
244                    initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_VHIGH, 30);
245                } else {
246                    if (stepDuration == 0) {
247                        setSpeedMatchStateTimerDuration(5000);
248                        setThrottle(true, 28);
249                    }
250                    stepDuration += 5;
251                }
252                break;
253
254            case FORWARD_SPEED_MATCH_VHIGH:
255                //Use PID Controller logic to adjust vHigh to achieve desired speed
256                if (programmerState == ProgrammerState.IDLE) {
257                    if (stepDuration == 0) {
258                        statusLabel.setText(Bundle.getMessage("StatSettingSpeed", "5 (vHigh)"));
259                        logger.info("Setting CV 5 (vHigh) to {} KPH ({} MPH)", String.valueOf(targetTopSpeedKPH), String.valueOf(Speed.kphToMph(targetTopSpeedKPH)));
260                        setThrottle(true, 28);
261                        setSpeedMatchStateTimerDuration(8000);
262                        stepDuration = 1;
263                    } else {
264                        setSpeedMatchError(targetTopSpeedKPH);
265
266                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
267                            initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_VMID);
268                        } else {
269                            vHigh = getNextSpeedMatchValue(lastVHigh, VHIGH_MAX, VHIGH_MIN);
270
271                            if (((vHigh == VHIGH_MAX) || (vHigh == VHIGH_MIN)) && (vHigh == lastVHigh)) {
272                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", "5 (vHigh)"));
273                                logger.info("Unable to achieve desired speed for CV 5 (vHigh)");
274                                abort();
275                                break;
276                            }
277
278                            lastVHigh = vHigh;
279                            writeVHigh(vHigh);
280                        }
281                    }
282                }
283                break;
284
285            case FORWARD_SPEED_MATCH_VMID:
286                //Use PID Controller logic to adjust vMid to achieve desired speed
287                if (programmerState == ProgrammerState.IDLE) {
288                    if (stepDuration == 0) {
289                        vMid = INITIAL_VSTART + ((vHigh - INITIAL_VSTART) / 2);
290                        lastVMid = vMid;
291                        vMidMax = vHigh - 1;
292                        writeVMid(vMid);
293
294                        statusLabel.setText(Bundle.getMessage("StatSettingSpeed", "6 (vMid)"));
295                        logger.info("Setting CV 6 (vMid) to {} KPH ({} MPH)", String.valueOf(targetMidSpeedKPH), String.valueOf(Speed.kphToMph(targetMidSpeedKPH)));
296                        setSpeedMatchStateTimerDuration(8000);
297                        setThrottle(true, 14);
298                        stepDuration = 1;
299
300                    } else {
301                        setSpeedMatchError(targetMidSpeedKPH);
302
303                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
304                            initNextSpeedMatcherState(SpeedMatcherState.FORWARD_SPEED_MATCH_VSTART, 3);
305                        } else {
306                            vMid = getNextSpeedMatchValue(lastVMid, vMidMax, VMID_MIN);
307
308                            if (((vMid == vMidMax) || (vMid == VMID_MIN)) && (vMid == lastVMid)) {
309                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", "6 (vMid)"));
310                                logger.info("Unable to achieve desired speed for CV 6 (vMid)");
311                                abort();
312                                break;
313                            }
314
315                            lastVMid = vMid;
316                            writeVMid(vMid);
317                        }
318                    }
319                }
320                break;
321
322            case FORWARD_SPEED_MATCH_VSTART: {
323                //Use PID Controller to adjust vStart to achieve desired speed
324                if (programmerState == ProgrammerState.IDLE) {
325                    if (stepDuration == 0) {
326                        vStartMax = vMid - 1;
327                        statusLabel.setText(Bundle.getMessage("StatSettingSpeed", "2 (vStart)"));
328                        logger.info("Setting CV 2 (vStart) to {} KPH ({} MPH)", String.valueOf(targetStartSpeedKPH), String.valueOf(Speed.kphToMph(targetStartSpeedKPH)));
329                        setThrottle(true, 1);
330                        setSpeedMatchStateTimerDuration(15000);
331                        stepDuration = 1;
332                    } else {
333                        setSpeedMatchError(targetStartSpeedKPH);
334
335                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
336                            SpeedMatcherState nextState;
337                            if (trimReverseSpeed) {
338                                if (warmUpReverseSeconds > 0) {
339                                    nextState = SpeedMatcherState.REVERSE_WARM_UP;
340                                } else {
341                                    nextState = SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM;
342                                }
343                            } else {
344                                nextState = SpeedMatcherState.COMPLETE;
345                            }
346                            initNextSpeedMatcherState(nextState);
347                        } else {
348                            vStart = getNextSpeedMatchValue(lastVStart, vStartMax, VSTART_MIN);
349
350                            if (((vStart == vStartMax) || (vStart == VSTART_MIN)) && (vStart == lastVStart)) {
351                                statusLabel.setText(Bundle.getMessage("StatSetSpeedFail", "2 (vStart)"));
352                                logger.info("Unable to achieve desired speed for CV 2 (vStart)");
353                                abort();
354                                break;
355                            }
356
357                            lastVStart = vStart;
358                            writeVStart(vStart);
359                        }
360                    }
361                }
362                break;
363            }
364
365            case REVERSE_WARM_UP:
366                //Run specified reverse warm up time at high speed in reverse
367                statusLabel.setText(Bundle.getMessage("StatReverseWarmUp", warmUpReverseSeconds - stepDuration));
368
369                if (stepDuration >= warmUpReverseSeconds) {
370                    initNextSpeedMatcherState(SpeedMatcherState.REVERSE_SPEED_MATCH_TRIM);
371                } else {
372                    if (stepDuration == 0) {
373                        setSpeedMatchStateTimerDuration(5000);
374                        setThrottle(false, 28);
375                    }
376                    stepDuration += 5;
377                }
378
379                break;
380
381            case REVERSE_SPEED_MATCH_TRIM:
382                //Use PID controller logic to adjust reverse trim until high speed reverse speed matches forward
383                if (programmerState == ProgrammerState.IDLE) {
384                    if (stepDuration == 0) {
385                        statusLabel.setText(Bundle.getMessage("StatSettingReverseTrim"));
386                        setThrottle(false, 28);
387                        setSpeedMatchStateTimerDuration(8000);
388                        stepDuration = 1;
389                    } else {
390                        setSpeedMatchError(targetTopSpeedKPH);
391
392                        if (Math.abs(speedMatchError) < ALLOWED_SPEED_MATCH_ERROR) {
393                            initNextSpeedMatcherState(SpeedMatcherState.COMPLETE);
394                        } else {
395                            reverseTrimValue = getNextSpeedMatchValue(lastReverseTrimValue, REVERSE_TRIM_MAX, REVERSE_TRIM_MIN);
396
397                            if (((lastReverseTrimValue == REVERSE_TRIM_MAX) || (lastReverseTrimValue == REVERSE_TRIM_MIN)) && (reverseTrimValue == lastReverseTrimValue)) {
398                                statusLabel.setText(Bundle.getMessage("StatSetReverseTrimFail"));
399                                logger.info("Unable to trim reverse to match forward");
400                                abort();
401                                break;
402                            }
403
404                            lastReverseTrimValue = reverseTrimValue;
405                            writeReverseTrim(reverseTrimValue);
406                        }
407                    }
408                }
409                break;
410
411            case COMPLETE:
412                if (programmerState == ProgrammerState.IDLE) {
413                    statusLabel.setText(Bundle.getMessage("StatSpeedMatchComplete"));
414                    setThrottle(true, 0);
415                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
416                }
417                break;
418
419            case USER_STOPPED:
420                if (programmerState == ProgrammerState.IDLE) {
421                    statusLabel.setText(Bundle.getMessage("StatUserStoppedSpeedMatch"));
422                    setThrottle(true, 0);
423                    initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
424                }
425                break;
426
427            case CLEAN_UP:
428                //wrap it up
429                if (programmerState == ProgrammerState.IDLE) {
430                    cleanUpSpeedMatcher();
431                }
432                break;
433
434            default:
435                cleanUpSpeedMatcher();
436                logger.error("Unexpected speed match timeout");
437                break;
438        }
439
440        if (speedMatcherState != SpeedMatcherState.IDLE) {
441            startSpeedMatchStateTimer();
442        }
443    }
444    //</editor-fold>
445
446    //<editor-fold defaultstate="collapsed" desc="ThrottleListener Overrides">
447    /**
448     * Called when a throttle is found
449     *
450     * @param t the requested DccThrottle
451     */
452    @Override
453    public void notifyThrottleFound(DccThrottle t) {
454        super.notifyThrottleFound(t);
455
456        if (speedMatcherState == SpeedMatcherState.WAIT_FOR_THROTTLE) {
457            logger.info("Starting speed matching");
458            // using speed matching timer to trigger each phase of speed matching            
459            initNextSpeedMatcherState(SpeedMatcherState.INIT_THROTTLE);
460            startSpeedMatchStateTimer();
461        } else {
462            cleanUpSpeedMatcher();
463        }
464    }
465    //</editor-fold>
466
467    //<editor-fold defaultstate="collapsed" desc="Helper Functions">
468    /**
469     * Aborts the speed matching process programmatically
470     */
471    private void abort() {
472        initNextSpeedMatcherState(SpeedMatcherState.CLEAN_UP);
473    }
474
475    /**
476     * Stops the speed matching process due to user input
477     */
478    private void userStop() {
479        initNextSpeedMatcherState(SpeedMatcherState.USER_STOPPED);
480    }
481
482    /**
483     * Sets up the speed match state by resetting the speed matcher with a value delta of 10,
484     * clearing the step duration, setting the timer duration, and setting the next state
485     *
486     * @param nextState next SpeedMatcherState to set
487     */
488    protected void initNextSpeedMatcherState(SpeedMatcherState nextState) {
489        initNextSpeedMatcherState(nextState, 10); 
490    }
491    
492    /**
493     * Sets up the speed match state by resetting the speed matcher with the given value delta,
494     * clearing the step duration, setting the timer duration, and setting the next state
495     *
496     * @param nextState next SpeedMatcherState to set
497     * @param speedMatchValueDelta the value delta to use when resetting the speed matcher
498     */
499    protected void initNextSpeedMatcherState(SpeedMatcherState nextState, int speedMatchValueDelta) {
500        resetSpeedMatcher(speedMatchValueDelta);
501        stepDuration = 0;
502        speedMatcherState = nextState;
503        setSpeedMatchStateTimerDuration(1800);
504    }
505    //</editor-fold>
506
507    //debugging logger
508    private final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(BasicSimpleCVSpeedMatcher.class);
509}