001package jmri.jmrix.ncemonitor;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Dimension;
006import java.awt.FlowLayout;
007import java.io.DataInputStream;
008import java.io.IOException;
009import java.io.OutputStream;
010import java.util.Vector;
011
012import javax.swing.BoxLayout;
013import javax.swing.ButtonGroup;
014import javax.swing.JCheckBox;
015import javax.swing.JComboBox;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.JRadioButton;
019import javax.swing.JScrollPane;
020import javax.swing.JSeparator;
021
022//import javax.swing.JToggleButton;
023import jmri.jmrix.nce.NceSystemConnectionMemo;
024import jmri.jmrix.nce.swing.NcePanelInterface;
025
026import com.fazecast.jSerialComm.SerialPort;
027
028/**
029 * Simple GUI for access to an NCE monitor card
030 * <p>
031 * When opened, the user must first select a serial port and click "Start". The
032 * rest of the GUI then appears.
033 *
034 * @author Ken Cameron Copyright (C) 2010 derived from -
035 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2023
036 * @author Ken Cameron Copyright (C) 2023
037 */
038@SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC", justification = "serialStream is access from separate thread, and this class isn't used much")
039public class NcePacketMonitorPanel extends jmri.jmrix.AbstractMonPane implements NcePanelInterface {
040    
041    Vector<String> portNameVector = null;
042    SerialPort activeSerialPort = null;
043    NceSystemConnectionMemo memo = null;
044
045    protected JCheckBox dupFilterCheckBox = new JCheckBox(Bundle.getMessage("DupFilterCheckBoxLabel"));
046    protected JComboBox<String> portBox = new javax.swing.JComboBox<String>();
047    protected javax.swing.JButton openButton = new javax.swing.JButton(Bundle.getMessage("OpenButtonLabel"));
048    protected javax.swing.JButton closePortButton = new javax.swing.JButton(Bundle.getMessage("CloseButtonLabel"));
049    protected JRadioButton verboseButton = new JRadioButton(Bundle.getMessage("VerboseButtonLabel"));
050    protected JRadioButton origHex0Button = new JRadioButton(Bundle.getMessage("OrigHex0Label"));
051    protected JRadioButton origHex1Button = new JRadioButton(Bundle.getMessage("OrigHex1Label"));
052    protected JRadioButton origHex2Button = new JRadioButton(Bundle.getMessage("OrigHex2Label"));
053    protected JRadioButton origHex3Button = new JRadioButton(Bundle.getMessage("OrigHex3Label"));
054    protected JRadioButton origHex4Button = new JRadioButton(Bundle.getMessage("OrigHex4Label"));
055    protected JRadioButton origHex5Button = new JRadioButton(Bundle.getMessage("OrigHex5Label"));
056    protected JRadioButton newHex0Button = new JRadioButton(Bundle.getMessage("NewHex0Label"));
057    protected JRadioButton newHex1Button = new JRadioButton(Bundle.getMessage("NewHex1Label"));
058    protected JRadioButton accOnButton = new JRadioButton(Bundle.getMessage("AccOnLabel"));
059    protected JRadioButton idleOnButton = new JRadioButton(Bundle.getMessage("IdleOnLabel"));
060    protected JRadioButton locoOnButton = new JRadioButton(Bundle.getMessage("LocoOnLabel"));
061    protected JRadioButton resetOnButton = new JRadioButton(Bundle.getMessage("ResetOnLabel"));
062    protected JRadioButton signalOnButton = new JRadioButton(Bundle.getMessage("SignalOnLabel"));
063    protected JRadioButton accSingleButton = new JRadioButton(Bundle.getMessage("AccSingleLabel"));
064    protected JRadioButton accPairedButton = new JRadioButton(Bundle.getMessage("AccPairedLabel"));
065
066    protected JComboBox<String> modelBox = new JComboBox<>();
067    protected JLabel modelBoxLabel;
068    private String[] validModelNames = new String[]{Bundle.getMessage("PacketAnalyzer"), Bundle.getMessage("DccMeter/Analyzer")};
069    private final static int MODELORIG = 0;
070    private final static int MODELNEW = 1;
071    private int[] validModelValues = new int[]{MODELORIG, MODELNEW};
072    private int[] modelBaudRates = new int[]{38400, 115200};
073    // For old model, Doc says 7 bits, but 8 seems needed, new calls for 115,200 n 8 1
074    private int[] modelBitValues = new int[] {8, 8};
075    private int[] modelStopValues = new int[] {SerialPort.ONE_STOP_BIT, SerialPort.ONE_STOP_BIT};
076    private int[] modelParityValues = new int[] {SerialPort.NO_PARITY, SerialPort.NO_PARITY};
077
078    public NcePacketMonitorPanel() {
079        super();
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    @Override
086    public void init() {
087    }
088
089    /**
090     * {@inheritDoc}
091     */
092    @Override
093    public void initContext(Object context) {
094        if (context instanceof NceSystemConnectionMemo) {
095            initComponents((NceSystemConnectionMemo) context);
096        }
097    }
098
099    /**
100     * {@inheritDoc}
101     */
102    @Override
103    public String getHelpTarget() {
104        return "package.jmri.jmrix.nce.analyzer.NcePacketMonitorFrame";
105    }
106
107    /**
108     * {@inheritDoc}
109     */
110    @Override
111    public String getTitle() {
112        StringBuilder x = new StringBuilder();
113        if (memo != null) {
114            x.append(memo.getUserName());
115        } else {
116            x.append("NCE_");
117        }
118        x.append(": ");
119        x.append(Bundle.getMessage("Title"));
120        return x.toString();
121    }
122    
123    /**
124     * {@inheritDoc}
125     */
126    @Override
127    public void initComponents(NceSystemConnectionMemo m) {
128        this.memo = m;
129
130        // populate the GUI, invoked as part of startup
131        enableDisableWhenOpen(false);
132        // load the port selection part
133        portBox.setToolTipText(Bundle.getMessage("PortBoxToolTip"));
134        portBox.setAlignmentX(JLabel.LEFT_ALIGNMENT);
135        Vector<String> v = getPortNames();
136        for (int i = 0; i < v.size(); i++) {
137            portBox.addItem(v.elementAt(i));
138        }
139        // offer model choice
140        modelBox.setToolTipText(Bundle.getMessage("ModelBoxToolTip"));
141        modelBox.setAlignmentX(LEFT_ALIGNMENT);
142        for (int i = 0; i < validModelNames.length; i++) {
143            modelBox.addItem(validModelNames[i]);
144        }
145        openButton.setToolTipText(Bundle.getMessage("OpenButtonToolTip"));
146        openButton.addActionListener(new java.awt.event.ActionListener() {
147            @Override
148            public void actionPerformed(java.awt.event.ActionEvent evt) {
149                try {
150                    openPortButtonActionPerformed(evt);
151                } catch (java.lang.UnsatisfiedLinkError ex) {
152                    log.error("Error while opening port.  Did you select the right one?\nException: ", ex);
153                }
154            }
155        });
156        closePortButton.setToolTipText(Bundle.getMessage("CloseButtonToolTip"));
157        closePortButton.addActionListener(new java.awt.event.ActionListener() {
158            @Override
159            public void actionPerformed(java.awt.event.ActionEvent evt) {
160                try {
161                    closePortButtonActionPerformed();
162                } catch (java.lang.UnsatisfiedLinkError ex) {
163                    log.error("Error while closing port.  Did you select the right one?\\nException: ", ex);
164                }
165            }
166        });
167        {
168            JSeparator js = new JSeparator();
169            js.setMaximumSize(new Dimension(10000, 10));
170            add(js);
171        }
172        {
173            JPanel p1 = new JPanel();
174            p1.setLayout(new FlowLayout());
175            p1.add(new JLabel(Bundle.getMessage("SerialPortLabel")));
176            p1.add(portBox);
177            p1.add(new JLabel(Bundle.getMessage("ModelBoxLabel")));
178            p1.add(modelBox);
179            p1.add(openButton);
180            p1.add(closePortButton);
181            //p1.setMaximumSize(p1.getPreferredSize());
182            add(p1);
183        }
184
185        // add user part of GUI
186        {
187            JSeparator js = new JSeparator();
188            js.setMaximumSize(new Dimension(10000, 10));
189            add(js);
190        }
191        JPanel p2 = new JPanel();
192        JPanel p2A = new JPanel();
193        p2A.setLayout(new BoxLayout(p2A, BoxLayout.Y_AXIS));
194        JPanel p2B = new JPanel();
195        JPanel p2C = new JPanel();
196        JPanel p2D = new JPanel();
197        ButtonGroup gD = new ButtonGroup();
198        {   // begin dup group
199            JPanel p = new JPanel();
200            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
201            dupFilterCheckBox.setToolTipText(Bundle.getMessage("DupFilterCheckBoxToolTip"));
202            p.add(dupFilterCheckBox);
203            p2.add(p);
204        }   // end dup group
205        
206        {   // begin verbose group
207            JPanel p = new JPanel();
208            p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
209            verboseButton.setToolTipText(Bundle.getMessage("VerboseButtonToolTip"));
210            gD.add(verboseButton);
211            p.add(verboseButton);
212            verboseButton.addActionListener(new java.awt.event.ActionListener() {
213                @Override
214                public void actionPerformed(java.awt.event.ActionEvent evt) {
215                    sendBytes(new byte[]{(byte) 'V'});
216                }
217            });
218            p2A.add(p);
219        }   // end verbose group
220
221        {   // begin old hex group
222            JPanel p = new JPanel();
223            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
224            origHex0Button.setToolTipText(Bundle.getMessage("OrigHex0ButtonToolTip"));
225            gD.add(origHex0Button);
226            p.add(origHex0Button);
227            origHex0Button.addActionListener(new java.awt.event.ActionListener() {
228                @Override
229                public void actionPerformed(java.awt.event.ActionEvent evt) {
230                    sendBytes(new byte[]{(byte) 'H', (byte) '0'});
231                }
232            });
233            p2B.add(p);
234            origHex1Button.setToolTipText(Bundle.getMessage("OrigHex1ButtonToolTip"));
235            gD.add(origHex1Button);
236            p.add(origHex1Button);
237            origHex1Button.addActionListener(new java.awt.event.ActionListener() {
238                @Override
239                public void actionPerformed(java.awt.event.ActionEvent evt) {
240                    sendBytes(new byte[]{(byte) 'H', (byte) '1'});
241                }
242            });
243            p2B.add(p);
244            origHex2Button.setToolTipText(Bundle.getMessage("OrigHex2ButtonToolTip"));
245            gD.add(origHex2Button);
246            p.add(origHex2Button);
247            origHex2Button.addActionListener(new java.awt.event.ActionListener() {
248                @Override
249                public void actionPerformed(java.awt.event.ActionEvent evt) {
250                    sendBytes(new byte[]{(byte) 'H', (byte) '2'});
251                }
252            });
253            p2.add(p);
254            origHex3Button.setToolTipText(Bundle.getMessage("OrigHex3ButtonToolTip"));
255            gD.add(origHex3Button);
256            p.add(origHex3Button);
257            origHex3Button.addActionListener(new java.awt.event.ActionListener() {
258                @Override
259                public void actionPerformed(java.awt.event.ActionEvent evt) {
260                    sendBytes(new byte[]{(byte) 'H', (byte) '3'});
261                }
262            });
263            p2B.add(p);
264            origHex4Button.setToolTipText(Bundle.getMessage("OrigHex4ButtonToolTip"));
265            gD.add(origHex4Button);
266            p.add(origHex4Button);
267            origHex4Button.addActionListener(new java.awt.event.ActionListener() {
268                @Override
269                public void actionPerformed(java.awt.event.ActionEvent evt) {
270                    sendBytes(new byte[]{(byte) 'H', (byte) '4'});
271                }
272            });
273            p2.add(p);
274            origHex5Button.setToolTipText(Bundle.getMessage("OrigHex5ButtonToolTip"));
275            gD.add(origHex5Button);
276            p.add(origHex5Button);
277            origHex5Button.addActionListener(new java.awt.event.ActionListener() {
278                @Override
279                public void actionPerformed(java.awt.event.ActionEvent evt) {
280                    sendBytes(new byte[]{(byte) 'H', (byte) '5'});
281                }
282            });
283            p2B.add(p);
284        }  // end old hex group
285
286        {   // begin new hex group
287            JPanel p = new JPanel();
288            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
289            newHex0Button.setToolTipText(Bundle.getMessage("NewHex0ButtonToolTip"));
290            gD.add(newHex0Button);
291            p.add(newHex0Button);
292            newHex0Button.addActionListener(new java.awt.event.ActionListener() {
293                @Override
294                public void actionPerformed(java.awt.event.ActionEvent evt) {
295                    sendBytes(new byte[]{(byte) 'H', (byte) '0'});
296                }
297            });
298            p2C.add(p);
299            newHex1Button.setToolTipText(Bundle.getMessage("NewHex1ButtonToolTip"));
300            gD.add(newHex1Button);
301            p.add(newHex1Button);
302            newHex1Button.addActionListener(new java.awt.event.ActionListener() {
303                @Override
304                public void actionPerformed(java.awt.event.ActionEvent evt) {
305                    sendBytes(new byte[]{(byte) 'H', (byte) '1'});
306                }
307            });
308            p2C.add(p);
309        }  // end new hex group
310        p2D.setLayout(new BoxLayout(p2D, BoxLayout.X_AXIS));
311        p2D.add(p2B);
312        p2D.add(p2C);
313        p2A.add(p2D);
314        p2.add(p2A);
315
316        { // start on
317            JPanel p = new JPanel();
318            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
319            accOnButton.setToolTipText(Bundle.getMessage("AccOnButtonToolTip"));
320            p.add(accOnButton);
321            accOnButton.addActionListener(new java.awt.event.ActionListener() {
322                @Override
323                public void actionPerformed(java.awt.event.ActionEvent evt) {
324                    if (accOnButton.isSelected()) {
325                        sendBytes(new byte[]{(byte) 'A', (byte) '+'});
326                    } else {
327                        sendBytes(new byte[]{(byte) 'A', (byte) '-'});
328                    }
329                }
330            });
331            idleOnButton.setToolTipText(Bundle.getMessage("IdleOnButtonToolTip"));
332            p.add(idleOnButton);
333            idleOnButton.addActionListener(new java.awt.event.ActionListener() {
334                @Override
335                public void actionPerformed(java.awt.event.ActionEvent evt) {
336                    if (idleOnButton.isSelected()) {
337                        sendBytes(new byte[]{(byte) 'I', (byte) '+'});
338                    } else {
339                        sendBytes(new byte[]{(byte) 'I', (byte) '-'});
340                    }
341                }
342            });
343            locoOnButton.setToolTipText(Bundle.getMessage("LocoOnButtonToolTip"));
344            p.add(locoOnButton);
345            locoOnButton.addActionListener(new java.awt.event.ActionListener() {
346                @Override
347                public void actionPerformed(java.awt.event.ActionEvent evt) {
348                    if (locoOnButton.isSelected()) {
349                        sendBytes(new byte[]{(byte) 'L', (byte) '+'});
350                    } else {
351                        sendBytes(new byte[]{(byte) 'L', (byte) '-'});
352                    }
353                }
354            });
355            resetOnButton.setToolTipText(Bundle.getMessage("ResetOnButtonToolTip"));
356            p.add(resetOnButton);
357            resetOnButton.addActionListener(new java.awt.event.ActionListener() {
358                @Override
359                public void actionPerformed(java.awt.event.ActionEvent evt) {
360                    if (resetOnButton.isSelected()) {
361                        sendBytes(new byte[]{(byte) 'R', (byte) '+'});
362                    } else {
363                        sendBytes(new byte[]{(byte) 'R', (byte) '-'});
364                    }
365                }
366            });
367            signalOnButton.setToolTipText(Bundle.getMessage("SignalOnButtonToolTip"));
368            p.add(signalOnButton);
369            signalOnButton.addActionListener(new java.awt.event.ActionListener() {
370                @Override
371                public void actionPerformed(java.awt.event.ActionEvent evt) {
372                    if (signalOnButton.isSelected()) {
373                        sendBytes(new byte[]{(byte) 'S', (byte) '+'});
374                    } else {
375                        sendBytes(new byte[]{(byte) 'S', (byte) '-'});
376                    }
377                }
378            });
379            p2.add(p);
380        }  // end on
381
382        { // Monitor command acc single/double
383            JPanel p = new JPanel();
384            p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
385            JLabel t = new JLabel(Bundle.getMessage("MonitorCmdLabel"));
386            p.add(t);
387            p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
388            ButtonGroup gA = new ButtonGroup();
389            accSingleButton.setToolTipText(Bundle.getMessage("AccSingleButtonToolTip"));
390            gA.add(accSingleButton);
391            p.add(accSingleButton);
392            accSingleButton.addActionListener(new java.awt.event.ActionListener() {
393                @Override
394                public void actionPerformed(java.awt.event.ActionEvent evt) {
395                    sendBytes(new byte[]{(byte) 'A', (byte) 'S'});
396                }
397            });
398            accPairedButton.setToolTipText(Bundle.getMessage("AccPairedButtonToolTip"));
399            gA.add(accPairedButton);
400            p.add(accPairedButton);
401            accPairedButton.addActionListener(new java.awt.event.ActionListener() {
402                @Override
403                public void actionPerformed(java.awt.event.ActionEvent evt) {
404                    sendBytes(new byte[]{(byte) 'A', (byte) 'P'});
405                }
406            });
407            p2.add(p);
408        }  // end acc single/double
409
410        p2.setMaximumSize(p2.getPreferredSize());
411        JScrollPane ps = new JScrollPane(p2);
412        ps.setMaximumSize(ps.getPreferredSize());
413        ps.setVisible(true);
414        add(ps);
415    }
416
417    /**
418     * Sends stream of bytes to the command station
419     *
420     * @param bytes  array of bytes to send
421     */
422    synchronized void sendBytes(byte[] bytes) {
423        try {
424            // only attempt to send data if output stream is not null (i.e. it
425            // was opened successfully)
426            if (ostream == null) {
427                throw new IOException(
428                        "Unable to send data to command station: output stream is null");
429            } else {
430                for (int i = 0; i < bytes.length; i++) {
431                    ostream.write(bytes[i]);
432                    wait(3);
433                }
434                final byte endbyte = 13;
435                ostream.write(endbyte);
436            }
437        } catch (IOException e) {
438            log.error("Exception on output: ", e);
439        } catch (InterruptedException e) {
440            Thread.currentThread().interrupt(); // retain if needed later
441            log.error("Interrupted output: ", e);
442        }
443    }
444    
445    /**
446     * Enable/Disable options depending on port open/closed status
447     * @param isOpen enables/disables buttons/checkbox when connection is open/closed
448     */
449    void enableDisableWhenOpen(boolean isOpen) {
450        openButton.setEnabled(!isOpen);
451        closePortButton.setEnabled(isOpen);
452        portBox.setEnabled(!isOpen);
453        modelBox.setEnabled(!isOpen);
454        verboseButton.setEnabled(isOpen);
455        if (!isOpen || (modelBox.getSelectedIndex() == MODELORIG)) {
456            origHex0Button.setEnabled(isOpen);
457            origHex1Button.setEnabled(isOpen);
458            origHex2Button.setEnabled(isOpen);
459            origHex3Button.setEnabled(isOpen);
460            origHex4Button.setEnabled(isOpen);
461            origHex5Button.setEnabled(isOpen);
462        }
463        if (!isOpen || (modelBox.getSelectedIndex() == MODELNEW)) {
464            newHex0Button.setEnabled(isOpen);
465            newHex1Button.setEnabled(isOpen);
466        }
467        accOnButton.setEnabled(isOpen);
468        idleOnButton.setEnabled(isOpen);
469        locoOnButton.setEnabled(isOpen);
470        resetOnButton.setEnabled(isOpen);
471        signalOnButton.setEnabled(isOpen);
472        accSingleButton.setEnabled(isOpen);
473        accPairedButton.setEnabled(isOpen);
474    }
475
476    /**
477     * Open button has been pushed, create the actual display connection
478     * @param e open button event
479     */
480    void openPortButtonActionPerformed(java.awt.event.ActionEvent e) {
481        //log.info("Open button pushed");
482        // can't change this anymore
483        String openStatus = openPort((String) portBox.getSelectedItem(), validModelValues[modelBox.getSelectedIndex()], "JMRI");
484        if (openStatus != null) {
485            log.debug("Open Returned: {}", openStatus);
486            return;
487        }
488        // start the reader
489        readerThread = new Thread(new Reader());
490        readerThread.start();
491        readerThread.setName("NCE Packet Monitor");
492        // enable buttons
493        enableDisableWhenOpen(true);
494        //log.info("Open button processing complete");
495    }
496
497    /**
498     * Open button has been pushed, create the actual display connection
499     */
500    void closePortButtonActionPerformed() {
501        //log.info("Close button pushed");
502        if (readerThread != null) {
503            stopThread(readerThread);
504        }
505
506        // release port
507        if (activeSerialPort != null) {
508            activeSerialPort.closePort();
509            log.info("{} port closed", portBox.getSelectedItem());
510        }
511        serialStream = null;
512        ostream = null;
513        activeSerialPort = null;
514        portNameVector = null;
515        // enable buttons
516        enableDisableWhenOpen(false);
517    }
518    
519    Thread readerThread;
520
521    /*
522     * tell the reader thread to close down
523     */
524    void stopThread(Thread t) {
525        t.interrupt();
526    }
527
528    @Override
529    public synchronized void dispose() {
530        // stop operations here. This is a deprecated method, but OK for us.
531        closePortButtonActionPerformed();
532
533        // and clean up parent
534        super.dispose();
535    }
536
537    public Vector<String> getPortNames() {
538        return jmri.jmrix.AbstractSerialPortController.getActualPortNames();
539    }
540
541    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SR_NOT_CHECKED",
542                                        justification="this is for skip-chars while loop: no matter how many, we're skipping")
543    public synchronized String openPort(String portName, int modelValue, String appName) {
544        // open the port, check ability to set moderators
545
546        // get and open the primary port
547        activeSerialPort = com.fazecast.jSerialComm.SerialPort.getCommPort(portName);
548        activeSerialPort.openPort();
549        
550        // set it for communication
551        activeSerialPort.setNumDataBits(modelBitValues[modelValue]);
552        activeSerialPort.setNumStopBits(modelStopValues[modelValue]);
553        activeSerialPort.setParity(modelParityValues[modelValue]);
554        activeSerialPort.setBaudRate(modelBaudRates[modelValue]);
555        
556        // set RTS high, DTR high
557        activeSerialPort.setRTS(); // not connected in some serial ports and adapters
558        activeSerialPort.setDTR(); // pin 1 in DIN8; on main connector, this is DTR
559
560        // disable flow control; hardware lines used for signaling, XON/XOFF might appear in data
561        activeSerialPort.setFlowControl(com.fazecast.jSerialComm.SerialPort.FLOW_CONTROL_DISABLED);
562
563        // set timeout
564        activeSerialPort.setComPortTimeouts(com.fazecast.jSerialComm.SerialPort.TIMEOUT_READ_BLOCKING, 0, 0);
565
566        // get and save stream
567        serialStream = new DataInputStream(activeSerialPort.getInputStream());
568        ostream = activeSerialPort.getOutputStream();
569
570        // purge contents, if any
571        try {
572            int count = serialStream.available();
573            log.debug("input stream shows {} bytes available", count);
574            while (count > 0) {
575                serialStream.skip(count);
576                count = serialStream.available();
577            }
578        } catch (IOException e) {
579            log.error("problem purging port at startup", e);
580        }
581
582        // report status?
583        if (log.isInfoEnabled()) {
584            log.info("Port {} {} opened at {} baud, sees DTR: {} RTS: {} DSR: {} CTS: {} DCD: {}", 
585                    portName, activeSerialPort.getDescriptivePortName(),
586                    activeSerialPort.getBaudRate(), activeSerialPort.getDTR(), 
587                    activeSerialPort.getRTS(), activeSerialPort.getDSR(), activeSerialPort.getCTS(),
588                    activeSerialPort.getDCD());
589        }
590
591        return null; // indicates OK return
592    }
593
594    DataInputStream serialStream = null;
595    OutputStream ostream = null;
596
597    /**
598     * Internal class to handle the separate character-receive thread
599     *
600     */
601    class Reader implements Runnable {
602
603        /**
604         * Handle incoming characters. This is a permanent loop, looking for
605         * input messages in character form on the stream connected to the
606         * PortController via <code>connectPort</code>. Terminates with the
607         * input stream breaking out of the try block.
608         */
609        @Override
610        public void run() {
611            // have to limit verbosity!
612
613            while (true) {   // loop permanently, stream close will exit via exception
614                try {
615                    handleIncomingData();
616                } catch (java.io.EOFException e) {
617                    log.info("{} thread ending, port closed", Thread.currentThread().getName());
618                    return;
619                } catch (java.io.IOException e) {
620                    log.warn("{} thread ending: Exception: {}", Thread.currentThread().getName(), e.toString());
621                    return;
622                }
623            }
624        }
625
626        static final int maxMsg = 80;
627        StringBuffer msg;
628        private int duplicates = 0;
629        String msgString;
630        String matchString = "";
631
632        void handleIncomingData() throws java.io.IOException {
633            // we sit in this until the message is complete, relying on
634            // threading to let other stuff happen
635
636            // Create output message
637            msg = new StringBuffer(maxMsg);
638            // message exists, now fill it
639            int i;
640            for (i = 0; i < maxMsg; i++) {
641                char char1 = (char) serialStream.readByte();
642                if (char1 == 13) {  // 13 is the CR at the end; done this
643                    // way to be coding-independent
644                    break;
645                }
646                msg.append(char1);
647            }
648
649            // create the String to display (as String has .equals)
650            msgString = msg.toString();
651
652            // is this a duplicate?
653            if (msgString.equals(matchString) && dupFilterCheckBox.isSelected()) {
654                // yes, keep count
655                duplicates++;
656            } else {
657                // no, message is complete, dispatch it!!
658                if (!msgString.equals(matchString) && dupFilterCheckBox.isSelected() && (duplicates > 0)) {
659                    // prepend the duplicate info
660                    String dupString = matchString + " [" + duplicates + "]\n";
661                    // return a notification via the queue to ensure end
662                    Runnable r = new Runnable() {
663                        @Override
664                        public void run() {
665                            nextLine(dupString, "");
666                        }
667                    };
668                    javax.swing.SwingUtilities.invokeLater(r);
669                }
670                duplicates = 0;
671                matchString = msgString;
672                msgString = msgString + "\n";
673                // return a notification via the queue to ensure end
674                Runnable r = new Runnable() {
675                    @Override
676                    public void run() {
677                        nextLine(msgString, "");
678                    }
679                };
680                javax.swing.SwingUtilities.invokeLater(r);
681            }
682        }
683
684    } // end class Reader
685
686    /**
687     * Nested class to create one of these using old-style defaults
688     */
689    static public class Default extends jmri.jmrix.nce.swing.NceNamedPaneAction {
690
691        public Default() {
692            super("Open NCE DCC Packet Analyzer",
693                    new jmri.util.swing.sdi.JmriJFrameInterface(),
694                    NcePacketMonitorPanel.class.getName(),
695                    jmri.InstanceManager.getDefault(NceSystemConnectionMemo.class));
696        }
697    }
698
699    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NcePacketMonitorPanel.class);
700}