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}