001package jmri.jmrix.pricom.downloader;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.FlowLayout;
006import java.io.DataInputStream;
007import java.io.IOException;
008import java.io.OutputStream;
009import java.util.Vector;
010
011import javax.swing.AbstractAction;
012import javax.swing.BoxLayout;
013import javax.swing.JButton;
014import javax.swing.JComboBox;
015import javax.swing.JFileChooser;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.JProgressBar;
019import javax.swing.JSeparator;
020import javax.swing.JTextArea;
021
022import jmri.jmrix.purejavacomm.*;
023
024/**
025 * Pane for downloading software updates to PRICOM products
026 *
027 * @author Bob Jacobsen Copyright (C) 2005
028 */
029public class LoaderPane extends javax.swing.JPanel {
030
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();
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    void stopThread() {
355        if (activeSerialPort != null) {
356            activeSerialPort.close();
357        }
358    }
359
360    public void dispose() {
361        // stop operations if in process
362        if (readerThread != null) {
363            stopThread();
364        }
365
366        // release port
367        if (activeSerialPort != null) {
368            activeSerialPort.close();
369        }
370        serialStream = null;
371        ostream = null;
372        activeSerialPort = null;
373        //opened = false;
374    }
375
376    public Vector<String> getPortNames() {
377        return jmri.jmrix.AbstractSerialPortController.getActualPortNames();
378    }
379
380    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SR_NOT_CHECKED",
381                                        justification="this is for skip-chars while loop: no matter how many, we're skipping")
382    public String openPort(String portName, String appName) {
383        // open the port, check ability to set moderators
384        try {
385            // get and open the primary port
386            CommPortIdentifier portID = CommPortIdentifier.getPortIdentifier(portName);
387            try {
388                activeSerialPort = portID.open(appName, 2000);  // name of program, msec to wait
389            } catch (PortInUseException p) {
390                handlePortBusy(p, portName);
391                return "Port " + p + " already in use";
392            }
393
394            // try to set it for communication via SerialDriver
395            try {
396                // get selected speed
397                int speed = 9600;
398                // Doc says 7 bits, but 8 seems needed
399                activeSerialPort.setSerialPortParams(speed, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
400            } catch (UnsupportedCommOperationException e) {
401                log.error("Cannot set serial parameters on port {}: {}", portName, e.getMessage());
402                return "Cannot set serial parameters on port " + portName + ": " + e.getMessage();
403            }
404
405            // set RTS high, DTR high
406            activeSerialPort.setRTS(true); // not connected in some serial ports and adapters
407            activeSerialPort.setDTR(true); // pin 1 in DIN8; on main connector, this is DTR
408
409            // disable flow control; hardware lines used for signaling, XON/XOFF might appear in data
410            activeSerialPort.setFlowControlMode(0);
411
412            // set timeout
413            log.debug("Serial timeout was observed as: {} {}", activeSerialPort.getReceiveTimeout(),
414                    activeSerialPort.isReceiveTimeoutEnabled());
415
416            // get and save stream
417            serialStream = new DataInputStream(activeSerialPort.getInputStream());
418            ostream = activeSerialPort.getOutputStream();
419
420            // purge contents, if any
421            int count = serialStream.available();
422            log.debug("input stream shows {} bytes available", count);
423            while (count > 0) {
424                serialStream.skip(count);
425                count = serialStream.available();
426            }
427
428            // report status?
429            if (log.isInfoEnabled()) {
430                log.info("{} port opened at {} baud, sees  DTR: {} RTS: {} DSR: {} CTS: {}  CD: {}",
431                        portName, activeSerialPort.getBaudRate(), activeSerialPort.isDTR(),
432                        activeSerialPort.isRTS(), activeSerialPort.isDSR(), activeSerialPort.isCTS(),
433                        activeSerialPort.isCD());
434            }
435
436            //opened = true;
437        } catch (NoSuchPortException | UnsupportedCommOperationException | IOException | RuntimeException ex) {
438            log.error("Unexpected exception while opening port {}", portName, ex);
439            return "Unexpected error while opening port " + portName + ": " + ex;
440        }
441        return null; // indicates OK return
442    }
443
444    void handlePortBusy(PortInUseException p, String port) {
445        log.error("Port {} in use, cannot open", port, p);
446    }
447
448    public LoaderPane() {
449        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
450
451        addCommGUI();
452
453        add(new JSeparator());
454
455        {
456            JPanel p = new JPanel();
457            p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
458
459            fileButton = new JButton(Bundle.getMessage("ButtonSelect"));
460            fileButton.setEnabled(false);
461            fileButton.setToolTipText(Bundle.getMessage("TipFileDisabled"));
462            fileButton.addActionListener(new AbstractAction() {
463                @Override
464                public void actionPerformed(java.awt.event.ActionEvent e) {
465                    selectInputFile();
466                }
467            });
468            p.add(fileButton);
469            p.add(new JLabel(Bundle.getMessage("LabelInpFile")));
470            p.add(inputFileName);
471
472            add(p);
473        }
474
475        {
476            JPanel p = new JPanel();
477            p.setLayout(new FlowLayout());
478            JLabel l = new JLabel(Bundle.getMessage("LabelFileComment"));
479            l.setAlignmentX(JLabel.LEFT_ALIGNMENT);
480            p.add(l);
481            add(p);
482        }
483
484        comment.setEditable(false);
485        comment.setEnabled(true);
486        comment.setText("\n\n\n\n"); // just to save some space
487        add(comment);
488
489        add(new JSeparator());
490
491        {
492            JPanel p = new JPanel();
493            p.setLayout(new FlowLayout());
494
495            loadButton = new JButton(Bundle.getMessage("ButtonDownload"));
496            loadButton.setEnabled(false);
497            loadButton.setToolTipText(Bundle.getMessage("TipLoadDisabled"));
498            p.add(loadButton);
499            loadButton.addActionListener(new AbstractAction() {
500                @Override
501                public void actionPerformed(java.awt.event.ActionEvent e) {
502                    doLoad();
503                }
504            });
505
506            add(p);
507        }
508
509        bar = new JProgressBar();
510        add(bar);
511
512        add(new JSeparator());
513
514        {
515            JPanel p = new JPanel();
516            p.setLayout(new FlowLayout());
517            status.setText(Bundle.getMessage("StatusSelectPort"));
518            status.setAlignmentX(JLabel.LEFT_ALIGNMENT);
519            p.add(status);
520            add(p);
521        }
522    }
523
524    void selectInputFile() {
525        chooser.rescanCurrentDirectory();
526        int retVal = chooser.showOpenDialog(this);
527        if (retVal != JFileChooser.APPROVE_OPTION) {
528            return;  // give up if no file selected
529        }
530        inputFileName.setText(chooser.getSelectedFile().getPath());
531
532        // now read the file
533        pdiFile = new PdiFile(chooser.getSelectedFile());
534        try {
535            pdiFile.open();
536        } catch (IOException e) {
537            log.error("Error opening file", e);
538        }
539
540        comment.setText(pdiFile.getComment());
541        status.setText(Bundle.getMessage("StatusDoDownload"));
542        loadButton.setEnabled(true);
543        loadButton.setToolTipText(Bundle.getMessage("TipLoadEnabled"));
544        validate();
545    }
546
547    void doLoad() {
548        status.setText(Bundle.getMessage("StatusRestartUnit"));
549        loadButton.setEnabled(false);
550        loadButton.setToolTipText(Bundle.getMessage("TipLoadGoing"));
551        // start read/write thread
552        readerThread = new LocalReader();
553        readerThread.start();
554    }
555
556    long CRC_char(long crcin, byte ch) {
557        long crc;
558
559        crc = crcin;                    // copy incoming for local use
560
561        crc = swap(crc);                // swap crc bytes
562        crc ^= ((long) ch & 0xff);       // XOR on the byte, no sign extension
563        crc ^= ((crc & 0xFF) >> 4);
564
565        /*  crc:=crc xor (swap(lo(crc)) shl 4) xor (lo(crc) shl 5);     */
566        crc = (crc ^ (swap((crc & 0xFF)) << 4)) ^ ((crc & 0xFF) << 5);
567        crc &= 0xffff;                  // make sure to mask off anything above 16 bits
568        return crc;
569    }
570
571    long swap(long val) {
572        long low = val & 0xFF;
573        long high = (val >> 8) & 0xFF;
574        return low * 256 + high;
575    }
576
577    /**
578     * Insert the CRC for a block of characters in a buffer
579     * <p>
580     * The last two bytes of the buffer hold the checksum, and are not included
581     * in the checksum.
582     * @param buffer Buffer holding the message to be get a CRC
583     */
584    void CRC_block(byte[] buffer) {
585        long crc = 0;
586
587        for (int r = 0; r < buffer.length - 2; r++) {
588            crc = CRC_char(crc, buffer[r]); // do this character
589        }
590
591        // store into buffer
592        byte high = (byte) ((crc >> 8) & 0xFF);
593        byte low = (byte) (crc & 0xFF);
594        buffer[buffer.length - 2] = low;
595        buffer[buffer.length - 1] = high;
596    }
597
598    /**
599     * Check to see if message starts transmission
600     * @param buffer Buffer holding the message to be checked
601     * @return True if buffer is a upload-ready message
602     */
603    boolean isUploadReady(byte[] buffer) {
604        if (buffer[0] != 31) {
605            return false;
606        }
607        if (buffer[1] != 32) {
608            return false;
609        }
610        if (buffer[2] != 99) {
611            return false;
612        }
613        if (buffer[3] != 00) {
614            return false;
615        }
616        return (buffer[4] == 44) || (buffer[4] == 45);
617    }
618
619    /**
620     * Check to see if this is a request for the next block
621     * @param buffer Buffer holding the message to be checked
622     * @return True if buffer is a sent-next message
623     */
624    boolean isSendNext(byte[] buffer) {
625        if (buffer[0] != 31) {
626            return false;
627        }
628        if (buffer[1] != 32) {
629            return false;
630        }
631        if (buffer[2] != 99) {
632            return false;
633        }
634        if (buffer[3] != 00) {
635            return false;
636        }
637        if (buffer[4] != 22) {
638            return false;
639        }
640        log.debug("OK isSendNext");
641        return true;
642    }
643
644    /**
645     * Get output data length from 1st message
646     *
647     * @param buffer Message from which length is to be extracted
648     * @return length of the buffer
649     */
650    int getDataSize(byte[] buffer) {
651        if (buffer[4] == 44) {
652            return 64;
653        }
654        if (buffer[4] == 45) {
655            return 128;
656        }
657        log.error("Bad length byte: {}", buffer[3]);
658        return 64;
659    }
660
661    /**
662     * Return a properly formatted boot message, complete with CRC
663     * @return buffer Contains boot message that's been created
664     */
665    byte[] bootMessage() {
666        byte[] buffer = new byte[]{99, 0, 0, 0, 0};
667        CRC_block(buffer);
668        return buffer;
669    }
670
671    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LoaderPane.class);
672
673}