001package jmri.jmrix.bachrus;
002
003//<editor-fold defaultstate="collapsed" desc="Imports">
004import java.awt.BorderLayout;
005import java.awt.CardLayout;
006import java.awt.Color;
007import java.awt.Dimension;
008import java.awt.FlowLayout;
009import java.awt.Font;
010import java.awt.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.Insets;
013import java.beans.PropertyChangeEvent;
014import java.beans.PropertyChangeListener;
015import java.text.MessageFormat;
016import java.text.SimpleDateFormat;
017import java.util.Date;
018import java.util.Locale;
019
020import javax.swing.*;
021import javax.swing.border.Border;
022import javax.swing.border.EtchedBorder;
023import javax.swing.border.TitledBorder;
024
025import jmri.AddressedProgrammer;
026import jmri.AddressedProgrammerManager;
027import jmri.CommandStation;
028import jmri.DccLocoAddress;
029import jmri.DccThrottle;
030import jmri.GlobalProgrammerManager;
031import jmri.InstanceManager;
032import jmri.JmriException;
033import jmri.PowerManager;
034import jmri.ProgListener;
035import jmri.Programmer;
036import jmri.ProgrammerException;
037import jmri.SpeedStepMode;
038import jmri.ThrottleListener;
039import jmri.jmrit.DccLocoAddressSelector;
040import jmri.jmrit.roster.RosterEntry;
041import jmri.jmrit.roster.RosterEntrySelector;
042import jmri.jmrit.roster.swing.GlobalRosterEntryComboBox;
043import jmri.util.JmriJFrame;
044import jmri.util.swing.JmriJOptionPane;
045
046//</editor-fold>
047/**
048 * Frame for Speedo Console for Bachrus running stand reader interface
049 *
050 * @author Andrew Crosland Copyright (C) 2010
051 * @author Dennis Miller Copyright (C) 2015
052 * @author Todd Wegter Copyright (C) 2019
053 */
054public class SpeedoConsoleFrame extends JmriJFrame implements SpeedoListener,
055        ThrottleListener,
056        ProgListener,
057        PropertyChangeListener {
058
059    /**
060     * TODO: Complete the help file
061     */
062    //<editor-fold defaultstate="collapsed" desc="Enums">
063    protected enum DisplayType {
064        NUMERIC, DIAL
065    }
066
067    protected enum ProfileState {
068        IDLE, WAIT_FOR_THROTTLE, RUNNING
069    }
070
071    protected enum ProfileDirection {
072        FORWARD, REVERSE
073    }
074
075    protected enum SpeedMatchState {
076        IDLE,
077        WAIT_FOR_THROTTLE,
078        SETUP,
079        FORWARD_WARM_UP,
080        REVERSE_WARM_UP,
081        FORWARD_SPEED_MATCH_STEP_1,
082        FORWARD_SPEED_MATCH_STEP_28,
083        REVERSE_SPEED_MATCH_TRIM,
084        RESTORE_MOMENTUM
085    }
086
087    protected enum SpeedMatchSetupState {
088        IDLE,
089        MOMENTUM_ACCEL_READ,
090        MOMENTUM_DECEL_READ,
091        MOMENTUM_ACCEL_WRITE,
092        MOMENTUM_DECEL_WRITE,
093        VSTART,
094        VHIGH,
095        FORWARD_TRIM,
096        REVERSE_TRIM,
097        BEGIN_SPEED_MATCH
098    }
099
100    protected enum ProgState {
101        IDLE,
102        READ1,
103        READ3,
104        READ4,
105        READ17,
106        READ18,
107        READ29,
108        WRITE2,
109        WRITE3,
110        WRITE4,
111        WRITE5,
112        WRITE6,
113        WRITE66,
114        WRITE95
115    }
116
117    static final int SPEEDMATCHWARMUPTIME = 60;
118
119    //</editor-fold>
120    //<editor-fold defaultstate="collapsed" desc="Member Variables">
121    //<editor-fold defaultstate="collapsed" desc="General GUI Elements">
122    protected JLabel scaleLabel = new JLabel();
123    protected JLabel customScaleLabel = new JLabel();
124    protected JTextField customScaleField = new JTextField(3);
125    protected int customScale = 148;
126    protected JTextField speedTextField = new JTextField(12);
127    protected JPanel displayCards = new JPanel();
128
129    protected ButtonGroup modeGroup = new ButtonGroup();
130    protected JRadioButton progButton = new JRadioButton(Bundle.getMessage("ProgTrack"));
131    protected JRadioButton mainButton = new JRadioButton(Bundle.getMessage("OnMain"));
132
133    protected ButtonGroup speedGroup = new ButtonGroup();
134    protected JRadioButton mphButton = new JRadioButton(Bundle.getMessage("MPH"));
135    protected JRadioButton kphButton = new JRadioButton(Bundle.getMessage("KPH"));
136    protected ButtonGroup displayGroup = new ButtonGroup();
137    protected JRadioButton numButton = new JRadioButton(Bundle.getMessage("Numeric"));
138    protected JRadioButton dialButton = new JRadioButton(Bundle.getMessage("Dial"));
139    protected SpeedoDial speedoDialDisplay = new SpeedoDial();
140    protected JCheckBox dirFwdButton = new JCheckBox(Bundle.getMessage("ScanForward"));
141    protected JCheckBox dirRevButton = new JCheckBox(Bundle.getMessage("ScanReverse"));
142    protected JCheckBox toggleGridButton = new JCheckBox(Bundle.getMessage("ToggleGrid"));
143
144    GraphPane profileGraphPane;
145
146    protected JLabel statusLabel = new JLabel(" ");
147
148    protected javax.swing.JLabel readerLabel = new javax.swing.JLabel();
149    //</editor-fold>
150    //<editor-fold defaultstate="collapsed" desc="General Member Variables">
151    protected static final int defaultScale = 8;
152
153    protected float selectedScale = 0;
154    protected int series = 0;
155    protected float sampleSpeed = 0;
156    protected float targetSpeed = 0;
157    protected float currentSpeed = 0;
158    protected float incSpeed = 0;
159    protected float oldSpeed = 0;
160    protected float acc = 0;
161    protected float avSpeed = 0;
162    protected int range = 1;
163    protected float circ = 0;
164    protected float count = 1;
165    protected float freq;
166    protected static final int DISPLAY_UPDATE = 500;
167    protected static final int FAST_DISPLAY_RATIO = 5;
168
169    /*
170     * At low speed, readings arrive less often and less filtering
171     * is applied to minimize the delay in updating the display
172     *
173     * Speed measurement is split into 4 ranges with an overlap, to
174     * prevent "hunting" between the ranges.
175     */
176    protected static final int RANGE1LO = 0;
177    protected static final int RANGE1HI = 9;
178    protected static final int RANGE2LO = 7;
179    protected static final int RANGE2HI = 31;
180    protected static final int RANGE3LO = 29;
181    protected static final int RANGE3HI = 62;
182    protected static final int RANGE4LO = 58;
183    protected static final int RANGE4HI = 9999;
184    static final int[] filterLength = {0, 3, 6, 10, 20};
185
186    String selectedScalePref = this.getClass().getName() + ".SelectedScale"; // NOI18N
187    String customScalePref = this.getClass().getName() + ".CustomScale"; // NOI18N
188    String speedUnitsKphPref = this.getClass().getName() + ".SpeedUnitsKph"; // NOI18N
189    String dialTypePref = this.getClass().getName() + ".DialType"; // NOI18N
190    jmri.UserPreferencesManager prefs;
191
192    // members for handling the Speedo interface
193    SpeedoTrafficController tc = null;
194    String replyString;
195
196    protected String[] scaleStrings = new String[]{
197        Bundle.getMessage("ScaleZ"),
198        Bundle.getMessage("ScaleEuroN"),
199        Bundle.getMessage("ScaleNFine"),
200        Bundle.getMessage("ScaleJapaneseN"),
201        Bundle.getMessage("ScaleBritishN"),
202        Bundle.getMessage("Scale3mm"),
203        Bundle.getMessage("ScaleTT"),
204        Bundle.getMessage("Scale00"),
205        Bundle.getMessage("ScaleH0"),
206        Bundle.getMessage("ScaleS"),
207        Bundle.getMessage("Scale048"),
208        Bundle.getMessage("Scale045"),
209        Bundle.getMessage("Scale043"),
210        Bundle.getMessage("ScaleOther")
211    };
212
213    protected float[] scales = new float[]{
214        220,
215        160,
216        152,
217        150,
218        148,
219        120,
220        101.6F,
221        76,
222        87,
223        64,
224        48,
225        45,
226        43,
227        -1
228    };
229
230    //Create the combo box, and assign the scales to it
231    JComboBox<String> scaleList = new JComboBox<>(scaleStrings);
232
233    private SpeedoSystemConnectionMemo _memo = null;
234
235    protected DisplayType display = DisplayType.NUMERIC;
236    //</editor-fold>
237    //<editor-fold defaultstate="collapsed" desc="DCC Services">
238    /*
239     * Keep track of the DCC services available
240     */
241    protected int dccServices;
242    protected static final int BASIC = 0;
243    protected static final int PROG = 1;
244    protected static final int COMMAND = 2;
245    protected static final int THROTTLE = 4;
246
247    protected boolean timerRunning = false;
248
249    protected ProgState progState = ProgState.IDLE;
250
251    protected float throttleIncrement;
252    protected Programmer prog = null;
253    protected AddressedProgrammer ops_mode_prog = null;
254    protected CommandStation commandStation = null;
255
256    private PowerManager pm = null;
257    //</editor-fold>
258    //<editor-fold defaultstate="collapsed" desc="Address Selector GUI Elements">
259    //protected JLabel profileAddressLabel = new JLabel(Bundle.getMessage("LocoAddress"));
260    //protected JTextField profileAddressField = new JTextField(6);
261    protected JButton readAddressButton = new JButton(Bundle.getMessage("Read"));
262
263    private final DccLocoAddressSelector addrSelector = new DccLocoAddressSelector();
264    private JButton setButton;
265    private GlobalRosterEntryComboBox rosterBox;
266    protected RosterEntry rosterEntry;
267    //</editor-fold>
268    //<editor-fold defaultstate="collapsed" desc="Address Selector Member Variables">
269    private final boolean disableRosterBoxActions = false;
270    private DccLocoAddress locomotiveAddress = new DccLocoAddress(0, false);
271
272    //protected int profileAddress = 0;
273    protected int readAddress = 0;
274    //</editor-fold>
275    //<editor-fold defaultstate="collapsed" desc="Speed Profile GUI Elements">
276    protected JButton trackPowerButton = new JButton(Bundle.getMessage("PowerUp"));
277    protected JButton startProfileButton = new JButton(Bundle.getMessage("Start"));
278    protected JButton stopProfileButton = new JButton(Bundle.getMessage("Stop"));
279    protected JButton exportProfileButton = new JButton(Bundle.getMessage("Export"));
280    protected JButton printProfileButton = new JButton(Bundle.getMessage("Print"));
281    protected JButton resetGraphButton = new JButton(Bundle.getMessage("ResetGraph"));
282    protected JButton loadProfileButton = new JButton(Bundle.getMessage("LoadRef"));
283    protected JTextField printTitleText = new JTextField();
284    //</editor-fold>
285    //<editor-fold defaultstate="collapsed" desc="Speed Profile Member Variables">
286    protected DccSpeedProfile spFwd;
287    protected DccSpeedProfile spRev;
288    protected DccSpeedProfile spRef;
289
290    protected ProfileDirection profileDir = ProfileDirection.FORWARD;
291    protected DccThrottle throttle = null;
292    protected int profileStep = 0;
293    protected float profileSpeed;
294
295    protected ProfileState profileState = ProfileState.IDLE;
296    //</editor-fold>
297    //<editor-fold defaultstate="collapsed" desc="Speed Matching GUI Elements">
298    protected JLabel speedStep1TargetLabel = new JLabel(Bundle.getMessage("lblSpeedStep1"));
299    protected JTextField speedStep1TargetField = new JTextField("3", 3);
300    protected JLabel speedStep1TargetUnit = new JLabel(Bundle.getMessage("lblMPH"));
301    protected JLabel speedStep28TargetLabel = new JLabel(Bundle.getMessage("lblSpeedStep28"));
302    protected JTextField speedStep28TargetField = new JTextField("55", 3);
303    protected JLabel speedStep28TargetUnit = new JLabel(Bundle.getMessage("lblMPH"));
304    protected JCheckBox speedMatchWarmUpCheckBox = new JCheckBox(Bundle.getMessage("chkbxWarmUp"));
305    protected JButton speedMatchButton = new JButton(Bundle.getMessage("btnStartSpeedMatch"));
306    //</editor-fold>
307    //<editor-fold defaultstate="collapsed" desc="Speed Matching Memeber Variables">
308    //PID Controller Values
309    protected static float kP = 0.75f;
310    protected static float kI = 0.3f;
311    protected static float kD = 0.4f;
312    protected float speedMatchIntegral = 0;
313    protected float speedMatchDerivative = 0;
314    protected float lastSpeedMatchError = 0;
315    protected float speedMatchError = 0;
316    protected float speedStep1Target = 0;
317    protected float speedStep28Target = 0;
318    protected int lastVStart = 1;
319    protected int lastVHigh = 255;
320    protected int lastReverseTrim = 128;
321    protected int vStart = 1;
322    protected int vHigh = 255;
323    protected int reverseTrim = 128;
324
325    protected int speedMatchDuration = 0;
326
327    protected int oldMomentumAccel;
328    protected int oldMomentumDecel;
329
330    protected SpeedMatchState speedMatchState = SpeedMatchState.IDLE;
331    protected SpeedMatchSetupState speedMatchSetupState = SpeedMatchSetupState.IDLE;
332    //</editor-fold>
333    //</editor-fold>
334    // For testing only, must be 1 for normal use
335    protected static final int speedTestScaleFactor = 1;
336
337    /**
338     * Constructor for the SpeedoConsoleFrame
339     *
340     * @param memo the memo for the connection the Speedo is using
341     */
342    public SpeedoConsoleFrame(SpeedoSystemConnectionMemo memo) {
343        super();
344        _memo = memo;
345    }
346
347    /**
348     * Grabs the title for the SpeedoConsoleFrame
349     *
350     * @return the frame's title
351     */
352    protected String title() {
353        return Bundle.getMessage("SpeedoConsole");
354    }
355
356    /**
357     * Sets the description for the speed profile
358     */
359    private void setTitle() {
360        Date today;
361        String result;
362        SimpleDateFormat formatter;
363        formatter = new SimpleDateFormat("EEE d MMM yyyy", Locale.getDefault());
364        today = new Date();
365        result = formatter.format(today);
366        String annotate = Bundle.getMessage("ProfileFor") + " "
367                + locomotiveAddress.getNumber() + " " + Bundle.getMessage("CreatedOn")
368                + " " + result;
369        printTitleText.setText(annotate);
370    }
371
372    /**
373     * Override for the JmriJFrame's dispose function
374     */
375    @Override
376    public void dispose() {
377        if(prefs!=null) {
378           prefs.setComboBoxLastSelection(selectedScalePref, (String)scaleList.getSelectedItem());
379           prefs.setProperty(customScalePref, "customScale", customScale);
380           prefs.setSimplePreferenceState(speedUnitsKphPref, kphButton.isSelected());
381           prefs.setSimplePreferenceState(dialTypePref, dialButton.isSelected());
382        }
383        _memo.getTrafficController().removeSpeedoListener(this);
384        super.dispose();
385    }
386
387    // FIXME: Why does the if statement in this method include a direct false?
388    /**
389     * Override for the JmriJFrame's initComponents function
390     */
391    @Override
392    public void initComponents() {
393        prefs = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
394
395        setTitle(title());
396        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
397
398        // What services do we have?
399        dccServices = BASIC;
400        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null) {
401            if (InstanceManager.getDefault(GlobalProgrammerManager.class).isGlobalProgrammerAvailable()) {
402                prog = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
403                dccServices |= PROG;
404            }
405        }
406        if (InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
407            // otherwise we'll send speed commands
408            log.info("Using Throttle interface for profiling");
409            dccServices |= THROTTLE;
410        }
411
412        if (InstanceManager.getNullableDefault(jmri.PowerManager.class) != null) {
413            pm = InstanceManager.getDefault(jmri.PowerManager.class);
414            pm.addPropertyChangeListener(this);
415        }
416
417        //<editor-fold defaultstate="collapsed" desc="GUI Layout and Button Handlers">
418        //<editor-fold defaultstate="collapsed" desc="Basic Setup Panel">
419        /*
420         * Setup pane for basic operations
421         */
422        JPanel basicPane = new JPanel();
423        basicPane.setLayout(new BoxLayout(basicPane, BoxLayout.Y_AXIS));
424
425        // Scale panel to hold the scale selector
426        JPanel scalePanel = new JPanel();
427        scalePanel.setBorder(BorderFactory.createTitledBorder(
428                BorderFactory.createEtchedBorder(), Bundle.getMessage("SelectScale")));
429        scalePanel.setLayout(new FlowLayout());
430
431        scaleList.setToolTipText(Bundle.getMessage("SelectScaleToolTip"));
432        String lastSelectedScale = prefs.getComboBoxLastSelection(selectedScalePref);
433        if (lastSelectedScale != null && !lastSelectedScale.equals("")) {
434            try {
435                scaleList.setSelectedItem(lastSelectedScale);
436            } catch (ArrayIndexOutOfBoundsException e) {
437                scaleList.setSelectedIndex(defaultScale);
438            }
439        } else {
440            scaleList.setSelectedIndex(defaultScale);
441        }
442
443        if (scaleList.getSelectedIndex() > -1) {
444            selectedScale = scales[scaleList.getSelectedIndex()];
445        }
446
447        // Listen to selection of scale
448        scaleList.addActionListener(e -> {
449            selectedScale = scales[scaleList.getSelectedIndex()];
450            checkCustomScale();
451        });
452
453
454
455        scaleLabel.setText(Bundle.getMessage("Scale"));
456        scaleLabel.setVisible(true);
457
458        readerLabel.setText(Bundle.getMessage("UnknownReader"));
459        readerLabel.setVisible(true);
460
461        scalePanel.add(scaleLabel);
462        scalePanel.add(scaleList);
463        scalePanel.add(readerLabel);
464
465        // Custom Scale panel to hold the custome scale selection
466        JPanel customScalePanel = new JPanel();
467        customScalePanel.setBorder(BorderFactory.createTitledBorder(
468                BorderFactory.createEtchedBorder(), Bundle.getMessage("CustomScale")));
469        customScalePanel.setLayout(new FlowLayout());
470
471        customScaleLabel.setText("1: ");
472        customScaleLabel.setVisible(true);
473        customScaleField.setVisible(true);
474        try {
475            customScaleField.setText(prefs.getProperty(customScalePref, "customScale").toString());
476        } catch (java.lang.NullPointerException npe) {
477            customScaleField.setText("1");
478        }
479        checkCustomScale();
480        getCustomScale();
481
482        // Let user press return to enter custom scale
483        customScaleField.addActionListener(e -> getCustomScale());
484
485        customScalePanel.add(customScaleLabel);
486        customScalePanel.add(customScaleField);
487
488        basicPane.add(scalePanel);
489        basicPane.add(customScalePanel);
490
491        //</editor-fold>
492        //<editor-fold defaultstate="collapsed" desc="Mode Panel">
493        // Mode panel for selection of profile mode
494        JPanel modePanel = new JPanel();
495        modePanel.setBorder(BorderFactory.createTitledBorder(
496                BorderFactory.createEtchedBorder(), Bundle.getMessage("SelectMode")));
497        modePanel.setLayout(new FlowLayout());
498
499        // Buttons to select the mode
500        modeGroup.add(progButton);
501        modeGroup.add(mainButton);
502        progButton.setSelected(true);
503        progButton.setToolTipText(Bundle.getMessage("TTProg"));
504        mainButton.setToolTipText(Bundle.getMessage("TTMain"));
505        modePanel.add(progButton);
506        modePanel.add(mainButton);
507
508        // Listen to change of profile mode
509        progButton.addActionListener(e -> {
510            if (((dccServices & PROG) == PROG)) {
511                // Programmer is available to read back CVs
512                readAddressButton.setEnabled(true);
513                statusLabel.setText(Bundle.getMessage("StatProg"));
514            }
515        });
516
517        mainButton.addActionListener(e -> {
518            // no programmer available to read back CVs
519            readAddressButton.setEnabled(false);
520            statusLabel.setText(Bundle.getMessage("StatMain"));
521        });
522        // added to left side later
523
524        //</editor-fold>
525        //<editor-fold defaultstate="collapsed" desc="Speedometer Panel">
526        // Speed panel for the dial or digital speed display
527        JPanel speedPanel = new JPanel();
528        speedPanel.setBorder(BorderFactory.createTitledBorder(
529                BorderFactory.createEtchedBorder(), Bundle.getMessage("MeasuredSpeed")));
530        speedPanel.setLayout(new BoxLayout(speedPanel, BoxLayout.X_AXIS));
531
532        // Display Panel which is a card layout with cards to show
533        // numeric or dial type speed display
534        displayCards.setLayout(new CardLayout());
535
536        // Numeric speed card
537        JPanel numericSpeedPanel = new JPanel();
538        numericSpeedPanel.setLayout(new BoxLayout(numericSpeedPanel, BoxLayout.X_AXIS));
539        Font f = new Font("", Font.PLAIN, 96);
540        speedTextField.setFont(f);
541        speedTextField.setHorizontalAlignment(JTextField.RIGHT);
542        speedTextField.setColumns(3);
543        speedTextField.setText("0.0");
544        speedTextField.setVisible(true);
545        speedTextField.setToolTipText(Bundle.getMessage("SpeedHere"));
546        numericSpeedPanel.add(speedTextField);
547
548        // Dial speed card
549        JPanel dialSpeedPanel = new JPanel();
550        dialSpeedPanel.setLayout(new BoxLayout(dialSpeedPanel, BoxLayout.X_AXIS));
551        dialSpeedPanel.add(speedoDialDisplay);
552        speedoDialDisplay.update(0.0F);
553
554        // Add cards to panel
555        displayCards.add(dialSpeedPanel, "DIAL");
556        displayCards.add(numericSpeedPanel, "NUMERIC");
557        CardLayout cl = (CardLayout) displayCards.getLayout();
558        cl.show(displayCards, "DIAL");
559
560        // button panel
561        JPanel buttonPanel = new JPanel();
562        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
563        speedGroup.add(mphButton);
564        speedGroup.add(kphButton);
565        mphButton.setToolTipText(Bundle.getMessage("TTDisplayMPH"));
566        kphButton.setToolTipText(Bundle.getMessage("TTDisplayKPH"));
567        mphButton.setSelected(!prefs.getSimplePreferenceState(speedUnitsKphPref));
568        kphButton.setSelected(prefs.getSimplePreferenceState(speedUnitsKphPref));
569        displayGroup.add(numButton);
570        displayGroup.add(dialButton);
571        numButton.setToolTipText(Bundle.getMessage("TTDisplayNumeric"));
572        dialButton.setToolTipText(Bundle.getMessage("TTDisplayDial"));
573        numButton.setSelected(!prefs.getSimplePreferenceState(dialTypePref));
574        dialButton.setSelected(prefs.getSimplePreferenceState(dialTypePref));
575        buttonPanel.add(mphButton);
576        buttonPanel.add(kphButton);
577        buttonPanel.add(numButton);
578        buttonPanel.add(dialButton);
579
580        speedPanel.add(displayCards);
581        speedPanel.add(buttonPanel);
582
583        // Listen to change of units, convert current average and update display
584        mphButton.addActionListener(e -> setUnits());
585        kphButton.addActionListener(e -> setUnits());
586
587        // Listen to change of display
588        numButton.addActionListener(e -> setDial());
589        dialButton.addActionListener(e -> setDial());
590
591        basicPane.add(speedPanel);
592
593        //</editor-fold>
594        //<editor-fold defaultstate="collapsed" desc="Speed Profiling and Speed Matching Panels">
595        /*
596         * Pane for profiling loco speed curve
597         */
598        JPanel profilePane = new JPanel();
599        profilePane.setLayout(new BorderLayout());
600
601        //<editor-fold defaultstate="collapsed" desc="Address Panel">
602        JPanel addrPane = new JPanel();
603        GridBagLayout gLayout = new GridBagLayout();
604        GridBagConstraints gConstraints = new GridBagConstraints();
605        gConstraints.insets = new Insets(3, 3, 3, 3);
606        Border addrPaneBorder = javax.swing.BorderFactory.createEtchedBorder();
607        TitledBorder addrPaneTitle = javax.swing.BorderFactory.createTitledBorder(addrPaneBorder, Bundle.getMessage("LocoSelection"));
608        addrPane.setLayout(gLayout);
609        addrPane.setBorder(addrPaneTitle);
610
611        setButton = new JButton(Bundle.getMessage("ButtonSet"));
612        setButton.addActionListener(e -> changeOfAddress());
613        addrSelector.setAddress(null);
614
615        rosterBox = new GlobalRosterEntryComboBox();
616        rosterBox.setNonSelectedItem(Bundle.getMessage("NoLocoSelected"));
617        rosterBox.setToolTipText(Bundle.getMessage("TTSelectLocoFromRoster"));
618
619        /*
620         Using an ActionListener didn't select a loco from the ComboBox properly
621         so changed it to a PropertyChangeListener approach modeled on the code
622         in CombinedLocoSelPane class, layoutRosterSelection method, which is known to work.
623         Not sure why the ActionListener didn't work properly, but this fixes the bug
624         */
625        rosterBox.addPropertyChangeListener(RosterEntrySelector.SELECTED_ROSTER_ENTRIES, pce -> {
626            if (!disableRosterBoxActions) { //Have roster box actions been disabled?
627                rosterItemSelected();
628            }
629        });
630
631        readAddressButton.setToolTipText(Bundle.getMessage("ReadLoco"));
632
633        addrPane.add(addrSelector.getCombinedJPanel(), gConstraints);
634        addrPane.add(new JLabel(" "), gConstraints);
635        addrPane.add(setButton, gConstraints);
636        addrPane.add(new JLabel(" "), gConstraints);
637        addrPane.add(rosterBox, gConstraints);
638        addrPane.add(new JLabel(" "), gConstraints);
639        addrPane.add(readAddressButton, gConstraints);
640
641        if (((dccServices & PROG) != PROG) || (mainButton.isSelected())) {
642            // No programming facility so user must enter address
643            addrSelector.setEnabled(false);
644            readAddressButton.setEnabled(false);
645        } else {
646            addrSelector.setEnabled(true);
647            readAddressButton.setEnabled(true);
648        }
649
650        // Listen to read button
651        readAddressButton.addActionListener(e -> readAddress());
652
653        // set up top panel of modePanel and addrPane
654        var topLeftPane = new JPanel();
655        topLeftPane.setLayout(new BorderLayout());
656        topLeftPane.add(modePanel, BorderLayout.NORTH);
657        topLeftPane.add(addrPane, BorderLayout.SOUTH);
658
659        profilePane.add(topLeftPane, BorderLayout.NORTH);
660
661
662        //</editor-fold>
663        //<editor-fold defaultstate="collapsed" desc="Graph and Buttons Panel">
664        // pane to hold the graph
665        spFwd = new DccSpeedProfile(29);       // 28 step plus step 0
666        spRev = new DccSpeedProfile(29);       // 28 step plus step 0
667        spRef = new DccSpeedProfile(29);       // 28 step plus step 0
668        profileGraphPane = new GraphPane(spFwd, spRev, spRef);
669        profileGraphPane.setPreferredSize(new Dimension(600, 300));
670        profileGraphPane.setXLabel(Bundle.getMessage("SpeedStep"));
671        profileGraphPane.setUnitsMph();
672
673        profilePane.add(profileGraphPane, BorderLayout.CENTER);
674
675        // pane to hold the buttons
676        JPanel profileButtonPane = new JPanel();
677        profileButtonPane.setLayout(new FlowLayout());
678        profileButtonPane.add(trackPowerButton);
679        trackPowerButton.setToolTipText(Bundle.getMessage("TTPower"));
680        profileButtonPane.add(startProfileButton);
681        startProfileButton.setToolTipText(Bundle.getMessage("TTStartProfile"));
682        profileButtonPane.add(stopProfileButton);
683        stopProfileButton.setToolTipText(Bundle.getMessage("TTStopProfile"));
684        profileButtonPane.add(exportProfileButton);
685        exportProfileButton.setToolTipText(Bundle.getMessage("TTSaveProfile"));
686        profileButtonPane.add(printProfileButton);
687        printProfileButton.setToolTipText(Bundle.getMessage("TTPrintProfile"));
688        profileButtonPane.add(resetGraphButton);
689        resetGraphButton.setToolTipText(Bundle.getMessage("TTResetGraph"));
690        profileButtonPane.add(loadProfileButton);
691        loadProfileButton.setToolTipText(Bundle.getMessage("TTLoadProfile"));
692
693        // pane to hold the title
694        JPanel profileTitlePane = new JPanel();
695        profileTitlePane.setLayout(new BoxLayout(profileTitlePane, BoxLayout.X_AXIS));
696        //JTextArea profileTitle = new JTextArea("Title: ");
697        //profileTitlePane.add(profileTitle);
698        printTitleText.setToolTipText(Bundle.getMessage("TTPrintTitle"));
699        printTitleText.setText(Bundle.getMessage("TTText1"));
700        profileTitlePane.add(printTitleText);
701
702        // pane to wrap buttons and title
703        JPanel profileSouthPane = new JPanel();
704        profileSouthPane.setLayout(new BoxLayout(profileSouthPane, BoxLayout.Y_AXIS));
705        profileSouthPane.add(profileButtonPane);
706
707        //</editor-fold>
708        //<editor-fold defaultstate="collapsed" desc="Speed Matching Panel">
709        // pane for speed matching
710        speedStep1TargetField.setHorizontalAlignment(JTextField.RIGHT);
711        speedStep1TargetUnit.setPreferredSize(new Dimension(35, 16));
712        speedStep28TargetField.setHorizontalAlignment(JTextField.RIGHT);
713        speedStep28TargetUnit.setPreferredSize(new Dimension(35, 16));
714        speedMatchWarmUpCheckBox.setSelected(true);
715
716        profileSouthPane.add(new JSeparator());
717
718        JPanel speedMatchPane = new JPanel();
719        speedMatchPane.setLayout(new FlowLayout());
720        speedMatchPane.add(speedStep1TargetLabel);
721        speedMatchPane.add(speedStep1TargetField);
722        speedMatchPane.add(speedStep1TargetUnit);
723        speedMatchPane.add(speedStep28TargetLabel);
724        speedMatchPane.add(speedStep28TargetField);
725        speedMatchPane.add(speedStep28TargetUnit);
726        speedMatchPane.add(speedMatchWarmUpCheckBox);
727        speedMatchPane.add(speedMatchButton);
728        profileSouthPane.add(speedMatchPane);
729
730        profileSouthPane.add(profileTitlePane);
731
732        profilePane.add(profileSouthPane, BorderLayout.SOUTH);
733
734        //</editor-fold>
735        //<editor-fold defaultstate="collapsed" desc="Control Panel">
736        // Pane to hold controls
737        JPanel profileControlPane = new JPanel();
738        profileControlPane.setLayout(new BoxLayout(profileControlPane, BoxLayout.Y_AXIS));
739        dirFwdButton.setSelected(true);
740        dirFwdButton.setToolTipText(Bundle.getMessage("TTMeasFwd"));
741        dirRevButton.setToolTipText(Bundle.getMessage("TTMeasRev"));
742        dirFwdButton.setForeground(Color.RED);
743        dirRevButton.setForeground(Color.BLUE);
744        profileControlPane.add(dirFwdButton);
745        profileControlPane.add(dirRevButton);
746        toggleGridButton.setSelected(true);
747        profileControlPane.add(toggleGridButton);
748        profileGraphPane.showGrid(toggleGridButton.isSelected());
749
750        profilePane.add(profileControlPane, BorderLayout.EAST);
751
752        //</editor-fold>
753        //<editor-fold defaultstate="collapsed" desc="Speed Profiling and Speed Matching Button Handlers">
754        // Listen to track Power button
755        trackPowerButton.addActionListener(e -> trackPower());
756
757        // Listen to start profile button
758        startProfileButton.addActionListener(e -> {
759            getCustomScale();
760            startProfile();
761        });
762
763        // Listen to stop profile button
764        stopProfileButton.addActionListener(e -> stopProfileAndSpeedMatch());
765
766        // Listen to speed match button
767        speedMatchButton.addActionListener(e -> {
768            if ((speedMatchState == SpeedMatchState.IDLE) && (profileState == ProfileState.IDLE)) {
769                getCustomScale();
770                speedStep1Target = Integer.parseInt(speedStep1TargetField.getText());
771                speedStep28Target = Integer.parseInt(speedStep28TargetField.getText());
772
773                if (mphButton.isSelected()) {
774                    speedStep1Target = Speed.mphToKph(speedStep1Target);
775                    speedStep28Target = Speed.mphToKph(speedStep28Target);
776                }
777
778                startSpeedMatch();
779            } else {
780                stopProfileAndSpeedMatch();
781            }
782        });
783
784        // Listen to grid button
785        toggleGridButton.addActionListener(e -> {
786            profileGraphPane.showGrid(toggleGridButton.isSelected());
787            profileGraphPane.repaint();
788        });
789
790        // Listen to export button
791        exportProfileButton.addActionListener(e -> {
792            if (dirFwdButton.isSelected() && dirRevButton.isSelected()) {
793                DccSpeedProfile[] sp = {spFwd, spRev};
794                DccSpeedProfile.export(sp, locomotiveAddress.getNumber(), profileGraphPane.getUnits());
795            } else if (dirFwdButton.isSelected()) {
796                DccSpeedProfile.export(spFwd, locomotiveAddress.getNumber(), "fwd", profileGraphPane.getUnits());
797            } else if (dirRevButton.isSelected()) {
798                DccSpeedProfile.export(spRev, locomotiveAddress.getNumber(), "rev", profileGraphPane.getUnits());
799            }
800        });
801
802        // Listen to print button
803        printProfileButton.addActionListener(e -> profileGraphPane.printProfile(printTitleText.getText()));
804
805        // Listen to reset graph button
806        resetGraphButton.addActionListener(e -> {
807            spFwd.clear();
808            spRev.clear();
809            spRef.clear();
810            speedoDialDisplay.reset();
811            profileGraphPane.repaint();
812        });
813
814        // Listen to Load Reference button
815        loadProfileButton.addActionListener(e -> {
816            spRef.clear();
817            int response = spRef.importDccProfile(profileGraphPane.getUnits());
818            if (response == -1) {
819                statusLabel.setText(Bundle.getMessage("StatFileError"));
820            } else {
821                statusLabel.setText(Bundle.getMessage("StatFileSuccess"));
822            }
823            profileGraphPane.repaint();
824        });
825
826        //</editor-fold>
827        //</editor-fold>
828        /*
829         * Create the tabbed pane and add the panes
830         */
831        JPanel tabbedPane = new JPanel();
832        tabbedPane.setLayout(new BoxLayout(tabbedPane, BoxLayout.X_AXIS));
833        // make basic panel
834        tabbedPane.add(basicPane);
835
836        if (((dccServices & THROTTLE) == THROTTLE)
837                || ((dccServices & COMMAND) == COMMAND)) {
838            tabbedPane.add(profilePane);
839        } else {
840            log.info("{} Connection:{}", Bundle.getMessage("StatNoDCC"), _memo.getUserName());
841            statusLabel.setText(Bundle.getMessage("StatNoDCC"));
842        }
843
844        // add help menu to window
845        addHelpMenu("package.jmri.jmrix.bachrus.SpeedoConsoleFrame", true);
846
847        // Create a wrapper with a status line and add the main content
848        JPanel statusWrapper = new JPanel();
849        statusWrapper.setLayout(new BorderLayout());
850        JPanel statusPanel = new JPanel();
851        statusPanel.setLayout(new BorderLayout());
852        statusPanel.add(statusLabel, BorderLayout.WEST);
853
854        statusPanel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
855        statusWrapper.add(tabbedPane, BorderLayout.CENTER);
856        statusWrapper.add(statusPanel, BorderLayout.SOUTH);
857
858        getContentPane().add(statusWrapper);
859        //</editor-fold>
860
861        // connect to TrafficController
862        tc = _memo.getTrafficController();
863        tc.addSpeedoListener(this);
864
865        setUnits();
866        setDial();
867
868        // pack for display
869        pack();
870
871        speedoDialDisplay.scaleFace();
872    }
873
874    //<editor-fold defaultstate="collapsed" desc="Speed Reader and Calculations">
875    /**
876     * Handle "replies" from the hardware. In fact, all the hardware does is
877     * send a constant stream of unsolicited speed updates.
878     *
879     * @param l the reply to handle
880     */
881    @Override
882    public synchronized void reply(SpeedoReply l) {  // receive a reply message and log it
883        //log.debug("Speedo reply " + l.toString());
884        count = l.getCount();
885        series = l.getSeries();
886        if (count > 0) {
887            switch (series) {
888                case 4:
889                    circ = 12.5664F;
890                    readerLabel.setText(Bundle.getMessage("Reader40"));
891                    break;
892                case 5:
893                    circ = 18.8496F;
894                    readerLabel.setText(Bundle.getMessage("Reader50"));
895                    break;
896                case 6:
897                    circ = 50.2655F;
898                    readerLabel.setText(Bundle.getMessage("Reader60"));
899                    break;
900                case 103:
901                    circ = (float) ((5.95+0.9) * Math.PI);
902                    readerLabel.setText(Bundle.getMessage("Reader103"));
903                    break;
904                default:
905                    speedTextField.setText(Bundle.getMessage("ReaderErr"));
906                    log.error("Invalid reader type");
907                    break;
908            }
909
910            // Update speed
911            calcSpeed();
912        }
913        if (timerRunning == false) {
914            // first reply starts the timer
915            startReplyTimer();
916            startDisplayTimer();
917            startFastDisplayTimer();
918            timerRunning = true;
919        } else {
920            // subsequent replies restart it
921            replyTimer.restart();
922        }
923    }
924
925    /**
926     * Calculates the scale speed in KPH
927     */
928    protected void calcSpeed() {
929        float thisScale = (selectedScale == -1) ? customScale : selectedScale;
930        if (series == 103) {
931            // KPF-Zeller
932            // calculate kph: r/sec * circumference converted to hours and kph in scaleFace()
933            sampleSpeed = (float) ( (count/8.) * circ * 3600 / 1.0E6 * thisScale * speedTestScaleFactor);
934            // data arrives at constant rate, so we don't average nor switch range
935            avSpeed = sampleSpeed;
936            log.debug("New KPF-Zeller sample: {} Average: {}", sampleSpeed, avSpeed);
937
938        } else if (series > 0 && series <= 6) {
939            // Bachrus
940            // Scale the data and calculate kph
941            try {
942                freq = 1500000 / count;
943                sampleSpeed = (freq / 24) * circ * thisScale * 3600 / 1000000 * speedTestScaleFactor;
944            } catch (ArithmeticException ae) {
945                log.error("Exception calculating sampleSpeed", ae);
946            }
947            avFn(sampleSpeed);
948            log.debug("New Bachrus sample: {} Average: {}", sampleSpeed, avSpeed);
949            log.debug("Acc: {} range: {}", acc, range);
950            switchRange();
951        }
952    }
953
954    /**
955     * Calculates the average speed using a filter
956     *
957     * @param speed the speed of the latest interation
958     */
959    protected void avFn(float speed) {
960        // Averaging function used for speed is
961        // S(t) = S(t-1) - [S(t-1)/N] + speed
962        // A(t) = S(t)/N
963        //
964        // where S is an accumulator, N is the length of the filter (i.e.,
965        // the number of samples included in the rolling average), and A is
966        // the result of the averaging function.
967        //
968        // Re-arranged
969        // S(t) = S(t-1) - A(t-1) + speed
970        // A(t) = S(t)/N
971        acc = acc - avSpeed + speed;
972        avSpeed = acc / filterLength[range];
973    }
974
975    /**
976     * Clears the average speed calculation
977     */
978    protected void avClr() {
979        acc = 0;
980        avSpeed = 0;
981    }
982
983    /**
984     * Switches the filter used for averaging speed based on the measured speed
985     */
986    protected void switchRange() {
987        // When we switch range we must compensate the current accumulator
988        // value for the longer filter.
989        switch (range) {
990            case 1:
991                if (sampleSpeed > RANGE1HI) {
992                    range++;
993                    acc = acc * filterLength[2] / filterLength[1];
994                }
995                break;
996            case 2:
997                if (sampleSpeed < RANGE2LO) {
998                    range--;
999                    acc = acc * filterLength[1] / filterLength[2];
1000                } else if (sampleSpeed > RANGE2HI) {
1001                    range++;
1002                    acc = acc * filterLength[3] / filterLength[2];
1003                }
1004                break;
1005            case 3:
1006                if (sampleSpeed < RANGE3LO) {
1007                    range--;
1008                    acc = acc * filterLength[2] / filterLength[3];
1009                } else if (sampleSpeed > RANGE3HI) {
1010                    range++;
1011                    acc = acc * filterLength[4] / filterLength[3];
1012                }
1013                break;
1014            case 4:
1015                if (sampleSpeed < RANGE4LO) {
1016                    range--;
1017                    acc = acc * filterLength[3] / filterLength[4];
1018                }
1019                break;
1020            default:
1021                log.debug("range {} unsupported, range unchanged.", range);
1022        }
1023    }
1024
1025    /**
1026     * Displays the speed in the SpeedoConsoleFrame's digital/analog speedometer
1027     */
1028    protected void showSpeed() {
1029        float speedForText = currentSpeed;
1030        if (mphButton.isSelected()) {
1031            speedForText = Speed.kphToMph(speedForText);
1032        }
1033        if (series > 0) {
1034            if ((currentSpeed < 0) || (currentSpeed > 999)) {
1035                log.error("Calculated speed out of range: {}", currentSpeed);
1036                speedTextField.setText("999");
1037            } else {
1038                // Final smoothing as applied by Bachrus Console. Don't update display
1039                // unless speed has changed more than 2%
1040                if ((currentSpeed > oldSpeed * 1.02) || (currentSpeed < oldSpeed * 0.98)) {
1041                    speedTextField.setText(MessageFormat.format("{0,number,##0.0}", speedForText));
1042                    speedTextField.setHorizontalAlignment(JTextField.RIGHT);
1043                    oldSpeed = currentSpeed;
1044                    speedoDialDisplay.update(currentSpeed);
1045                }
1046            }
1047        }
1048    }
1049
1050    //</editor-fold>
1051    //<editor-fold defaultstate="collapsed" desc="Speedometer Helper Functions">
1052    /**
1053     * Check if custom scale selected and enable the custom scale entry field.
1054     */
1055    protected void checkCustomScale() {
1056        if (selectedScale == -1) {
1057            customScaleField.setEnabled(true);
1058        } else {
1059            customScaleField.setEnabled(false);
1060        }
1061    }
1062
1063    /**
1064     * Set the speed to be displayed as a dial or numeric
1065     */
1066    protected void setDial() {
1067        CardLayout cl = (CardLayout) displayCards.getLayout();
1068        if (numButton.isSelected()) {
1069            display = DisplayType.NUMERIC;
1070            cl.show(displayCards, "NUMERIC");
1071        } else {
1072            display = DisplayType.DIAL;
1073            cl.show(displayCards, "DIAL");
1074        }
1075    }
1076
1077    /**
1078     * Set the displays to mile per hour or kilometers per hour
1079     */
1080    protected void setUnits() {
1081        if (mphButton.isSelected()) {
1082            profileGraphPane.setUnitsMph();
1083            speedStep1TargetUnit.setText(Bundle.getMessage("lblMPH"));
1084            speedStep28TargetUnit.setText(Bundle.getMessage("lblMPH"));
1085        } else {
1086            profileGraphPane.setUnitsKph();
1087            speedStep1TargetUnit.setText(Bundle.getMessage("lblKPH"));
1088            speedStep28TargetUnit.setText(Bundle.getMessage("lblKPH"));
1089        }
1090        profileGraphPane.repaint();
1091        if (mphButton.isSelected()) {
1092            speedoDialDisplay.setUnitsMph();
1093        } else {
1094            speedoDialDisplay.setUnitsKph();
1095        }
1096        speedoDialDisplay.update(currentSpeed);
1097        speedoDialDisplay.repaint();
1098    }
1099
1100    /**
1101     * Validate the users custom scale entry.
1102     */
1103    protected void getCustomScale() {
1104        if (selectedScale == -1) {
1105            try {
1106                customScale = Integer.parseUnsignedInt(customScaleField.getText());
1107            } catch (NumberFormatException ex) {
1108                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CustomScaleDialog"),
1109                        Bundle.getMessage("CustomScaleTitle"), JmriJOptionPane.ERROR_MESSAGE);
1110            }
1111        }
1112    }
1113
1114    //</editor-fold>
1115    //<editor-fold defaultstate="collapsed" desc="Address Helper Functions">
1116    /**
1117     * Handle changing/setting the address.
1118     */
1119    private synchronized void changeOfAddress() {
1120        if (addrSelector.getAddress() != null) {
1121            locomotiveAddress = addrSelector.getAddress();
1122            setTitle();
1123        } else {
1124            locomotiveAddress = new DccLocoAddress(0, true);
1125        }
1126    }
1127
1128    /**
1129     * Set the RosterEntry for this throttle.
1130     *
1131     * @param entry roster entry selected for throttle
1132     */
1133    public void setRosterEntry(RosterEntry entry) {
1134        rosterBox.setSelectedItem(entry);
1135        addrSelector.setAddress(entry.getDccLocoAddress());
1136        rosterEntry = entry;
1137        changeOfAddress();
1138    }
1139
1140    /**
1141     * Called when a RosterEntry is selected
1142     */
1143    private void rosterItemSelected() {
1144        if (rosterBox.getSelectedRosterEntries().length != 0) {
1145            setRosterEntry(rosterBox.getSelectedRosterEntries()[0]);
1146        }
1147    }
1148
1149    //</editor-fold>
1150    //<editor-fold defaultstate="collapsed" desc="Power Manager Helper Functions">
1151    /**
1152     * {@inheritDoc}
1153     *
1154     * Handles property changes from the power manager.
1155     */
1156    @Override
1157    public void propertyChange(PropertyChangeEvent evt) {
1158        setPowerStatus();
1159    }
1160
1161    /**
1162     * Switches the track power on or off
1163     */
1164    private void setPowerStatus() {
1165        if (pm == null) {
1166            return;
1167        }
1168        if (pm.getPower() == PowerManager.ON) {
1169            trackPowerButton.setText(Bundle.getMessage("PowerDown"));
1170            //statusLabel.setText(Bundle.getMessage("StatTOn"));
1171        } else if (pm.getPower() == PowerManager.OFF) {
1172            trackPowerButton.setText(Bundle.getMessage("PowerUp"));
1173            //statusLabel.setText(Bundle.getMessage("StatTOff"));
1174        }
1175    }
1176
1177    /**
1178     * Called when the track power button is clicked to turn on or off track
1179     * power Allows user to power up and give time for sound decoder startup
1180     * sequence before running a profile
1181     */
1182    protected void trackPower() {
1183        try {
1184            if (pm.getPower() != PowerManager.ON) {
1185                pm.setPower(PowerManager.ON);
1186            } else {
1187                stopProfileAndSpeedMatch();
1188                pm.setPower(PowerManager.OFF);
1189            }
1190        } catch (JmriException e) {
1191            log.error("Exception during power on: {}", e.toString());
1192        }
1193    }
1194    //</editor-fold>
1195    //<editor-fold defaultstate="collapsed" desc="Speed Matching">
1196    javax.swing.Timer speedMatchTimer = null;
1197
1198    /**
1199     * Sets up the speed match timer by setting the throttle direction and
1200     * speed, clearing the speed match error, and setting the timer initial
1201     * delay (timer does not auto-repeat for accuracy)
1202     *
1203     * @param isForward    - throttle direction - true for forward, false for
1204     *                     reverse
1205     * @param speedStep    - throttle speed step
1206     * @param initialDelay - initial delay for the timer in milliseconds
1207     */
1208    protected void setupSpeedMatchTimer(boolean isForward, int speedStep, int initialDelay) {
1209        throttle.setIsForward(isForward);
1210        throttle.setSpeedSetting(speedStep * throttleIncrement);
1211        speedMatchError = 0;
1212        speedMatchTimer.setInitialDelay(initialDelay);
1213    }
1214
1215    /**
1216     * Sets the PID controller's speed match error for speed matching
1217     *
1218     * @param speedTarget - target speed in KPH
1219     */
1220    protected void setSpeedMatchError(float speedTarget) {
1221        speedMatchError = speedTarget - currentSpeed;
1222    }
1223
1224    /**
1225     * Gets the next value to try for speed matching using a PID controller
1226     *
1227     * @param lastValue - the last vStart or vHigh value tried
1228     * @return the next value to try for speed matching (1-255 inclusive)
1229     */
1230    protected int getNextSpeedMatchValue(int lastValue) {
1231        speedMatchIntegral += speedMatchError;
1232        speedMatchDerivative = speedMatchError - lastSpeedMatchError;
1233
1234        int value = (lastValue + Math.round((kP * speedMatchError) + (kI * speedMatchIntegral) + (kD * speedMatchDerivative)));
1235
1236        if (value > 255) {
1237            value = 255;
1238        } else if (value < 1) {
1239            value = 1;
1240        }
1241
1242        return value;
1243    }
1244
1245    /**
1246     * Starts the auto speed matching process
1247     */
1248    protected void startSpeedMatch() {
1249        DccLocoAddress dccLocoAddress = addrSelector.getAddress();
1250
1251        //Validate require variables
1252        if (speedStep1Target < 1) {
1253            statusLabel.setText(Bundle.getMessage("StatInvalidSpeedStep1"));
1254            log.error("Attempt to speed match to invalid speed step 1 target speed");
1255            return;
1256        }
1257        if (speedStep28Target <= speedStep1Target) {
1258            statusLabel.setText(Bundle.getMessage("StatInvalidSpeedStep28"));
1259            log.error("Attempt to speed match to invalid speed step 28 target speed");
1260            return;
1261        }
1262        if (locomotiveAddress.getNumber() <= 0) {
1263            statusLabel.setText(Bundle.getMessage("StatInvalidDCCAddress"));
1264            log.error("Attempt to speed match loco address 0");
1265            return;
1266        }
1267
1268        //start speed matching
1269        if ((speedMatchState == SpeedMatchState.IDLE) && (profileState == ProfileState.IDLE)) {
1270            speedMatchState = SpeedMatchState.WAIT_FOR_THROTTLE;
1271            speedMatchButton.setText(Bundle.getMessage("btnStopSpeedMatch"));
1272
1273            //reset member variables
1274            vStart = 1;
1275            vHigh = 255;
1276            reverseTrim = 128;
1277            lastVStart = vStart;
1278            lastVHigh = vHigh;
1279            lastReverseTrim = reverseTrim;
1280
1281            //get OPS MODE Programmer
1282            if (InstanceManager.getNullableDefault(AddressedProgrammerManager.class) != null) {
1283                if (InstanceManager.getDefault(AddressedProgrammerManager.class).isAddressedModePossible(dccLocoAddress)) {
1284                    ops_mode_prog = InstanceManager.getDefault(AddressedProgrammerManager.class).getAddressedProgrammer(dccLocoAddress);
1285                }
1286            }
1287
1288            //start speed match timer
1289            speedMatchTimer = new javax.swing.Timer(4000, e -> speedMatchTimeout());
1290            speedMatchTimer.setRepeats(false); //timer is used without repeats to improve time accuracy when changing the delay
1291
1292            //request a throttle
1293            statusLabel.setText(Bundle.getMessage("StatReqThrottle"));
1294            speedMatchTimer.start();
1295            log.info("Requesting Throttle");
1296            boolean requestOK = InstanceManager.throttleManagerInstance().requestThrottle(locomotiveAddress, this, true);
1297            if (!requestOK) {
1298                log.error("Loco Address in use, throttle request failed.");
1299                statusLabel.setText(Bundle.getMessage("StatAddressInUse"));
1300            }
1301        }
1302    }
1303
1304    /**
1305     * Timer timeout handler for the speed match timer
1306     */
1307    protected synchronized void speedMatchTimeout() {
1308        log.debug("speedMatchTimeout in states {} {} {}", speedMatchState, speedMatchSetupState, progState);
1309        switch (speedMatchState) {
1310            case WAIT_FOR_THROTTLE:
1311                tidyUp();
1312                log.error("Timeout waiting for throttle");
1313                statusLabel.setText(Bundle.getMessage("StatusTimeout"));
1314                break;
1315
1316            case SETUP:
1317                //setup the decoder for speed matching
1318                switch (speedMatchSetupState) {
1319                    case MOMENTUM_ACCEL_READ:
1320                        //grab the current acceleration momentum value for later restoration (CV 3)
1321                        if (progState == ProgState.IDLE) {
1322                            readMomentumAccel();
1323                            speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_DECEL_READ;
1324                        }
1325                        break;
1326
1327                    case MOMENTUM_DECEL_READ:
1328                        //grab the current deceleration momentum value for later restoration (CV 4)
1329                        if (progState == ProgState.IDLE) {
1330                            readMomentumDecel();
1331                            speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_ACCEL_WRITE;
1332                        }
1333                        break;
1334
1335                    case MOMENTUM_ACCEL_WRITE:
1336                        //set acceleration momentum to 0 (CV 3)
1337                        if (progState == ProgState.IDLE) {
1338                            writeMomentumAccel(0);
1339                            speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_DECEL_WRITE;
1340                            speedMatchTimer.setInitialDelay(5000);
1341                        }
1342                        break;
1343
1344                    case MOMENTUM_DECEL_WRITE:
1345                        //set deceleration mementum to 0 (CV 4)
1346                        if (progState == ProgState.IDLE) {
1347                            writeMomentumDecel(0);
1348                            speedMatchSetupState = SpeedMatchSetupState.VSTART;
1349                            speedMatchTimer.setInitialDelay(1500);
1350                        }
1351                        break;
1352
1353                    case VSTART:
1354                        //set vStart to 1 (CV 2 - also sets vMid CV 6 to halway between vStart and vHigh)
1355                        if (progState == ProgState.IDLE) {
1356                            writeVStart();
1357                            speedMatchSetupState = SpeedMatchSetupState.VHIGH;
1358                        }
1359                        break;
1360
1361                    case VHIGH:
1362                        //set vHigh to 255 (CV 5 - also sets vMid CV 6 to halway between vStart and vHigh)
1363                        if (progState == ProgState.IDLE) {
1364                            writeVHigh();
1365                            speedMatchSetupState = SpeedMatchSetupState.FORWARD_TRIM;
1366                        }
1367                        break;
1368
1369                    case FORWARD_TRIM:
1370                        //set forward trim to 128 (CV 66)
1371                        if (progState == ProgState.IDLE) {
1372                            writeForwardTrim(128);
1373                            speedMatchSetupState = SpeedMatchSetupState.REVERSE_TRIM;
1374                        }
1375                        break;
1376
1377                    case REVERSE_TRIM:
1378                        //set revers trim to 128 (CV 95)
1379                        if (progState == ProgState.IDLE) {
1380                            writeReverseTrim(128);
1381                            speedMatchSetupState = SpeedMatchSetupState.BEGIN_SPEED_MATCH;
1382                        }
1383                        break;
1384
1385                    case BEGIN_SPEED_MATCH:
1386                        //start warming up or speed matching
1387                        if (progState == ProgState.IDLE) {
1388                            speedMatchSetupState = SpeedMatchSetupState.IDLE;
1389                            if (speedMatchWarmUpCheckBox.isSelected()) {
1390                                speedMatchState = SpeedMatchState.FORWARD_WARM_UP;
1391                            } else {
1392                                speedMatchState = SpeedMatchState.FORWARD_SPEED_MATCH_STEP_1;
1393                            }
1394                            setupSpeedMatchTimer(true, 0, 5000);
1395                            speedMatchDuration = 0;
1396                        }
1397                        break;
1398
1399                    default:
1400                        log.warn("Unhandled speed match setup state: {}", speedMatchSetupState);
1401                        break;
1402                }
1403                break;
1404
1405            case FORWARD_WARM_UP:
1406                //Run for SPEEDMATCHWARMUPTIME seconds at high speed forward
1407                statusLabel.setText(Bundle.getMessage("StatForwardWarmUp", SPEEDMATCHWARMUPTIME - speedMatchDuration));
1408
1409                if (speedMatchDuration >= SPEEDMATCHWARMUPTIME) {
1410                    speedMatchState = SpeedMatchState.FORWARD_SPEED_MATCH_STEP_1;
1411                    setupSpeedMatchTimer(true, 0, 5000);
1412                    speedMatchDuration = 0;
1413                    speedMatchTimer.start();
1414                } else {
1415                    setupSpeedMatchTimer(true, 28, 5000);
1416                    speedMatchDuration += 5;
1417                }
1418                break;
1419
1420            case FORWARD_SPEED_MATCH_STEP_1:
1421                //Use PID Controller to adjust vStart (and VMid) to achieve desired speed
1422                if (progState == ProgState.IDLE) {
1423                    if (speedMatchDuration == 0) {
1424                        statusLabel.setText(Bundle.getMessage("StatSettingSpeedStep1"));
1425                        setupSpeedMatchTimer(true, 1, 15000);
1426                        speedMatchDuration = 1;
1427                    } else {
1428                        setSpeedMatchError(speedStep1Target);
1429
1430                        if ((speedMatchError < 0.5) && (speedMatchError > -0.5)) {
1431                            speedMatchState = SpeedMatchState.FORWARD_SPEED_MATCH_STEP_28;
1432                            setupSpeedMatchTimer(true, 0, 8000);
1433                            speedMatchDuration = 0;
1434                        } else {
1435                            vStart = getNextSpeedMatchValue(lastVStart);
1436
1437                            if (((lastVStart == 1) || (lastVStart == 255)) && (vStart == lastVStart)) {
1438                                statusLabel.setText(Bundle.getMessage("StatSetSpeedStep1Fail"));
1439                                log.debug("Unable to achieve desired speed at Speed Step 1");
1440                                tidyUp();
1441                            } else {
1442                                lastVStart = vStart;
1443                                writeVStart();
1444                            }
1445                            speedMatchTimer.setInitialDelay(8000);
1446                        }
1447                    }
1448                }
1449                break;
1450
1451            case FORWARD_SPEED_MATCH_STEP_28:
1452                //Use PID Controller llogic to adjust vHigh (and vMid) to achieve desired speed
1453                if (progState == ProgState.IDLE) {
1454                    if (speedMatchDuration == 0) {
1455                        statusLabel.setText(Bundle.getMessage("StatSettingSpeedStep28"));
1456                        setupSpeedMatchTimer(true, 28, 15000);
1457                        speedMatchDuration = 1;
1458                    } else {
1459                        setSpeedMatchError(speedStep28Target);
1460                        log.info("forward speed error is {} with vHigh {}", speedMatchError, vHigh);
1461
1462                        if ((speedMatchError < 0.5) && (speedMatchError > -0.5)) {
1463                            if (speedMatchWarmUpCheckBox.isSelected()) {
1464                                speedMatchState = SpeedMatchState.REVERSE_WARM_UP;
1465                            } else {
1466                                speedMatchState = SpeedMatchState.REVERSE_SPEED_MATCH_TRIM;
1467                            }
1468                            setupSpeedMatchTimer(false, 0, 5000);
1469                            speedMatchDuration = 0;
1470                        } else {
1471                            log.info("  setting Vhigh {} was {}", vHigh, lastVHigh);
1472                            vHigh = getNextSpeedMatchValue(lastVHigh);
1473
1474                            if (((lastVHigh == 1) || (lastVHigh == 255)) && (vHigh == lastVHigh)) {
1475                                statusLabel.setText(Bundle.getMessage("StatSetSpeedStep28Fail"));
1476                                log.debug("Unable to achieve desired speed at Speed Step 28");
1477                                tidyUp();
1478                            } else {
1479                                lastVHigh = vHigh;
1480                                writeVHigh();
1481                            }
1482                            speedMatchTimer.setInitialDelay(8000);
1483                        }
1484                    }
1485                }
1486                break;
1487
1488            case REVERSE_WARM_UP:
1489                //Run for SPEEDMATCHWARMUPTIME seconds at high speed reverse
1490                statusLabel.setText(Bundle.getMessage("StatReverseWarmUp", SPEEDMATCHWARMUPTIME - speedMatchDuration));
1491
1492                if (speedMatchDuration >= SPEEDMATCHWARMUPTIME) {
1493                    speedMatchState = SpeedMatchState.REVERSE_SPEED_MATCH_TRIM;
1494                } else {
1495                    speedMatchDuration += 5;
1496                }
1497                setupSpeedMatchTimer(false, 28, 5000);
1498                break;
1499
1500            case REVERSE_SPEED_MATCH_TRIM:
1501                //Use PID controller logic to adjust reverse trim until high speed reverse speed matches forward
1502                if (progState == ProgState.IDLE) {
1503                    if (speedMatchDuration == 0) {
1504                        statusLabel.setText(Bundle.getMessage("StatSettingReverseTrim"));
1505                        setupSpeedMatchTimer(false, 28, 15000);
1506                        speedMatchDuration = 1;
1507                    } else {
1508                        setSpeedMatchError(speedStep28Target);
1509
1510                        if ((speedMatchError < 0.5) && (speedMatchError > -0.5)) {
1511                            // done
1512                            // next step depends on programming on main vs programming track
1513                            if (mainButton.isSelected()) {
1514                                log.debug("ending by calling tidyup()");
1515                                tidyUp();
1516                            } else {
1517                                log.debug("ending by going to RESTORE_MOMENTUM");
1518                                speedMatchState = SpeedMatchState.RESTORE_MOMENTUM;
1519                                speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_ACCEL_WRITE;
1520                                setupSpeedMatchTimer(false, 0, 1500);
1521                                speedMatchDuration = 0;
1522                            }
1523                        } else {
1524                            reverseTrim = getNextSpeedMatchValue(lastReverseTrim);
1525                            log.info("setting reverse trim {} was {}", reverseTrim, lastReverseTrim);
1526
1527                            if (((lastReverseTrim == 1) || (lastReverseTrim == 255)) && (reverseTrim == lastReverseTrim)) {
1528                                statusLabel.setText(Bundle.getMessage("StatSetReverseTripFail"));
1529                                log.debug("Unable to trim reverse to match forward");
1530                                tidyUp();
1531                            } else {
1532                                lastReverseTrim = reverseTrim;
1533                                writeReverseTrim(reverseTrim);
1534                            }
1535                            speedMatchTimer.setInitialDelay(8000);
1536                        }
1537                    }
1538                }
1539                break;
1540
1541            case RESTORE_MOMENTUM:
1542                //restore momentum CVs
1543                switch (speedMatchSetupState) {
1544                    case MOMENTUM_ACCEL_WRITE:
1545                        //restore acceleration momentum (CV 3)
1546                        if (progState == ProgState.IDLE) {
1547                            writeMomentumAccel(oldMomentumAccel);
1548                            speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_DECEL_WRITE;
1549                        }
1550                        break;
1551
1552                    case MOMENTUM_DECEL_WRITE:
1553                        //restore deceleration mumentum (CV 4)
1554                        if (progState == ProgState.IDLE) {
1555                            writeMomentumDecel(oldMomentumDecel);
1556                            speedMatchSetupState = SpeedMatchSetupState.IDLE;
1557                        }
1558                        break;
1559
1560                    case IDLE:
1561                        //wrap everything up
1562                        if (progState == ProgState.IDLE) {
1563                            tidyUp();
1564                            statusLabel.setText(Bundle.getMessage("StatSpeedMatchComplete"));
1565                        }
1566                        break;
1567
1568                    default:
1569                        log.warn("Unhandled speed match cleanup state: {}", speedMatchSetupState);
1570                }
1571                break;
1572
1573            default:
1574                tidyUp();
1575                log.error("Unexpected speed match timeout");
1576                break;
1577        }
1578
1579        if (speedMatchState != SpeedMatchState.IDLE) {
1580            speedMatchTimer.start();
1581        }
1582    }
1583    //</editor-fold>
1584    //<editor-fold defaultstate="collapsed" desc="Speed Profiling">
1585    javax.swing.Timer profileTimer = null;
1586
1587    /**
1588     * Start the speed profiling process
1589     */
1590    protected synchronized void startProfile() {
1591        if (locomotiveAddress.getNumber() > 0) {
1592            if (dirFwdButton.isSelected() || dirRevButton.isSelected()) {
1593                if ((speedMatchState == SpeedMatchState.IDLE) && (profileState == ProfileState.IDLE)) {
1594                    profileTimer = new javax.swing.Timer(4000, e -> profileTimeout());
1595                    profileTimer.setRepeats(false);
1596                    // Request a throttle
1597                    profileState = ProfileState.WAIT_FOR_THROTTLE;
1598                    // Request a throttle
1599                    statusLabel.setText(Bundle.getMessage("StatReqThrottle"));
1600                    spFwd.clear();
1601                    spRev.clear();
1602                    if (dirFwdButton.isSelected()) {
1603                        profileDir = ProfileDirection.FORWARD;
1604                    } else {
1605                        profileDir = ProfileDirection.REVERSE;
1606                    }
1607                    resetGraphButton.setEnabled(false);
1608                    profileGraphPane.repaint();
1609                    profileTimer.start();
1610                    log.info("Requesting throttle");
1611                    boolean requestOK = jmri.InstanceManager.throttleManagerInstance().requestThrottle(locomotiveAddress, this, true);
1612                    if (!requestOK) {
1613                        log.error("Loco Address in use, throttle request failed.");
1614                    }
1615                }
1616            }
1617        } else {
1618            // Must have a non-zero address
1619            //profileAddressField.setBackground(Color.RED);
1620            log.error("Attempt to profile loco address 0");
1621        }
1622    }
1623
1624    /**
1625     * Profile timer timeout handler
1626     */
1627    protected synchronized void profileTimeout() {
1628        if (profileState == ProfileState.WAIT_FOR_THROTTLE) {
1629            tidyUp();
1630            log.error("Timeout waiting for throttle");
1631            statusLabel.setText(Bundle.getMessage("StatusTimeout"));
1632        } else if (profileState == ProfileState.RUNNING) {
1633            if (profileDir == ProfileDirection.FORWARD) {
1634                spFwd.setPoint(profileStep, avSpeed);
1635                statusLabel.setText(Bundle.getMessage("Fwd", profileStep));
1636            } else {
1637                spRev.setPoint(profileStep, avSpeed);
1638                statusLabel.setText(Bundle.getMessage("Rev", profileStep));
1639            }
1640            profileGraphPane.repaint();
1641            if (profileStep == 29) {
1642                if ((profileDir == ProfileDirection.FORWARD)
1643                        && dirRevButton.isSelected()) {
1644                    // Start reverse profile
1645                    profileDir = ProfileDirection.REVERSE;
1646                    throttle.setIsForward(false);
1647                    profileStep = 0;
1648                    avClr();
1649                    statusLabel.setText(Bundle.getMessage("StatCreateRev"));
1650                } else {
1651                    tidyUp();
1652                    statusLabel.setText(Bundle.getMessage("StatDone"));
1653                }
1654            } else {
1655                if (profileStep == 28) {
1656                    profileSpeed = 0.0F;
1657                } else {
1658                    profileSpeed += throttleIncrement;
1659                }
1660                throttle.setSpeedSetting(profileSpeed);
1661                profileStep += 1;
1662                // adjust delay as we get faster and averaging is quicker
1663                profileTimer.setDelay(7000 - range * 1000);
1664            }
1665        } else {
1666            log.error("Unexpected profile timeout");
1667            profileTimer.stop();
1668        }
1669    }
1670
1671    //</editor-fold>
1672    //<editor-fold defaultstate="collapsed" desc="Speed Profiling and Speed Matching Cleanup">
1673    /**
1674     * Resets profiling and speed matching timers and other pertinent values and
1675     * releases the throttle and ops mode programmer
1676     * <p>
1677     * Called both when profiling or speed matching finish successfully or error
1678     * out
1679     */
1680    protected void tidyUp() {
1681        stopTimers();
1682
1683        //turn off power
1684        //Turning power off is bad for some systems, e.g. Digitrax
1685//      try {
1686//          pm.setPower(PowerManager.OFF);
1687//      } catch (JmriException e) {
1688//          log.error("Exception during power off: "+e.toString());
1689//      }
1690        //release throttle
1691        if (throttle != null) {
1692            throttle.setSpeedSetting(0.0F);
1693            InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this);
1694            //throttle.release();
1695            throttle = null;
1696        }
1697
1698        //release ops mode programmer
1699        if (ops_mode_prog != null) {
1700            InstanceManager.getDefault(AddressedProgrammerManager.class).releaseAddressedProgrammer(ops_mode_prog);
1701            ops_mode_prog = null;
1702        }
1703
1704        resetGraphButton.setEnabled(true);
1705        progState = ProgState.IDLE;
1706        profileState = ProfileState.IDLE;
1707        speedMatchState = SpeedMatchState.IDLE;
1708        speedMatchSetupState = SpeedMatchSetupState.IDLE;
1709        speedMatchButton.setText(Bundle.getMessage("btnStartSpeedMatch"));
1710    }
1711
1712    /**
1713     * Stops the profiling and speed matching processes. Called by pressing
1714     * either the stop profile or stop speed matching buttons.
1715     */
1716    protected synchronized void stopProfileAndSpeedMatch() {
1717        if (profileState != ProfileState.IDLE) {
1718            tidyUp();
1719            profileState = ProfileState.IDLE;
1720            log.info("Profiling stopped by user");
1721        }
1722
1723        if (speedMatchState != SpeedMatchState.IDLE) {
1724            tidyUp();
1725            speedMatchState = SpeedMatchState.IDLE;
1726            statusLabel.setText(" ");
1727            log.info("Speed matching stopped by user");
1728        }
1729    }
1730
1731    /**
1732     * Stops profile and speed match timers
1733     */
1734    protected void stopTimers() {
1735        if (profileTimer != null) {
1736            profileTimer.stop();
1737        }
1738        if (speedMatchTimer != null) {
1739            speedMatchTimer.stop();
1740        }
1741    }
1742
1743    //</editor-fold>
1744    //<editor-fold defaultstate="collapsed" desc="Notifiers">
1745    /**
1746     * Called when a throttle is found
1747     *
1748     * @param t the requested DccThrottle
1749     */
1750    @Override
1751    public synchronized void notifyThrottleFound(DccThrottle t) {
1752        stopTimers();
1753
1754        throttle = t;
1755        log.info("Throttle acquired");
1756        throttle.setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
1757        if (throttle.getSpeedStepMode() != SpeedStepMode.NMRA_DCC_28) {
1758            log.error("Failed to set 28 step mode");
1759            statusLabel.setText(Bundle.getMessage("ThrottleError28"));
1760            InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this);
1761            //throttle.release();
1762            return;
1763        }
1764
1765        // turn on power
1766        try {
1767            pm.setPower(PowerManager.ON);
1768        } catch (JmriException e) {
1769            log.error("Exception during power on: {}", e.toString());
1770        }
1771
1772        throttleIncrement = throttle.getSpeedIncrement();
1773
1774        if (profileState == ProfileState.WAIT_FOR_THROTTLE) {
1775            log.info("Starting profiling");
1776            profileState = ProfileState.RUNNING;
1777            // Start at step 0 with 28 step packets
1778            profileSpeed = 0.0F;
1779            profileStep = 0;
1780            throttle.setSpeedSetting(profileSpeed);
1781            if (profileDir == ProfileDirection.FORWARD) {
1782                throttle.setIsForward(true);
1783                statusLabel.setText(Bundle.getMessage("StatCreateFwd"));
1784            } else {
1785                throttle.setIsForward(false);
1786                statusLabel.setText(Bundle.getMessage("StatCreateRev"));
1787            }
1788            // using profile timer to trigger each next step
1789            profileTimer.setRepeats(true);
1790            profileTimer.start();
1791        } else if (speedMatchState == SpeedMatchState.WAIT_FOR_THROTTLE) {
1792            log.info("Starting speed matching");
1793
1794            // using speed matching timer to trigger each phase of speed matching
1795            speedMatchState = SpeedMatchState.SETUP;
1796
1797            // start phase depends on program track vs main track
1798            if (mainButton.isSelected()) {
1799                log.debug("starting by going to VSTART");
1800                speedMatchSetupState = SpeedMatchSetupState.VSTART;
1801            } else {
1802                log.debug("starting by going to MOMENTUM_ACCEL_READ");
1803                speedMatchSetupState = SpeedMatchSetupState.MOMENTUM_ACCEL_READ;
1804            }
1805            speedMatchTimer.setInitialDelay(1500);
1806            speedMatchTimer.start();
1807        } else {
1808            tidyUp();
1809        }
1810    }
1811
1812    /**
1813     * Called when a throttle could not be obtained
1814     *
1815     * @param address the requested address
1816     * @param reason  the reason the throttle could not be obtained
1817     */
1818    @Override
1819    public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1820    }
1821
1822    /**
1823     * Called when we must decide to steal the throttle for the requested address. Since this is a
1824     * an automatically stealing implementation, the throttle will be automatically stolen.
1825     */
1826    @Override
1827    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1828      InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL );
1829    }
1830    //</editor-fold>
1831
1832    //<editor-fold defaultstate="collapsed" desc="Other Timers">
1833    javax.swing.Timer replyTimer = null;
1834    javax.swing.Timer displayTimer = null;
1835    javax.swing.Timer fastDisplayTimer = null;
1836
1837    /**
1838     * Starts the speedo hardware reply timer. Once we receive a speedoReply we
1839     * expect them regularly, at least once every 4 seconds.
1840     */
1841    protected void startReplyTimer() {
1842        replyTimer = new javax.swing.Timer(4000, e -> replyTimeout());
1843        replyTimer.setRepeats(true);     // refresh until stopped by dispose
1844        replyTimer.start();
1845    }
1846
1847    /**
1848     * Starts the timer used to update the speedometer display speed.
1849     */
1850    protected void startDisplayTimer() {
1851        displayTimer = new javax.swing.Timer(DISPLAY_UPDATE, e -> displayTimeout());
1852        displayTimer.setRepeats(true);     // refresh until stopped by dispose
1853        displayTimer.start();
1854    }
1855
1856    /**
1857     * Starts the timer used to update the speedometer display speed at a faster
1858     * rate.
1859     */
1860    protected void startFastDisplayTimer() {
1861        fastDisplayTimer = new javax.swing.Timer(DISPLAY_UPDATE / FAST_DISPLAY_RATIO, e -> fastDisplayTimeout());
1862        fastDisplayTimer.setRepeats(true);     // refresh until stopped by dispose
1863        fastDisplayTimer.start();
1864    }
1865
1866    //<editor-fold defaultstate="collapsed" desc="Timer Timeout Handlers">
1867    /**
1868     * Internal routine to reset the speed on a timeout.
1869     */
1870    protected synchronized void replyTimeout() {
1871        //log.debug("Timed out - display speed zero");
1872        targetSpeed = 0;
1873        avClr();
1874        oldSpeed = 0;
1875        showSpeed();
1876    }
1877
1878    /**
1879     * Internal routine to update the target speed for display
1880     */
1881    protected synchronized void displayTimeout() {
1882        //log.info("Display timeout");
1883        targetSpeed = avSpeed;
1884        incSpeed = (targetSpeed - currentSpeed) / FAST_DISPLAY_RATIO;
1885    }
1886
1887    /**
1888     * Internal routine to update the displayed speed
1889     */
1890    protected synchronized void fastDisplayTimeout() {
1891        //log.info("Display timeout");
1892        if (Math.abs(targetSpeed - currentSpeed) < Math.abs(incSpeed)) {
1893            currentSpeed = targetSpeed;
1894        } else {
1895
1896            currentSpeed += incSpeed;
1897        }
1898        if (currentSpeed < 0.01F) {
1899            currentSpeed = 0.0F;
1900        }
1901        showSpeed();
1902    }
1903
1904    /**
1905     * Timeout requesting a throttle.
1906     */
1907    protected synchronized void throttleTimeout() {
1908        jmri.InstanceManager.throttleManagerInstance().cancelThrottleRequest(locomotiveAddress, this);
1909        profileState = ProfileState.IDLE;
1910        speedMatchState = SpeedMatchState.IDLE;
1911        log.error("Timeout waiting for throttle");
1912    }
1913
1914    //</editor-fold>
1915    //</editor-fold>
1916    //<editor-fold defaultstate="collapsed" desc="Programming Functions">
1917    /**
1918     * Starts writing acceleration momentum (CV 3) using the ops mode programmer
1919     *
1920     * @param value acceleration value (0-255 inclusive)
1921     */
1922    protected synchronized void writeMomentumAccel(int value) {
1923        progState = ProgState.WRITE3;
1924        statusLabel.setText(Bundle.getMessage("ProgSetAccel", value));
1925        startOpsModeWrite("3", value);
1926    }
1927
1928    /**
1929     * Starts writing deceleration momentum (CV 4) using the ops mode programmer
1930     *
1931     * @param value deceleration value (0-255 inclusive)
1932     */
1933    protected synchronized void writeMomentumDecel(int value) {
1934        progState = ProgState.WRITE4;
1935        statusLabel.setText(Bundle.getMessage("ProgSetDecel", value));
1936        startOpsModeWrite("4", value);
1937    }
1938
1939    /**
1940     * Starts writing vStart to vStart (CV 2) using the ops mode programmer
1941     */
1942    protected synchronized void writeVStart() {
1943        progState = ProgState.WRITE2;
1944        statusLabel.setText(Bundle.getMessage("ProgSetVStart", vStart));
1945        startOpsModeWrite("2", vStart);
1946    }
1947
1948    /**
1949     * Starts writing the average of vStart and vHigh to vMid (CV 6) using the
1950     * ops mode programmer
1951     */
1952    protected synchronized void writeVMid() {
1953        int vMid = ((vStart + vHigh) / 2);
1954        progState = ProgState.WRITE6;
1955        //statusLabel.setText(Bundle.getMessage("ProgSetVMid", vMid));
1956        startOpsModeWrite("6", vMid);
1957    }
1958
1959    /**
1960     * Starts writing vHigh to vHigh (CV 5) using the ops mode programmer
1961     */
1962    protected synchronized void writeVHigh() {
1963        progState = ProgState.WRITE5;
1964        statusLabel.setText(Bundle.getMessage("ProgSetVHigh", vHigh));
1965        startOpsModeWrite("5", vHigh);
1966    }
1967
1968    /**
1969     * Starts writing forward trim (CV 66) using the ops mode programmer
1970     *
1971     * @param value forward trim value (0-255 inclusive)
1972     */
1973    protected synchronized void writeForwardTrim(int value) {
1974        progState = ProgState.WRITE66;
1975        statusLabel.setText(Bundle.getMessage("ProgSetForwardTrim", value));
1976        startOpsModeWrite("66", value);
1977    }
1978
1979    /**
1980     * Starts writing reverse trim (CV 95) using the ops mode programmer
1981     *
1982     * @param value reverse trim value (0-255 inclusive)
1983     */
1984    protected synchronized void writeReverseTrim(int value) {
1985        progState = ProgState.WRITE95;
1986        statusLabel.setText(Bundle.getMessage("ProgSetReverseTrim", value));
1987        startOpsModeWrite("95", value);
1988    }
1989
1990    /**
1991     * Starts reading the acceleration momentum (CV 3) using the service mode
1992     * programmer
1993     */
1994    protected void readMomentumAccel() {
1995        progState = ProgState.READ3;
1996        statusLabel.setText(Bundle.getMessage("ProgReadAccel"));
1997        startRead("3");
1998    }
1999
2000    /**
2001     * Starts reading the deceleration momentum (CV 4) using the service mode
2002     * programmer
2003     */
2004    protected void readMomentumDecel() {
2005        progState = ProgState.READ4;
2006        statusLabel.setText(Bundle.getMessage("ProgReadDecel"));
2007        startRead("4");
2008    }
2009
2010    /**
2011     * Starts reading the address (CVs 29 then 1 (short) or 17 and 18 (long))
2012     * using the service mode programmer
2013     */
2014    protected void readAddress() {
2015        progState = ProgState.READ29;
2016        statusLabel.setText(Bundle.getMessage("ProgRd29"));
2017        startRead("29");
2018    }
2019
2020    /**
2021     * Starts writing a CV using the ops mode programmer
2022     *
2023     * @param cv    the CV
2024     * @param value the value to write to the CV (0-255 inclusive)
2025     */
2026    protected void startOpsModeWrite(String cv, int value) {
2027        try {
2028            ops_mode_prog.writeCV(cv, value, this);
2029        } catch (ProgrammerException e) {
2030            log.error("Exception writing CV {}", cv, e);
2031        }
2032    }
2033
2034    /**
2035     * Starts reading a CV using the service mode programmer
2036     *
2037     * @param cv the CV
2038     */
2039    protected void startRead(String cv) {
2040        try {
2041            prog.readCV(String.valueOf(cv), this);
2042        } catch (ProgrammerException e) {
2043            log.error("Exception reading CV {}", cv, e);
2044        }
2045    }
2046
2047    /**
2048     * Called when the programmer (ops mode or service mode) has completed its
2049     * operation
2050     *
2051     * @param value  Value from a read operation, or value written on a write
2052     * @param status Denotes the completion code. Note that this is a bitwise
2053     *               combination of the various states codes defined in this
2054     *               interface. (see ProgListener.java for possible values)
2055     */
2056    @Override
2057    public void programmingOpReply(int value, int status) {
2058        if (status == 0) {
2059            switch (progState) {
2060                case IDLE:
2061                    log.debug("unexpected reply in IDLE state");
2062                    break;
2063
2064                case READ29:
2065                    // Check extended address bit
2066                    if ((value & 0x20) == 0) {
2067                        progState = ProgState.READ1;
2068                        statusLabel.setText(Bundle.getMessage("ProgRdShort"));
2069                        startRead("1");
2070                    } else {
2071                        progState = ProgState.READ17;
2072                        statusLabel.setText(Bundle.getMessage("ProgRdExtended"));
2073                        startRead("17");
2074                    }
2075                    break;
2076
2077                case READ1:
2078                    readAddress = value;
2079                    //profileAddressField.setText(Integer.toString(profileAddress));
2080                    //profileAddressField.setBackground(Color.WHITE);
2081                    addrSelector.setAddress(new DccLocoAddress(readAddress, false));
2082                    changeOfAddress();
2083                    progState = ProgState.IDLE;
2084                    break;
2085
2086                case READ17:
2087                    readAddress = value;
2088                    progState = ProgState.READ18;
2089                    startRead("18");
2090                    break;
2091
2092                case READ18:
2093                    readAddress = (readAddress & 0x3f) * 256 + value;
2094                    //profileAddressField.setText(Integer.toString(profileAddress));
2095                    //profileAddressField.setBackground(Color.WHITE);
2096                    addrSelector.setAddress(new DccLocoAddress(readAddress, true));
2097                    changeOfAddress();
2098                    statusLabel.setText(Bundle.getMessage("ProgRdComplete"));
2099                    progState = ProgState.IDLE;
2100                    break;
2101
2102                case READ3:
2103                    oldMomentumAccel = value;
2104                    progState = ProgState.IDLE;
2105                    break;
2106
2107                case READ4:
2108                    oldMomentumDecel = value;
2109                    progState = ProgState.IDLE;
2110                    break;
2111
2112                case WRITE3:
2113                case WRITE4:
2114                case WRITE6:
2115                case WRITE66:
2116                case WRITE95:
2117                    progState = ProgState.IDLE;
2118                    break;
2119
2120                // when writing vStart or vHigh, also write vMid
2121                case WRITE2:
2122                case WRITE5:
2123                    try {
2124                        Thread.sleep(1500);
2125                    } catch (InterruptedException e) {
2126                    }
2127                    writeVMid();
2128                    break;
2129
2130                default:
2131                    progState = ProgState.IDLE;
2132                    log.warn("Unhandled read state: {}", progState);
2133                    break;
2134            }
2135        } else {
2136            // Error during programming
2137            log.error("Status not OK during {}: {}", progState.toString(), status);
2138            //profileAddressField.setText("Error");
2139            statusLabel.setText(Bundle.getMessage("ProgError"));
2140            progState = ProgState.IDLE;
2141            tidyUp();
2142        }
2143    }
2144    //</editor-fold>
2145    //debugging logger
2146    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SpeedoConsoleFrame.class);
2147
2148}