001package jmri.jmrix.pricom.downloader;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.FlowLayout;
005import java.io.DataInputStream;
006import java.io.IOException;
007import java.io.OutputStream;
008import java.util.Vector;
009
010import javax.swing.AbstractAction;
011import javax.swing.BoxLayout;
012import javax.swing.JButton;
013import javax.swing.JComboBox;
014import javax.swing.JFileChooser;
015import javax.swing.JLabel;
016import javax.swing.JPanel;
017import javax.swing.JProgressBar;
018import javax.swing.JSeparator;
019import javax.swing.JTextArea;
020
021import purejavacomm.*;
022
023/**
024 * Pane for downloading software updates to PRICOM products
025 *
026 * @author Bob Jacobsen Copyright (C) 2005
027 */
028public class LoaderPane extends javax.swing.JPanel {
029
030    Vector<String> portNameVector = null;
031    SerialPort activeSerialPort = null;
032
033    Thread readerThread;
034    //private     boolean opened = false;
035    DataInputStream serialStream = null;
036
037    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC",
038            justification = "Class is no longer active, no hardware with which to test fix")
039    OutputStream ostream = null;
040
041    final JComboBox<String> portBox = new JComboBox<>();
042    final JButton openPortButton = new JButton();
043    final JTextArea traffic = new JTextArea();
044
045    final JFileChooser chooser = jmri.jmrit.XmlFile.userFileChooser();
046    final JButton fileButton;
047    final JLabel inputFileName = new JLabel("");
048    final JTextArea comment = new JTextArea();
049
050    final JButton loadButton;
051    final JProgressBar bar;
052    final JLabel status = new JLabel("");
053
054    PdiFile pdiFile;
055
056    // populate the com port part of GUI, invoked as part of startup
057    protected void addCommGUI() {
058        // load the port selection part
059        portBox.setToolTipText(Bundle.getMessage("TipSelectPort"));
060        portBox.setAlignmentX(JLabel.LEFT_ALIGNMENT);
061        Vector<String> v = getPortNames();
062
063        for (int i = 0; i < v.size(); i++) {
064            portBox.addItem(v.elementAt(i));
065        }
066
067        openPortButton.setText(Bundle.getMessage("ButtonOpen"));
068        openPortButton.setToolTipText(Bundle.getMessage("TipOpenPort"));
069        openPortButton.addActionListener(evt -> {
070            try {
071                openPortButtonActionPerformed(evt);
072            } catch (UnsatisfiedLinkError ex) {
073                log.error("Error while opening port. Did you select the right one?", ex);
074            }
075        });
076
077        JPanel p1 = new JPanel();
078        p1.setLayout(new FlowLayout());
079        p1.add(new JLabel(Bundle.getMessage("LabelSerialPort")));
080        p1.add(portBox);
081        p1.add(openPortButton);
082        add(p1);
083
084        {
085            JPanel p = new JPanel();
086            p.setLayout(new FlowLayout());
087            JLabel l = new JLabel(Bundle.getMessage("LabelTraffic"));
088            l.setAlignmentX(JLabel.LEFT_ALIGNMENT);
089            p.add(l);
090            add(p);
091        }
092
093        traffic.setEditable(false);
094        traffic.setEnabled(true);
095        traffic.setText("\n\n\n\n"); // just to save some space
096        add(traffic);
097    }
098
099    /**
100     * Open button has been pushed, create the actual display connection
101     * @param e Event from pressed button
102     */
103    void openPortButtonActionPerformed(java.awt.event.ActionEvent e) {
104        log.info("Open button pushed");
105        // can't change this anymore
106        openPortButton.setEnabled(false);
107        portBox.setEnabled(false);
108        // Open the port
109        openPort((String) portBox.getSelectedItem(), "JMRI");
110        //
111        status.setText(Bundle.getMessage("StatusSelectFile"));
112        fileButton.setEnabled(true);
113        fileButton.setToolTipText(Bundle.getMessage("TipFileEnabled"));
114        log.info("Open button processing complete");
115    }
116
117    synchronized void sendBytes(byte[] bytes) {
118        log.debug("Send {}: {}", bytes.length, jmri.util.StringUtil.hexStringFromBytes(bytes));
119        try {
120            // send the STX at the start
121            byte startbyte = 0x02;
122            ostream.write(startbyte);
123
124            // send the rest of the bytes
125            for (byte aByte : bytes) {
126                // expand as needed
127                switch (aByte) {
128                    case 0x01:
129                    case 0x02:
130                    case 0x03:
131                    case 0x06:
132                    case 0x15:
133                        ostream.write(0x01);
134                        ostream.write(aByte + 64);
135                        break;
136                    default:
137                        ostream.write(aByte);
138                        break;
139                }
140            }
141
142            byte endbyte = 0x03;
143            ostream.write(endbyte);
144        } catch (java.io.IOException e) {
145            log.error("Exception on output", e);
146        }
147    }
148
149    /**
150     * Internal class to handle the separate character-receive thread
151     *
152     */
153    class LocalReader extends Thread {
154
155        /**
156         * Handle incoming characters. This is a permanent loop, looking for
157         * input messages in character form on the stream connected to the
158         * PortController via <code>connectPort</code>. Terminates with the
159         * input stream breaking out of the try block.
160         */
161        @Override
162        public void run() {
163            // have to limit verbosity!
164
165            try {
166                nibbleIncomingData();            // remove any pending chars in queue
167            } catch (java.io.IOException e) {
168                log.warn("nibble: Exception", e);
169            }
170            while (true) {   // loop permanently, stream close will exit via exception
171                try {
172                    handleIncomingData();
173                } catch (java.io.IOException e) {
174                    log.warn("run: Exception", e);
175                }
176            }
177        }
178
179        static final int maxMsg = 80;
180        byte[] inBuffer;
181
182        @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SR_NOT_CHECKED",
183                                            justification="this is for skip-chars while loop: no matter how many, we're skipping")
184        void nibbleIncomingData() throws java.io.IOException {
185            long nibbled = 0;                         // total chars chucked
186            serialStream = new DataInputStream(activeSerialPort.getInputStream());
187            ostream = activeSerialPort.getOutputStream();
188
189            // purge contents, if any
190            int count = serialStream.available();     // check for pending chars
191            while (count > 0) {                      // go until gone
192                serialStream.skip(count);             // skip the pending chars
193                nibbled += count;                     // add on this pass count
194                count = serialStream.available();     // any more left?
195            }
196            log.debug("nibbled {} from input stream", nibbled);
197        }
198
199        void handleIncomingData() throws java.io.IOException {
200            // we sit in this until the message is complete, relying on
201            // threading to let other stuff happen
202
203            StringBuffer mbuff = new StringBuffer();
204            // wait for start of message
205            int dataChar;
206            while ((dataChar = serialStream.readByte()) != 0x02) {
207                mbuff.append(dataChar);
208                log.debug(" rcv char {}", dataChar);
209                if (dataChar == 0x0d) {
210                    // Queue the string for display
211                    javax.swing.SwingUtilities.invokeLater(new Notify(mbuff));
212                }
213            }
214
215            // Create output message
216            inBuffer = new byte[maxMsg];
217
218            // message started, now store it in buffer
219            int i;
220            for (i = 0; i < maxMsg; i++) {
221                byte char1 = serialStream.readByte();
222                if (char1 == 0x03) {  // 0x03 is the end of message
223                    break;
224                }
225                inBuffer[i] = char1;
226            }
227            log.debug("received {} bytes {}", (i + 1), jmri.util.StringUtil.hexStringFromBytes(inBuffer));
228
229            // and process the message for possible replies, etc
230            nextMessage(inBuffer, i);
231        }
232
233        int msgCount = 0;
234        int msgSize = 64;
235        boolean init = false;
236
237        /**
238         * Send the next message of the download.
239         * @param buffer holds message to be sent
240         * @param length length of message within buffer
241         */
242        void nextMessage(byte[] buffer, int length) {
243
244            // if first message, get size & start
245            if (isUploadReady(buffer)) {
246                msgSize = getDataSize(buffer);
247                init = true;
248            }
249
250            // if not initialized yet, just ignore message
251            if (!init) {
252                return;
253            }
254
255            // see if its a request for more data
256            if (!(isSendNext(buffer) || isUploadReady(buffer))) {
257                log.debug("extra message, ignore");
258                return;
259            }
260
261            // update progress bar via the queue to ensure synchronization
262            Runnable r = this::updateGUI;
263            javax.swing.SwingUtilities.invokeLater(r);
264
265            // get the next message
266            byte[] outBuffer = pdiFile.getNext(msgSize);
267
268            // if really a message, send it
269            if (outBuffer != null) {
270                javax.swing.SwingUtilities.invokeLater(new Notify(outBuffer));
271                CRC_block(outBuffer);
272                sendBytes(outBuffer);
273                return;
274            }
275
276            // if here, no next message, send end
277            outBuffer = bootMessage();
278            sendBytes(outBuffer);
279
280            // signal end to GUI via the queue to ensure synchronization
281            r = this::enableGUI;
282            javax.swing.SwingUtilities.invokeLater(r);
283
284            // stop this thread
285            stopThread(readerThread);
286
287        }
288
289        /**
290         * Update the GUI for progress
291         * <p>
292         * Should be invoked on the Swing thread
293         */
294        void updateGUI() {
295            log.debug("updateGUI with {} / {}", msgCount, (pdiFile.length() / msgSize));
296            if (!init) {
297                return;
298            }
299
300            status.setText(Bundle.getMessage("StatusDownloading"));
301            // update progress bar
302            msgCount++;
303            bar.setValue(100 * msgCount * msgSize / pdiFile.length());
304
305        }
306
307        /**
308         * Signal GUI that it's the end of the download
309         * <p>
310         * Should be invoked on the Swing thread
311         */
312        void enableGUI() {
313            log.debug("enableGUI");
314            if (!init) {
315                log.error("enableGUI with init false");
316            }
317
318            // enable GUI
319            loadButton.setEnabled(true);
320            loadButton.setToolTipText(Bundle.getMessage("TipLoadEnabled"));
321            status.setText(Bundle.getMessage("StatusDone"));
322        }
323
324        class Notify implements Runnable {
325
326            Notify(StringBuffer b) {
327                message = b.toString();
328            }
329
330            Notify(byte[] b) {
331                message = jmri.util.StringUtil.hexStringFromBytes(b);
332            }
333
334            Notify(byte[] b, int length) {
335                byte[] temp = new byte[length];
336                for (int i = 0; i < length; i++) {
337                    temp[i] = b[i];
338                }
339                message = jmri.util.StringUtil.hexStringFromBytes(temp);
340            }
341
342            final String message;
343
344            /**
345             * when invoked, format and display the message
346             */
347            @Override
348            public void run() {
349                traffic.setText(message);
350            }
351        } // end class Notify
352    } // end class LocalReader
353
354    // use deprecated stop method to stop thread,
355    // which will be sitting waiting for input
356    @SuppressWarnings("deprecation") // Thread.stop
357    void stopThread(Thread t) {
358        t.stop();
359    }
360
361    public void dispose() {
362        // stop operations if in process
363        if (readerThread != null) {
364            stopThread(readerThread);
365        }
366
367        // release port
368        if (activeSerialPort != null) {
369            activeSerialPort.close();
370        }
371        serialStream = null;
372        ostream = null;
373        activeSerialPort = null;
374        portNameVector = null;
375        //opened = false;
376    }
377
378    public Vector<String> getPortNames() {
379        return jmri.jmrix.AbstractSerialPortController.getActualPortNames();
380    }
381
382    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SR_NOT_CHECKED",
383                                        justification="this is for skip-chars while loop: no matter how many, we're skipping")
384    public String openPort(String portName, String appName) {
385        // open the port, check ability to set moderators
386        try {
387            // get and open the primary port
388            CommPortIdentifier portID = CommPortIdentifier.getPortIdentifier(portName);
389            try {
390                activeSerialPort = (SerialPort) portID.open(appName, 2000);  // name of program, msec to wait
391            } catch (PortInUseException p) {
392                handlePortBusy(p, portName);
393                return "Port " + p + " already in use";
394            }
395
396            // try to set it for communication via SerialDriver
397            try {
398                // get selected speed
399                int speed = 9600;
400                // Doc says 7 bits, but 8 seems needed
401                activeSerialPort.setSerialPortParams(speed, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
402            } catch (UnsupportedCommOperationException e) {
403                log.error("Cannot set serial parameters on port {}: {}", portName, e.getMessage());
404                return "Cannot set serial parameters on port " + portName + ": " + e.getMessage();
405            }
406
407            // set RTS high, DTR high
408            activeSerialPort.setRTS(true); // not connected in some serial ports and adapters
409            activeSerialPort.setDTR(true); // pin 1 in DIN8; on main connector, this is DTR
410
411            // disable flow control; hardware lines used for signaling, XON/XOFF might appear in data
412            activeSerialPort.setFlowControlMode(0);
413
414            // set timeout
415            log.debug("Serial timeout was observed as: {} {}", activeSerialPort.getReceiveTimeout(),
416                    activeSerialPort.isReceiveTimeoutEnabled());
417
418            // get and save stream
419            serialStream = new DataInputStream(activeSerialPort.getInputStream());
420            ostream = activeSerialPort.getOutputStream();
421
422            // purge contents, if any
423            int count = serialStream.available();
424            log.debug("input stream shows {} bytes available", count);
425            while (count > 0) {
426                serialStream.skip(count);
427                count = serialStream.available();
428            }
429
430            // report status?
431            if (log.isInfoEnabled()) {
432                log.info("{} port opened at {} baud, sees  DTR: {} RTS: {} DSR: {} CTS: {}  CD: {}",
433                        portName, activeSerialPort.getBaudRate(), activeSerialPort.isDTR(),
434                        activeSerialPort.isRTS(), activeSerialPort.isDSR(), activeSerialPort.isCTS(),
435                        activeSerialPort.isCD());
436            }
437
438            //opened = true;
439        } catch (NoSuchPortException | UnsupportedCommOperationException | IOException | RuntimeException ex) {
440            log.error("Unexpected exception while opening port {}", portName, ex);
441            return "Unexpected error while opening port " + portName + ": " + ex;
442        }
443        return null; // indicates OK return
444    }
445
446    void handlePortBusy(PortInUseException p, String port) {
447        log.error("Port {} in use, cannot open", port, p);
448    }
449
450    public LoaderPane() {
451        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
452
453        addCommGUI();
454
455        add(new JSeparator());
456
457        {
458            JPanel p = new JPanel();
459            p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
460
461            fileButton = new JButton(Bundle.getMessage("ButtonSelect"));
462            fileButton.setEnabled(false);
463            fileButton.setToolTipText(Bundle.getMessage("TipFileDisabled"));
464            fileButton.addActionListener(new AbstractAction() {
465                @Override
466                public void actionPerformed(java.awt.event.ActionEvent e) {
467                    selectInputFile();
468                }
469            });
470            p.add(fileButton);
471            p.add(new JLabel(Bundle.getMessage("LabelInpFile")));
472            p.add(inputFileName);
473
474            add(p);
475        }
476
477        {
478            JPanel p = new JPanel();
479            p.setLayout(new FlowLayout());
480            JLabel l = new JLabel(Bundle.getMessage("LabelFileComment"));
481            l.setAlignmentX(JLabel.LEFT_ALIGNMENT);
482            p.add(l);
483            add(p);
484        }
485
486        comment.setEditable(false);
487        comment.setEnabled(true);
488        comment.setText("\n\n\n\n"); // just to save some space
489        add(comment);
490
491        add(new JSeparator());
492
493        {
494            JPanel p = new JPanel();
495            p.setLayout(new FlowLayout());
496
497            loadButton = new JButton(Bundle.getMessage("ButtonDownload"));
498            loadButton.setEnabled(false);
499            loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled"));
500            p.add(loadButton);
501            loadButton.addActionListener(new AbstractAction() {
502                @Override
503                public void actionPerformed(java.awt.event.ActionEvent e) {
504                    doLoad();
505                }
506            });
507
508            add(p);
509        }
510
511        bar = new JProgressBar();
512        add(bar);
513
514        add(new JSeparator());
515
516        {
517            JPanel p = new JPanel();
518            p.setLayout(new FlowLayout());
519            status.setText(Bundle.getMessage("StatusSelectPort"));
520            status.setAlignmentX(JLabel.LEFT_ALIGNMENT);
521            p.add(status);
522            add(p);
523        }
524    }
525
526    void selectInputFile() {
527        chooser.rescanCurrentDirectory();
528        int retVal = chooser.showOpenDialog(this);
529        if (retVal != JFileChooser.APPROVE_OPTION) {
530            return;  // give up if no file selected
531        }
532        inputFileName.setText(chooser.getSelectedFile().getPath());
533
534        // now read the file
535        pdiFile = new PdiFile(chooser.getSelectedFile());
536        try {
537            pdiFile.open();
538        } catch (IOException e) {
539            log.error("Error opening file", e);
540        }
541
542        comment.setText(pdiFile.getComment());
543        status.setText(Bundle.getMessage("StatusDoDownload"));
544        loadButton.setEnabled(true);
545        loadButton.setToolTipText(Bundle.getMessage("TipLoadEnabled"));
546        validate();
547    }
548
549    void doLoad() {
550        status.setText(Bundle.getMessage("StatusRestartUnit"));
551        loadButton.setEnabled(false);
552        loadButton.setToolTipText(Bundle.getMessage("TipLoadGoing"));
553        // start read/write thread
554        readerThread = new LocalReader();
555        readerThread.start();
556    }
557
558    long CRC_char(long crcin, byte ch) {
559        long crc;
560
561        crc = crcin;                    // copy incoming for local use
562
563        crc = swap(crc);                // swap crc bytes
564        crc ^= ((long) ch & 0xff);       // XOR on the byte, no sign extension
565        crc ^= ((crc & 0xFF) >> 4);
566
567        /*  crc:=crc xor (swap(lo(crc)) shl 4) xor (lo(crc) shl 5);     */
568        crc = (crc ^ (swap((crc & 0xFF)) << 4)) ^ ((crc & 0xFF) << 5);
569        crc &= 0xffff;                  // make sure to mask off anything above 16 bits
570        return crc;
571    }
572
573    long swap(long val) {
574        long low = val & 0xFF;
575        long high = (val >> 8) & 0xFF;
576        return low * 256 + high;
577    }
578
579    /**
580     * Insert the CRC for a block of characters in a buffer
581     * <p>
582     * The last two bytes of the buffer hold the checksum, and are not included
583     * in the checksum.
584     * @param buffer Buffer holding the message to be get a CRC
585     */
586    void CRC_block(byte[] buffer) {
587        long crc = 0;
588
589        for (int r = 0; r < buffer.length - 2; r++) {
590            crc = CRC_char(crc, buffer[r]); // do this character
591        }
592
593        // store into buffer
594        byte high = (byte) ((crc >> 8) & 0xFF);
595        byte low = (byte) (crc & 0xFF);
596        buffer[buffer.length - 2] = low;
597        buffer[buffer.length - 1] = high;
598    }
599
600    /**
601     * Check to see if message starts transmission
602     * @param buffer Buffer holding the message to be checked
603     * @return True if buffer is a upload-ready message
604     */
605    boolean isUploadReady(byte[] buffer) {
606        if (buffer[0] != 31) {
607            return false;
608        }
609        if (buffer[1] != 32) {
610            return false;
611        }
612        if (buffer[2] != 99) {
613            return false;
614        }
615        if (buffer[3] != 00) {
616            return false;
617        }
618        return (buffer[4] == 44) || (buffer[4] == 45);
619    }
620
621    /**
622     * Check to see if this is a request for the next block
623     * @param buffer Buffer holding the message to be checked
624     * @return True if buffer is a sent-next message
625     */
626    boolean isSendNext(byte[] buffer) {
627        if (buffer[0] != 31) {
628            return false;
629        }
630        if (buffer[1] != 32) {
631            return false;
632        }
633        if (buffer[2] != 99) {
634            return false;
635        }
636        if (buffer[3] != 00) {
637            return false;
638        }
639        if (buffer[4] != 22) {
640            return false;
641        }
642        log.debug("OK isSendNext");
643        return true;
644    }
645
646    /**
647     * Get output data length from 1st message
648     *
649     * @param buffer Message from which length is to be extracted
650     * @return length of the buffer
651     */
652    int getDataSize(byte[] buffer) {
653        if (buffer[4] == 44) {
654            return 64;
655        }
656        if (buffer[4] == 45) {
657            return 128;
658        }
659        log.error("Bad length byte: {}", buffer[3]);
660        return 64;
661    }
662
663    /**
664     * Return a properly formatted boot message, complete with CRC
665     * @return buffer Contains boot message that's been created
666     */
667    byte[] bootMessage() {
668        byte[] buffer = new byte[]{99, 0, 0, 0, 0};
669        CRC_block(buffer);
670        return buffer;
671    }
672
673    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoaderPane.class);
674
675}