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 jmri.jmrix.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 = 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}