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}