001package jmri.jmrix.loconet; 002 003import java.awt.BorderLayout; 004import java.awt.Color; 005import java.awt.FlowLayout; 006import java.awt.event.ActionEvent; 007import java.awt.event.ActionListener; 008 009import javax.swing.BoxLayout; 010import javax.swing.JComponent; 011import javax.swing.JLabel; 012import javax.swing.JPanel; 013import javax.swing.JScrollPane; 014import javax.swing.JTextField; 015import javax.swing.JToggleButton; 016 017import jmri.util.swing.JmriJOptionPane; 018 019/** 020 * Display and modify an Digitrax board configuration. 021 * <p> 022 * Supports boards which can be read and write using LocoNet opcode 023 * OPC_MULTI_SENSE, such as PM4x, DS64, SE8c, BDL16x. 024 * <p> 025 * The read and write require a sequence of operations, which we handle with a 026 * state variable. 027 * <p> 028 * Each read or write OpSw access requires a response from the addressed board. 029 * If a response is not received within a fixed time, then the process will 030 * repeat the read or write OpSw access up to MAX_OPSW_ACCESS_RETRIES additional 031 * times to try to get a response from the addressed board. If the board does 032 * not respond, the access sequence is aborted and a failure message is 033 * populated in the "status" variable. 034 * <p> 035 * Programming of the board is done via configuration messages, so the board 036 * should not be put into programming mode via the built-in pushbutton while 037 * this tool is in use. 038 * <p> 039 * Throughout, the terminology is "closed" == true, "thrown" == false. Variables 040 * are named for their closed state. 041 * <p> 042 * Some of the message formats used in this class are Copyright Digitrax, Inc. 043 * and used with permission as part of the JMRI project. That permission does 044 * not extend to uses in other software products. If you wish to use this code, 045 * algorithm or these message formats outside of JMRI, please contact Digitrax 046 * Inc for separate permission. 047 * 048 * @author Bob Jacobsen Copyright (C) 2004, 2007 049 * @author B. Milhaupt Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017 050 */ 051abstract public class AbstractBoardProgPanel extends jmri.jmrix.loconet.swing.LnPanel 052 implements LocoNetListener { 053 054 JPanel contents = new JPanel(); 055 056 public JToggleButton readAllButton = null; 057 public JToggleButton writeAllButton = null; 058 public JTextField addrField = new JTextField(4); 059 JLabel status = new JLabel(); 060 061 public boolean read = false; 062 public int state = 0; 063 boolean awaitingReply = false; 064 int replyTryCount = 0; 065 066 /* The responseTimer provides a timeout mechanism for OpSw read and write 067 * requests. 068 */ 069 public javax.swing.Timer responseTimer = null; 070 071 /* The pacing timer is used to reduce the speed of this tool's requests to 072 * LocoNet. 073 */ 074 public javax.swing.Timer pacingTimer = null; 075 076 /* The boolean field onlyOneOperation is intended to allow accesses to 077 * a single OpSw value at a time. This is un-tested functionality. 078 */ 079 public boolean onlyOneOperation = false; 080 int address = 0; 081 082 /* typeWord provides the encoded device type number, and is used within the 083 * LocoNet OpSw Read and Write request messages. Different Digitrax boards 084 * respond to different encoded device type values, as shown here: 085 * PM4/PM42 0x70 086 * BDL16/BDL162/BDL168 0x71 087 * SE8C 0x72 088 * DS64 0x73 089 */ 090 int typeWord; 091 092 boolean readOnInit; 093 094 /** 095 * True is "closed", false is "thrown". This matches how we do the check 096 * boxes also, where we use the terminology for the "closed" option. Note 097 * that opsw[0] is not a legal OpSwitch. 098 */ 099 protected boolean[] opsw = new boolean[65]; 100 private final static int HALF_A_SECOND = 500; 101 private final static int FIFTIETH_OF_A_SECOND = 20; // 20 milliseconds = 1/50th of a second 102 103 private String boardTypeName; 104 105 /** 106 * Constructor which accepts a "board type" string. 107 * The board number defaults to 1, and the board will not 108 * be automatically read. 109 * 110 * @param boardTypeName device type name, to be included in read and write GUI buttons 111 */ 112 protected AbstractBoardProgPanel(String boardTypeName) { 113 this(1, false, boardTypeName); 114 } 115 116 /** 117 * Constructor which accepts a boolean which specifies whether 118 * to automatically read the board, plus a string defining 119 * the "board type". The board number defaults to 1. 120 * 121 * @param readOnInit true to read OpSw values of board 1 upon panel creation 122 * @param boardTypeName device type name, to be included in read and write GUI buttons 123 */ 124 protected AbstractBoardProgPanel(boolean readOnInit, String boardTypeName) { 125 this(1, readOnInit, boardTypeName); 126 } 127 128 /** 129 * Constructor which accepts parameters for the initial board number, whether 130 * to automatically read the board, and a "board type" string. 131 * 132 * @param boardNum default board ID number upon panel creation 133 * @param readOnInit true to read OpSw values of board 1 upon panel creation 134 * @param boardTypeName device type name, to be included in read and write GUI buttons 135 */ 136 protected AbstractBoardProgPanel(int boardNum, boolean readOnInit, String boardTypeName) { 137 super(); 138 this.boardTypeName = boardTypeName; 139 140 // basic formatting: Create pane to hold contents 141 // within a scroll box 142 contents.setLayout(new BoxLayout(contents, BoxLayout.Y_AXIS)); 143 144 // and prep for display 145 addrField.setText(Integer.toString(boardNum)); 146 this.readOnInit = readOnInit; 147 } 148 149 /** 150 * Constructor which allows the caller to pass in the board ID number 151 * and board type name 152 * 153 * @param boardNum default board ID number upon panel creation 154 * @param boardTypeName device type name, to be included in read and write GUI buttons 155 */ 156 protected AbstractBoardProgPanel(int boardNum, String boardTypeName) { 157 this(boardNum, false, boardTypeName); 158 } 159 160 /** 161 * In order to get the scrollpanel on the screen it must be added at the end when 162 * all components and sub panels have been added to the one panel. 163 * This must be called as the last thing in the initComponents. 164 */ 165 protected void panelToScroll() { 166 JScrollPane scroll = new JScrollPane(contents); 167 scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 168 scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 169 setLayout(new BorderLayout()); //!! added 170 add(scroll,BorderLayout.CENTER); 171 setVisible(true); 172 } 173 174 @Override 175 public void initComponents(LocoNetSystemConnectionMemo memo) { 176 super.initComponents(memo); 177 178 // listen for message traffic 179 if (memo.getLnTrafficController() != null) { 180 memo.getLnTrafficController().addLocoNetListener(~0, this); 181 if (readOnInit == true) { 182 readAllButton.setSelected(true); 183 readAllButton.updateUI(); 184 readAll(); 185 } 186 } else { 187 log.error("No LocoNet connection available, this tool cannot function"); // NOI18N 188 } 189 } 190 191 @Override 192 public void initComponents() { 193 initializeResponseTimer(); 194 initializePacingTimer(); 195 } 196 197 /** 198 * Set the Board ID number (also known as board address number) 199 * 200 * @param boardId board ID number to be accessed 201 */ 202 public void setBoardIdValue(Integer boardId) { 203 /* 204 * For device types where the valid range of Board ID numbers is different 205 * than implemented here (1 to 256, inclusive), this method should be 206 * overridden with appropriate range limits. 207 */ 208 209 if (boardId < 1) { 210 return; 211 } 212 if (boardId > 256) { 213 return; 214 } 215 addrField.setText(Integer.toString(boardId)); 216 address = boardId - 1; 217 } 218 219 public Integer getBoardIdValue() { 220 return Integer.parseInt(addrField.getText()); 221 } 222 223 /** 224 * Creates a JPanel to allow the user to specify a board address. Includes 225 * a previously-defined board type name within the panel, or, if none has 226 * been previously provided, a default board-type name. 227 * 228 * @return a JPanel with address entry 229 */ 230 protected JPanel provideAddressing() { 231 return this.provideAddressing(boardTypeName); 232 } 233 234 /** 235 * Creates a JPanel to allow the user to specify a board address and to 236 * read and write the device. The "read" and "write" buttons have text which 237 * uses the specified "board type name" from the method parameter. 238 * 239 * @param boardTypeName device type name, to be included in read and write GUI buttons 240 * @return JPanel containing a JTextField and read and write JButtons 241 */ 242 protected JPanel provideAddressing(String boardTypeName) { 243 JPanel pane0 = new JPanel(); 244 pane0.setLayout(new FlowLayout()); 245 pane0.add(new JLabel(Bundle.getMessage("LABEL_UNIT_ADDRESS") + " ")); 246 pane0.add(addrField); 247 readAllButton = new JToggleButton(Bundle.getMessage("AbstractBoardProgPanel_ReadFrom", boardTypeName)); 248 writeAllButton = new JToggleButton(Bundle.getMessage("AbstractBoardProgPanel_WriteTo", boardTypeName)); 249 250 // make both buttons a little bit bigger, with identical (preferred) sizes 251 // (width increased because some computers/displays trim the button text) 252 java.awt.Dimension d = writeAllButton.getPreferredSize(); 253 int w = d.width; 254 d = readAllButton.getPreferredSize(); 255 if (d.width > w) { 256 w = d.width; 257 } 258 writeAllButton.setPreferredSize(new java.awt.Dimension((int) (w * 1.1), d.height)); 259 readAllButton.setPreferredSize(new java.awt.Dimension((int) (w * 1.1), d.height)); 260 261 pane0.add(readAllButton); 262 pane0.add(writeAllButton); 263 264 // install read all, write all button handlers 265 readAllButton.addActionListener((ActionEvent a) -> { 266 if (readAllButton.isSelected()) { 267 readAll(); 268 } 269 }); 270 writeAllButton.addActionListener((ActionEvent a) -> { 271 if (writeAllButton.isSelected()) { 272 writeAll(); 273 } 274 }); 275 return pane0; 276 } 277 278 /** 279 * Create the status line for the GUI. 280 * 281 * @return JComponent which will display status updates 282 */ 283 protected JComponent provideStatusLine() { 284 status.setFont(status.getFont().deriveFont(0.9f * addrField.getFont().getSize())); // a bit smaller 285 status.setForeground(Color.gray); 286 return status; 287 } 288 289 /** 290 * Update the status line. 291 * 292 * @param msg to be displayed on the status line 293 */ 294 protected void setStatus(String msg) { 295 status.setText(msg); 296 } 297 298 /** 299 * Handle GUI layout details during construction. 300 * Adds items as lines onto JPanel. 301 * 302 * @param c component to put on a single line 303 */ 304 protected void appendLine(JComponent c) { 305 c.setAlignmentX(0.f); 306 contents.add(c); 307 } 308 309 /** 310 * Provides a mechanism to read several OpSw values in a sequence. The 311 * sequence is defined by the {@link #nextState(int)} method. 312 */ 313 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 314 justification="I18N of log message") 315 public void readAll() { 316 // check the address 317 try { 318 setAddress(256); 319 } catch (Exception e) { 320 log.debug("{}", Bundle.getMessage("ERROR_READALL_INVALID_ADDRESS")); 321 readAllButton.setSelected(false); 322 writeAllButton.setSelected(false); 323 status.setText(" "); 324 return; 325 } 326 if (responseTimer == null) { 327 initializeResponseTimer(); 328 } 329 if (pacingTimer == null) { 330 initializePacingTimer(); 331 } 332 // Start the first operation 333 read = true; 334 state = 1; 335 nextRequest(); 336 } 337 338 /** 339 * Configure the type word in the LocoNet messages. 340 * <p> 341 * Known values: 342 * <ul> 343 * <li>0x70 - PM4 344 * <li>0x71 - BDL16 345 * <li>0x72 - SE8 346 * <li>0x73 - DS64 347 * </ul> 348 * 349 * @param type board type number, per list above 350 */ 351 protected void setTypeWord(int type) { 352 typeWord = type; 353 } 354 355 /** 356 * Triggers the next read or write request. Is executed by the "pacing" 357 * delay timer, which allows time between any two OpSw accesses. 358 */ 359 private final void delayedNextRequest() { 360 pacingTimer.stop(); 361 if (read) { 362 // read op 363 status.setText(Bundle.getMessage("STATUS_READING_OPSW") + " " + state); 364 LocoNetMessage l = new LocoNetMessage(6); 365 l.setOpCode(LnConstants.OPC_MULTI_SENSE); 366 int element = 0x62; 367 if ((address & 0x80) != 0) { 368 element |= 1; 369 } 370 l.setElement(1, element); 371 l.setElement(2, address & 0x7F); 372 l.setElement(3, typeWord); 373 int loc = (state - 1) / 8; 374 int bit = (state - 1) - loc * 8; 375 l.setElement(4, loc * 16 + bit * 2); 376 memo.getLnTrafficController().sendLocoNetMessage(l); 377 awaitingReply = true; 378 responseTimer.stop(); 379 responseTimer.restart(); 380 } else { 381 //write op 382 status.setText(Bundle.getMessage("STATUS_WRITING_OPSW") + " " + state); 383 LocoNetMessage l = new LocoNetMessage(6); 384 l.setOpCode(LnConstants.OPC_MULTI_SENSE); 385 int element = 0x72; 386 if ((address & 0x80) != 0) { 387 element |= 1; 388 } 389 l.setElement(1, element); 390 l.setElement(2, address & 0x7F); 391 l.setElement(3, typeWord); 392 int loc = (state - 1) / 8; 393 int bit = (state - 1) - loc * 8; 394 l.setElement(4, loc * 16 + bit * 2 + (opsw[state] ? 1 : 0)); 395 memo.getLnTrafficController().sendLocoNetMessage(l); 396 awaitingReply = true; 397 responseTimer.stop(); 398 responseTimer.restart(); 399 } 400 } 401 402 /** 403 * Start the pacing timer, which, at timeout, will begin the next OpSw 404 * access request. 405 */ 406 private final void nextRequest() { 407 pacingTimer.stop(); 408 pacingTimer.restart(); 409 replyTryCount = 0; 410 } 411 412 /** 413 * Convert the GUI text field containing the address into a valid integer 414 * address, and handles user-input errors as needed. 415 * 416 * @param maxValid highest Board ID number allowed for the given device type 417 * @throws jmri.JmriException when the board address is invalid 418 */ 419 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 420 justification="I18N of log message") 421 void setAddress(int maxValid) throws jmri.JmriException { 422 try { 423 address = (Integer.parseInt(addrField.getText()) - 1); 424 } catch (NumberFormatException e) { 425 readAllButton.setSelected(false); 426 writeAllButton.setSelected(false); 427 status.setText(Bundle.getMessage("STATUS_INPUT_BAD")); 428 JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("STATUS_INVALID_ADDRESS"), 429 Bundle.getMessage("STATUS_TYPE_ERROR"), JmriJOptionPane.ERROR_MESSAGE); 430 log.error("{}", Bundle.getMessage("ERROR_PARSING_ADDRESS"), e); 431 throw e; 432 } 433 // parsed OK, check range 434 if (address > (maxValid - 1) || address < 0) { 435 readAllButton.setSelected(false); 436 writeAllButton.setSelected(false); 437 status.setText(Bundle.getMessage("STATUS_INPUT_BAD")); 438 String message = Bundle.getMessage("AbstractBoardProgPanel_ErrorAddressRange", 1, maxValid); 439 JmriJOptionPane.showMessageDialog(this, message, 440 Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE); 441 log.error("Invalid board ID number: {}", Integer.toString(address)); // NOI18N 442 throw new jmri.JmriException(Bundle.getMessage("ERROR_INVALID_ADDRESS") + " " + address); 443 } 444 } 445 446 /** 447 * Copy from the GUI to the opsw array. 448 * <p> 449 * Used before a write operation is started. 450 */ 451 abstract protected void copyToOpsw(); 452 453 /** 454 * Update the GUI based on the contents of opsw[]. 455 * <p> 456 * This method is executed after completion of a read operation sequence. 457 */ 458 abstract protected void updateDisplay(); 459 460 /** 461 * Compute the next OpSw number to be accessed, based on the current OpSw number. 462 * 463 * @param state current OpSw number 464 * @return computed next OpSw nubmer 465 */ 466 abstract protected int nextState(int state); 467 468 /** 469 * Provide a mechanism to write several OpSw values in a sequence. The 470 * sequence is defined by the {@link #nextState(int)} method. 471 */ 472 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 473 justification="I18N of log message") 474 public void writeAll() { 475 // check the address 476 try { 477 setAddress(256); 478 } catch (Exception e) { 479 log.debug("{}", Bundle.getMessage("ERROR_WRITEALL_ABORTED"), e); 480 readAllButton.setSelected(false); 481 writeAllButton.setSelected(false); 482 status.setText(" "); // NOI18N 483 return; 484 } 485 486 if (responseTimer == null) { 487 initializeResponseTimer(); 488 } 489 if (pacingTimer == null) { 490 initializePacingTimer(); 491 } 492 493 // copy over the display 494 copyToOpsw(); 495 496 // start the first operation 497 read = false; 498 state = 1; 499 // specify as single request, not multiple 500 onlyOneOperation = false; 501 nextRequest(); 502 } 503 504 /** 505 * writeOne() is intended to provide a mechanism to write a single OpSw 506 * value, rather than a sequence of OpSws as done by writeAll(). The value 507 * to be written is taken from the appropriate entry in booleans[]. 508 * 509 * @see jmri.jmrix.loconet.AbstractBoardProgPanel#writeAll() 510 * @param opswIndex OpSw number 511 */ 512 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 513 justification="I18N of log message") 514 public void writeOne(int opswIndex) { 515 // check the address 516 try { 517 setAddress(256); 518 } catch (Exception e) { 519 if (log.isDebugEnabled()) { 520 log.debug("{}", Bundle.getMessage("ERROR_WRITEONE_ABORTED"), e); 521 } 522 readAllButton.setSelected(false); 523 writeAllButton.setSelected(false); 524 status.setText(" "); 525 return; 526 } 527 528 // copy over the displayed value 529 copyToOpsw(); 530 531 // Start the first operation 532 read = false; 533 state = opswIndex; 534 535 // specify as single request, not multiple 536 onlyOneOperation = true; 537 nextRequest(); 538 } 539 540 /** 541 * Processes incoming LocoNet message m for OpSw responses to read and write 542 * operation messages, and automatically advances to the next OpSw operation 543 * as directed by {@link #nextState(int)}. 544 * 545 *@param m incoming LocoNet message 546 */ 547 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT", 548 justification="I18N of log message") 549 @Override 550 public void message(LocoNetMessage m) { 551 if (log.isDebugEnabled()) { 552 log.debug("{} {}", Bundle.getMessage("DEBUG_PARSING_LOCONET_MESSAGE"), m); 553 } 554 // are we reading? If not, ignore 555 if (state == 0) { 556 return; 557 } 558 // check for right type, unit 559 if ((m.getOpCode() != LnConstants.OPC_LONG_ACK) 560 || ((m.getElement(1) != 0x00) && (m.getElement(1) != 0x50))) { 561 return; 562 } 563 564 // LACK with 0 in opcode; assume its to us. Note that there 565 // should be a 0x50 in the opcode, not zero, but this is what we 566 // see... 567 if (awaitingReply == true) { 568 if (responseTimer != null) { 569 if (responseTimer.isRunning()) { 570 // stop the response timer! 571 responseTimer.stop(); 572 } 573 } 574 } 575 576 boolean value = false; 577 if ((m.getElement(2) & 0x20) != 0) { 578 value = true; 579 } 580 581 // update opsw array if LACK return status is not 0x7F 582 if ((m.getElement(2) != 0x7f)) { 583 // record this bit 584 opsw[state] = value; 585 } 586 587 // show what we've got so far 588 if (read) { 589 updateDisplay(); 590 } 591 592 // and continue through next state, if any 593 doTheNextThing(); 594 } 595 596 /** 597 * Helps continue sequences of OpSw accesses. 598 * <p> 599 * Handles aborting a sequence of reads or writes when the GUI Read button 600 * or the GUI Write button (as appropriate for the current operation) is 601 * de-selected. 602 */ 603 public void doTheNextThing() { 604 int origState; 605 origState = state; 606 if (origState != 0) { 607 state = nextState(origState); 608 } 609 if ((origState == 0) || (state == 0)) { 610 // done with sequence 611 readAllButton.setSelected(false); 612 writeAllButton.setSelected(false); 613 if (origState != 0) { 614 status.setText(Bundle.getMessage("AbstractBoardProgPanel_Done_Message")); 615 } else { 616 status.setText(Bundle.getMessage("ERROR_ABORTED_DUE_TO_TIMEOUT")); 617 } 618 // nothing more to do 619 } else { 620 // are not yet done, so create and send the next OpSw request message 621 nextRequest(); 622 } 623 } 624 625 private ActionListener responseTimerListener = new ActionListener() { 626 627 @Override 628 public void actionPerformed(ActionEvent e) { 629 if (responseTimer.isRunning()) { 630 // odd case - not sure why would get an event if the timer is not running. 631 } else { 632 if (awaitingReply == true) { 633 // Have a case where are awaiting a reply from the device, 634 // but the response timer has expired without a reply. 635 636 if (replyTryCount < MAX_OPSW_ACCESS_RETRIES) { 637 // have not reached maximum number of retries, so try 638 // the access again 639 replyTryCount++; 640 log.debug("retrying({}) access to OpSw{}", replyTryCount, state); // NOI18N 641 responseTimer.stop(); 642 delayedNextRequest(); 643 return; 644 } 645 646 // Have reached the maximum number of retries for accessing 647 // a given OpSw. 648 // Cancel the ongoing process and update the status line. 649 log.warn("Reached OpSw access retry limit of {} when accessing OpSw{}", MAX_OPSW_ACCESS_RETRIES, state); // NOI18N 650 awaitingReply = false; 651 responseTimer.stop(); 652 state = 0; 653 replyTryCount = 0; 654 doTheNextThing(); 655 } 656 } 657 } 658 }; 659 660 private void initializeResponseTimer() { 661 if (responseTimer == null) { 662 responseTimer = new javax.swing.Timer(HALF_A_SECOND, responseTimerListener); 663 responseTimer.setRepeats(false); 664 responseTimer.stop(); 665 responseTimer.setInitialDelay(HALF_A_SECOND); 666 responseTimer.setDelay(HALF_A_SECOND); 667 } 668 } 669 670 private ActionListener pacingTimerListener = new ActionListener() { 671 672 @Override 673 public void actionPerformed(ActionEvent e) { 674 if (pacingTimer.isRunning()) { 675 // odd case - not sure why would get an event if the timer is not running. 676 log.warn("Unexpected pacing timer event while OpSw access timer is running."); // NOI18N 677 } else { 678 pacingTimer.stop(); 679 delayedNextRequest(); 680 } 681 } 682 }; 683 684 private void initializePacingTimer() { 685 if (pacingTimer == null) { 686 pacingTimer = new javax.swing.Timer(FIFTIETH_OF_A_SECOND, pacingTimerListener); 687 pacingTimer.setRepeats(false); 688 pacingTimer.stop(); 689 pacingTimer.setInitialDelay(FIFTIETH_OF_A_SECOND); 690 pacingTimer.setDelay(FIFTIETH_OF_A_SECOND); 691 } 692 } 693 694 @Override 695 public void dispose() { 696 // Drop LocoNet connection 697 if (memo.getLnTrafficController() != null) { 698 memo.getLnTrafficController().removeLocoNetListener(~0, this); 699 } 700 super.dispose(); 701 702 // stop all timers (if necessary) before disposing of this class 703 if (responseTimer != null) { 704 responseTimer.stop(); 705 } 706 if (pacingTimer != null) { 707 pacingTimer.stop(); 708 } 709 } 710 711 // maximum number of additional retries after board does not respond to 712 // first attempt to access a given OpSw 713 private final int MAX_OPSW_ACCESS_RETRIES = 2; 714 715 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractBoardProgPanel.class); 716 717}