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