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}