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;
008import javax.swing.BoxLayout;
009import javax.swing.JComponent;
010import javax.swing.JLabel;
011import javax.swing.JOptionPane;
012import javax.swing.JPanel;
013import javax.swing.JScrollPane;
014import javax.swing.JTextField;
015import javax.swing.JToggleButton;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
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    public void readAll() {
314        // check the address
315        try {
316            setAddress(256);
317        } catch (Exception e) {
318            log.debug(Bundle.getMessage("ERROR_READALL_INVALID_ADDRESS"));
319            readAllButton.setSelected(false);
320            writeAllButton.setSelected(false);
321            status.setText(" ");
322            return;
323        }
324        if (responseTimer == null) {
325            initializeResponseTimer();
326        }
327        if (pacingTimer == null) {
328            initializePacingTimer();
329        }
330        // Start the first operation
331        read = true;
332        state = 1;
333        nextRequest();
334    }
335
336    /**
337     * Configure the type word in the LocoNet messages.
338     * <p>
339     * Known values:
340     * <ul>
341     *   <li>0x70 - PM4
342     *   <li>0x71 - BDL16
343     *   <li>0x72 - SE8
344     *   <li>0x73 - DS64
345     * </ul>
346     *
347     * @param type board type number, per list above
348     */
349    protected void setTypeWord(int type) {
350        typeWord = type;
351    }
352
353    /**
354     * Triggers the next read or write request. Is executed by the "pacing"
355     * delay timer, which allows time between any two OpSw accesses.
356     */
357    private final void delayedNextRequest() {
358        pacingTimer.stop();
359        if (read) {
360            // read op
361            status.setText(Bundle.getMessage("STATUS_READING_OPSW") + " " + state);
362            LocoNetMessage l = new LocoNetMessage(6);
363            l.setOpCode(LnConstants.OPC_MULTI_SENSE);
364            int element = 0x62;
365            if ((address & 0x80) != 0) {
366                element |= 1;
367            }
368            l.setElement(1, element);
369            l.setElement(2, address & 0x7F);
370            l.setElement(3, typeWord);
371            int loc = (state - 1) / 8;
372            int bit = (state - 1) - loc * 8;
373            l.setElement(4, loc * 16 + bit * 2);
374            memo.getLnTrafficController().sendLocoNetMessage(l);
375            awaitingReply = true;
376            responseTimer.stop();
377            responseTimer.restart();
378        } else {
379            //write op
380            status.setText(Bundle.getMessage("STATUS_WRITING_OPSW") + " " + state);
381            LocoNetMessage l = new LocoNetMessage(6);
382            l.setOpCode(LnConstants.OPC_MULTI_SENSE);
383            int element = 0x72;
384            if ((address & 0x80) != 0) {
385                element |= 1;
386            }
387            l.setElement(1, element);
388            l.setElement(2, address & 0x7F);
389            l.setElement(3, typeWord);
390            int loc = (state - 1) / 8;
391            int bit = (state - 1) - loc * 8;
392            l.setElement(4, loc * 16 + bit * 2 + (opsw[state] ? 1 : 0));
393            memo.getLnTrafficController().sendLocoNetMessage(l);
394            awaitingReply = true;
395            responseTimer.stop();
396            responseTimer.restart();
397        }
398    }
399
400    /**
401     * Start the pacing timer, which, at timeout, will begin the next OpSw
402     * access request.
403     */
404    private final void nextRequest() {
405        pacingTimer.stop();
406        pacingTimer.restart();
407        replyTryCount = 0;
408    }
409
410    /**
411     * Convert the GUI text field containing the address into a valid integer
412     * address, and handles user-input errors as needed.
413     *
414     * @param maxValid highest Board ID number allowed for the given device type
415     * @throws jmri.JmriException when the board address is invalid
416     */
417    void setAddress(int maxValid) throws jmri.JmriException {
418        try {
419            address = (Integer.parseInt(addrField.getText()) - 1);
420        } catch (NumberFormatException e) {
421            readAllButton.setSelected(false);
422            writeAllButton.setSelected(false);
423            status.setText(Bundle.getMessage("STATUS_INPUT_BAD"));
424            JOptionPane.showMessageDialog(this, Bundle.getMessage("STATUS_INVALID_ADDRESS"),
425                    Bundle.getMessage("STATUS_TYPE_ERROR"), JOptionPane.ERROR_MESSAGE);
426            log.error("{} {}", Bundle.getMessage("ERROR_PARSING_ADDRESS"), e);
427            throw e;
428        }
429        // parsed OK, check range
430        if (address > (maxValid - 1) || address < 0) {
431            readAllButton.setSelected(false);
432            writeAllButton.setSelected(false);
433            status.setText(Bundle.getMessage("STATUS_INPUT_BAD"));
434            String message = Bundle.getMessage("AbstractBoardProgPanel_ErrorAddressRange", 1, maxValid);
435            JOptionPane.showMessageDialog(this, message,
436                    "Error", JOptionPane.ERROR_MESSAGE); // NOI18N
437            log.error("Invalid board ID number: {}", Integer.toString(address)); // NOI18N
438            throw new jmri.JmriException(Bundle.getMessage("ERROR_INVALID_ADDRESS") + " " + address);
439        }
440    }
441
442    /**
443     * Copy from the GUI to the opsw array.
444     * <p>
445     * Used before a write operation is started.
446     */
447    abstract protected void copyToOpsw();
448
449    /**
450     * Update the GUI based on the contents of opsw[].
451     * <p>
452     * This method is executed after completion of a read operation sequence.
453     */
454    abstract protected void updateDisplay();
455
456    /**
457     * Compute the next OpSw number to be accessed, based on the current OpSw number.
458     *
459     * @param state current OpSw number
460     * @return computed next OpSw nubmer
461     */
462    abstract protected int nextState(int state);
463
464    /**
465     * Provide a mechanism to write several OpSw values in a sequence. The
466     * sequence is defined by the {@link #nextState(int)} method.
467     */
468    public void writeAll() {
469        // check the address
470        try {
471            setAddress(256);
472        } catch (Exception e) {
473            log.debug("{} {}", Bundle.getMessage("ERROR_WRITEALL_ABORTED"), e);
474            readAllButton.setSelected(false);
475            writeAllButton.setSelected(false);
476            status.setText(" "); // NOI18N
477            return;
478        }
479
480        if (responseTimer == null) {
481            initializeResponseTimer();
482        }
483        if (pacingTimer == null) {
484            initializePacingTimer();
485        }
486
487        // copy over the display
488        copyToOpsw();
489
490        // start the first operation
491        read = false;
492        state = 1;
493        // specify as single request, not multiple
494        onlyOneOperation = false;
495        nextRequest();
496    }
497
498    /**
499     * writeOne() is intended to provide a mechanism to write a single OpSw
500     * value, rather than a sequence of OpSws as done by writeAll().  The value
501     * to be written is taken from the appropriate entry in booleans[].
502     *
503     * @see jmri.jmrix.loconet.AbstractBoardProgPanel#writeAll()
504     * @param opswIndex  OpSw number
505     */
506    public void writeOne(int opswIndex) {
507        // check the address
508        try {
509            setAddress(256);
510        } catch (Exception e) {
511            if (log.isDebugEnabled()) {
512                log.debug("{} {}", Bundle.getMessage("ERROR_WRITEONE_ABORTED"), e);
513            }
514            readAllButton.setSelected(false);
515            writeAllButton.setSelected(false);
516            status.setText(" ");
517            return;
518        }
519
520        // copy over the displayed value
521        copyToOpsw();
522
523        // Start the first operation
524        read = false;
525        state = opswIndex;
526
527        // specify as single request, not multiple
528        onlyOneOperation = true;
529        nextRequest();
530    }
531
532    /**
533     * Processes incoming LocoNet message m for OpSw responses to read and write
534     * operation messages, and automatically advances to the next OpSw operation
535     * as directed by {@link #nextState(int)}.
536     *
537     *@param m  incoming LocoNet message
538     */
539    @Override
540    public void message(LocoNetMessage m) {
541        if (log.isDebugEnabled()) {
542            log.debug("{} {}", Bundle.getMessage("DEBUG_PARSING_LOCONET_MESSAGE"), m);
543        }
544        // are we reading? If not, ignore
545        if (state == 0) {
546            return;
547        }
548        // check for right type, unit
549        if ((m.getOpCode() != LnConstants.OPC_LONG_ACK)
550                || ((m.getElement(1) != 0x00) && (m.getElement(1) != 0x50))) {
551            return;
552        }
553
554        // LACK with 0 in opcode; assume its to us.  Note that there
555        // should be a 0x50 in the opcode, not zero, but this is what we
556        // see...
557        if (awaitingReply == true) {
558            if (responseTimer != null) {
559                if (responseTimer.isRunning()) {
560                    // stop the response timer!
561                    responseTimer.stop();
562                }
563            }
564        }
565
566        boolean value = false;
567        if ((m.getElement(2) & 0x20) != 0) {
568            value = true;
569        }
570
571        // update opsw array if LACK return status is not 0x7F
572        if ((m.getElement(2) != 0x7f)) {
573            // record this bit
574            opsw[state] = value;
575        }
576
577        // show what we've got so far
578        if (read) {
579            updateDisplay();
580        }
581
582        // and continue through next state, if any
583        doTheNextThing();
584    }
585
586    /**
587     * Helps continue sequences of OpSw accesses.
588     * <p>
589     * Handles aborting a sequence of reads or writes when the GUI Read button
590     * or the GUI Write button (as appropriate for the current operation) is
591     * de-selected.
592     */
593    public void doTheNextThing() {
594        int origState;
595        origState = state;
596        if (origState != 0) {
597            state = nextState(origState);
598        }
599        if ((origState == 0) || (state == 0)) {
600            // done with sequence
601            readAllButton.setSelected(false);
602            writeAllButton.setSelected(false);
603            if (origState != 0) {
604                status.setText(Bundle.getMessage("AbstractBoardProgPanel_Done_Message"));
605            } else {
606                status.setText(Bundle.getMessage("ERROR_ABORTED_DUE_TO_TIMEOUT"));
607            }
608            // nothing more to do
609        } else {
610            // are not yet done, so create and send the next OpSw request message
611            nextRequest();
612        }
613    }
614
615    private ActionListener responseTimerListener = new ActionListener() {
616
617        @Override
618        public void actionPerformed(ActionEvent e) {
619            if (responseTimer.isRunning()) {
620                // odd case - not sure why would get an event if the timer is not running.
621            } else {
622                if (awaitingReply == true) {
623                    // Have a case where are awaiting a reply from the device,
624                    // but the response timer has expired without a reply.
625
626                    if (replyTryCount < MAX_OPSW_ACCESS_RETRIES) {
627                        // have not reached maximum number of retries, so try
628                        // the access again
629                        replyTryCount++;
630                        log.debug("retrying({}) access to OpSw{}", replyTryCount, state); // NOI18N
631                        responseTimer.stop();
632                        delayedNextRequest();
633                        return;
634                    }
635
636                    // Have reached the maximum number of retries for accessing
637                    // a given OpSw.
638                    // Cancel the ongoing process and update the status line.
639                    log.warn("Reached OpSw access retry limit of {} when accessing OpSw{}", MAX_OPSW_ACCESS_RETRIES, state); // NOI18N
640                    awaitingReply = false;
641                    responseTimer.stop();
642                    state = 0;
643                    replyTryCount = 0;
644                    doTheNextThing();
645                }
646            }
647        }
648    };
649
650    private void initializeResponseTimer() {
651        if (responseTimer == null) {
652            responseTimer = new javax.swing.Timer(HALF_A_SECOND, responseTimerListener);
653            responseTimer.setRepeats(false);
654            responseTimer.stop();
655            responseTimer.setInitialDelay(HALF_A_SECOND);
656            responseTimer.setDelay(HALF_A_SECOND);
657        }
658    }
659
660    private ActionListener pacingTimerListener = new ActionListener() {
661
662        @Override
663        public void actionPerformed(ActionEvent e) {
664            if (pacingTimer.isRunning()) {
665                // odd case - not sure why would get an event if the timer is not running.
666                log.warn("Unexpected pacing timer event while OpSw access timer is running."); // NOI18N
667            } else {
668                pacingTimer.stop();
669                delayedNextRequest();
670            }
671        }
672    };
673
674    private void initializePacingTimer() {
675        if (pacingTimer == null) {
676            pacingTimer = new javax.swing.Timer(FIFTIETH_OF_A_SECOND, pacingTimerListener);
677            pacingTimer.setRepeats(false);
678            pacingTimer.stop();
679            pacingTimer.setInitialDelay(FIFTIETH_OF_A_SECOND);
680            pacingTimer.setDelay(FIFTIETH_OF_A_SECOND);
681        }
682    }
683
684    @Override
685    public void dispose() {
686        // Drop LocoNet connection
687        if (memo.getLnTrafficController() != null) {
688            memo.getLnTrafficController().removeLocoNetListener(~0, this);
689        }
690        super.dispose();
691
692        // stop all timers (if necessary) before disposing of this class
693        if (responseTimer != null) {
694            responseTimer.stop();
695        }
696        if (pacingTimer != null) {
697            pacingTimer.stop();
698        }
699    }
700
701    // maximum number of additional retries after board does not respond to
702    // first attempt to access a given OpSw
703    private final int MAX_OPSW_ACCESS_RETRIES = 2;
704
705    private final static Logger log = LoggerFactory.getLogger(AbstractBoardProgPanel.class);
706
707}