001package jmri.jmrix.openlcb.swing.send; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.awt.BorderLayout; 006import java.awt.Dimension; 007 008import javax.swing.Box; 009import javax.swing.BoxLayout; 010import javax.swing.JButton; 011import javax.swing.JCheckBox; 012import javax.swing.JComboBox; 013import javax.swing.JComponent; 014import javax.swing.JFormattedTextField; 015import javax.swing.JLabel; 016import javax.swing.JPanel; 017import javax.swing.JSeparator; 018import javax.swing.JTextField; 019import javax.swing.JToggleButton; 020 021import jmri.jmrix.can.CanListener; 022import jmri.jmrix.can.CanMessage; 023import jmri.jmrix.can.CanReply; 024import jmri.jmrix.can.CanSystemConnectionMemo; 025import jmri.jmrix.can.TrafficController; 026import jmri.jmrix.can.cbus.CbusAddress; 027import jmri.jmrix.openlcb.swing.ClientActions; 028import jmri.util.StringUtil; 029import jmri.util.javaworld.GridLayout2; 030import jmri.util.swing.WrapLayout; 031 032import org.openlcb.*; 033import org.openlcb.can.AliasMap; 034import org.openlcb.implementations.MemoryConfigurationService; 035import org.openlcb.swing.EventIdTextField; 036import org.openlcb.swing.NodeSelector; 037import org.openlcb.swing.MemorySpaceSelector; 038 039/** 040 * User interface for sending OpenLCB CAN frames to exercise the system 041 * <p> 042 * When sending a sequence of operations: 043 * <ul> 044 * <li>Send the next message and start a timer 045 * <li>When the timer trips, repeat if buttons still down. 046 * </ul> 047 * 048 * @author Bob Jacobsen Copyright (C) 2008, 2012 049 * 050 */ 051public class OpenLcbCanSendPane extends jmri.jmrix.can.swing.CanPanel implements CanListener { 052 053 // member declarations 054 final JLabel jLabel1 = new JLabel(); 055 final JButton sendButton = new JButton(); 056 final JTextField packetTextField = new JTextField(60); 057 058 // internal members to hold sequence widgets 059 static final int MAXSEQUENCE = 4; 060 final JTextField[] mPacketField = new JTextField[MAXSEQUENCE]; 061 final JCheckBox[] mUseField = new JCheckBox[MAXSEQUENCE]; 062 final JTextField[] mDelayField = new JTextField[MAXSEQUENCE]; 063 final JToggleButton mRunButton = new JToggleButton("Go"); 064 065 final JTextField srcAliasField = new JTextField(4); 066 NodeSelector nodeSelector; 067 final JFormattedTextField sendEventField = EventIdTextField.getEventIdTextField();// NOI18N 068 final JTextField datagramContentsField = new JTextField("20 61 00 00 00 00 08"); // NOI18N 069 final JTextField configNumberField = new JTextField("40"); // NOI18N 070 final JTextField configAddressField = new JTextField("000000"); // NOI18N 071 final JTextField readDataField = new JTextField(60); 072 final JTextField writeDataField = new JTextField(60); 073 final MemorySpaceSelector addrSpace = new MemorySpaceSelector(0xFF); 074 final JComboBox<String> validitySelector = new JComboBox<String>(new String[]{"Unknown", "Valid", "Invalid"}); 075 JButton cdiButton; 076 077 Connection connection; 078 AliasMap aliasMap; 079 NodeID srcNodeID; 080 MemoryConfigurationService mcs; 081 MimicNodeStore store; 082 OlcbInterface iface; 083 ClientActions actions; 084 085 public OpenLcbCanSendPane() { 086 // most of the action is in initComponents 087 } 088 089 @Override 090 public void initComponents(CanSystemConnectionMemo memo) { 091 super.initComponents(memo); 092 iface = memo.get(OlcbInterface.class); 093 actions = new ClientActions(iface, memo); 094 tc = memo.getTrafficController(); 095 tc.addCanListener(this); 096 connection = memo.get(org.openlcb.Connection.class); 097 srcNodeID = memo.get(org.openlcb.NodeID.class); 098 aliasMap = memo.get(org.openlcb.can.AliasMap.class); 099 100 // register request for notification 101 Connection.ConnectionListener cl = new Connection.ConnectionListener() { 102 @Override 103 public void connectionActive(Connection c) { 104 log.debug("connection active"); 105 // load the alias field 106 srcAliasField.setText(Integer.toHexString(aliasMap.getAlias(srcNodeID))); 107 } 108 }; 109 connection.registerStartNotification(cl); 110 111 mcs = memo.get(MemoryConfigurationService.class); 112 store = memo.get(MimicNodeStore.class); 113 nodeSelector = new NodeSelector(store); 114 nodeSelector.addActionListener (new ActionListener () { 115 public void actionPerformed(ActionEvent e) { 116 setCdiButton(); 117 } 118 }); 119 120 // start window layout 121 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); 122 123 // handle single-packet part 124 add(getSendSinglePacketJPanel()); 125 126 add(new JSeparator()); 127 128 // Configure the sequence 129 add(new JLabel("Send sequence of frames:")); 130 JPanel pane2 = new JPanel(); 131 pane2.setLayout(new GridLayout2(MAXSEQUENCE + 2, 4)); 132 pane2.add(new JLabel("")); 133 pane2.add(new JLabel("Send")); 134 pane2.add(new JLabel("packet")); 135 pane2.add(new JLabel("wait (msec)")); 136 for (int i = 0; i < MAXSEQUENCE; i++) { 137 pane2.add(new JLabel(Integer.toString(i + 1))); 138 mUseField[i] = new JCheckBox(); 139 mPacketField[i] = new JTextField(20); 140 mDelayField[i] = new JTextField(10); 141 pane2.add(mUseField[i]); 142 pane2.add(mPacketField[i]); 143 pane2.add(mDelayField[i]); 144 } 145 add(pane2); 146 add(mRunButton); // below rows 147 148 mRunButton.addActionListener(this::runButtonActionPerformed); 149 150 // special packet forms 151 add(new JSeparator()); 152 153 pane2 = new JPanel(); 154 pane2.setLayout(new WrapLayout()); 155 add(pane2); 156 pane2.add(new JLabel("Send control frame with source alias:")); 157 pane2.add(srcAliasField); 158 JButton b; 159 b = new JButton("Send CIM"); 160 b.addActionListener(this::sendCimPerformed); 161 pane2.add(b); 162 163 // send OpenLCB messages 164 add(new JSeparator()); 165 166 pane2 = new JPanel(); 167 pane2.setLayout(new WrapLayout()); 168 add(pane2); 169 pane2.add(new JLabel("Send OpenLCB global message:")); 170 b = new JButton("Send Verify Nodes Global"); 171 b.addActionListener(this::sendVerifyNodeGlobal); 172 pane2.add(b); 173 b = new JButton("Send Verify Node Global with NodeID"); 174 b.addActionListener(this::sendVerifyNodeGlobalID); 175 pane2.add(b); 176 177 // event messages 178 add(new JSeparator()); 179 180 var insert = new JPanel(); 181 insert.setLayout(new WrapLayout()); 182 insert.add(sendEventField); 183 insert.add(validitySelector); 184 185 186 add(addLineLabel("Send OpenLCB event message with eventID:", insert)); 187 pane2 = new JPanel(); 188 pane2.setLayout(new WrapLayout()); 189 add(pane2); 190 b = new JButton("Send Request Consumers"); 191 b.addActionListener(this::sendReqConsumers); 192 pane2.add(b); 193 b = new JButton("Send Consumer Identified"); 194 b.addActionListener(this::sendConsumerID); 195 pane2.add(b); 196 b = new JButton("Send Request Producers"); 197 b.addActionListener(this::sendReqProducers); 198 pane2.add(b); 199 b = new JButton("Send Producer Identified"); 200 b.addActionListener(this::sendProducerID); 201 pane2.add(b); 202 b = new JButton("Send Event Produced"); 203 b.addActionListener(this::sendEventPerformed); 204 pane2.add(b); 205 206 // addressed messages 207 add(new JSeparator()); 208 add(addLineLabel("Send OpenLCB addressed message to:", nodeSelector)); 209 pane2 = new JPanel(); 210 pane2.setLayout(new WrapLayout()); 211 add(pane2); 212 b = new JButton("Send Request Events"); 213 b.addActionListener(this::sendRequestEvents); 214 pane2.add(b); 215 b = new JButton("Send PIP Request"); 216 b.addActionListener(this::sendRequestPip); 217 pane2.add(b); 218 b = new JButton("Send SNIP Request"); 219 b.addActionListener(this::sendRequestSnip); 220 pane2.add(b); 221 222 add(new JSeparator()); 223 224 pane2 = new JPanel(); 225 pane2.setLayout(new WrapLayout()); 226 add(pane2); 227 b = new JButton("Send Datagram"); 228 b.addActionListener(this::sendDatagramPerformed); 229 pane2.add(b); 230 pane2.add(new JLabel("Contents: ")); 231 datagramContentsField.setColumns(45); 232 pane2.add(datagramContentsField); 233 b = new JButton("Send Datagram Reply"); 234 b.addActionListener(this::sendDatagramReply); 235 pane2.add(b); 236 237 // send OpenLCB Configuration message 238 add(new JSeparator()); 239 240 pane2 = new JPanel(); 241 pane2.setLayout(new WrapLayout()); 242 add(pane2); 243 pane2.add(new JLabel("Send OpenLCB memory request with address: ")); 244 pane2.add(configAddressField); 245 pane2.add(new JLabel("Address Space: ")); 246 pane2.add(addrSpace); 247 pane2 = new JPanel(); 248 pane2.setLayout(new WrapLayout()); 249 add(pane2); 250 pane2.add(new JLabel("Byte Count: ")); 251 pane2.add(configNumberField); 252 b = new JButton("Read"); 253 b.addActionListener(this::readPerformed); 254 pane2.add(b); 255 pane2.add(new JLabel("Data: ")); 256 pane2.add(readDataField); 257 258 pane2 = new JPanel(); 259 pane2.setLayout(new WrapLayout()); 260 add(pane2); 261 b = new JButton("Write"); 262 b.addActionListener(this::writePerformed); 263 pane2.add(b); 264 pane2.add(new JLabel("Data: ")); 265 writeDataField.setText("00 00"); // NOI18N 266 pane2.add(writeDataField); 267 268 cdiButton = new JButton("Open CDI Config Tool"); 269 add(cdiButton); 270 cdiButton.addActionListener(e -> openCdiPane()); 271 cdiButton.setToolTipText("If this button is disabled, please select another node."); 272 setCdiButton(); // get initial state 273 274 // listen for mimic store changes to set CDI button 275 store.addPropertyChangeListener(e -> { 276 setCdiButton(); 277 }); 278 jmri.util.ThreadingUtil.runOnGUIDelayed( ()->{ 279 setCdiButton(); 280 }, 500); 281 } 282 283 /** 284 * Set whether Open CDI button is enabled based on whether 285 * the selected node has CDI in its PIP 286 */ 287 protected void setCdiButton() { 288 var nodeID = nodeSelector.getSelectedNodeID(); 289 if (nodeID == null) { 290 cdiButton.setEnabled(false); 291 return; 292 } 293 var pip = store.getProtocolIdentification(nodeID); 294 if (pip == null || pip.getProtocols() == null) { 295 cdiButton.setEnabled(false); 296 return; 297 } 298 cdiButton.setEnabled( 299 pip.getProtocols() 300 .contains(org.openlcb.ProtocolIdentification.Protocol.ConfigurationDescription)); 301 } 302 303 private JPanel getSendSinglePacketJPanel() { 304 JPanel outer = new JPanel(); 305 outer.setLayout(new BoxLayout(outer, BoxLayout.X_AXIS)); 306 307 JPanel pane1 = new JPanel(); 308 pane1.setLayout(new BoxLayout(pane1, BoxLayout.Y_AXIS)); 309 310 jLabel1.setText("Single Frame: (Raw input format is [123] 12 34 56) "); 311 jLabel1.setVisible(true); 312 313 sendButton.setText("Send"); 314 sendButton.setVisible(true); 315 sendButton.setToolTipText("Send frame"); 316 317 packetTextField.setToolTipText("Frame as hex pairs, e.g. 82 7D; standard header in (), extended in []"); 318 packetTextField.setMaximumSize(packetTextField.getPreferredSize()); 319 320 pane1.add(jLabel1); 321 pane1.add(packetTextField); 322 pane1.add(sendButton); 323 pane1.add(Box.createVerticalGlue()); 324 325 sendButton.addActionListener(this::sendButtonActionPerformed); 326 327 outer.add(Box.createHorizontalGlue()); 328 outer.add(pane1); 329 outer.add(Box.createHorizontalGlue()); 330 return outer; 331 } 332 333 @Override 334 public String getHelpTarget() { 335 return "package.jmri.jmrix.openlcb.swing.send.OpenLcbCanSendFrame"; // NOI18N 336 } 337 338 @Override 339 public String getTitle() { 340 if (memo != null) { 341 return (memo.getUserName() + " Send CAN Frames and OpenLCB Messages"); 342 } 343 return "Send CAN Frames and OpenLCB Messages"; 344 } 345 346 JComponent addLineLabel(String text) { 347 return addLineLabel(text, null); 348 } 349 350 JComponent addLineLabel(String text, JComponent c) { 351 JLabel lab = new JLabel(text); 352 JPanel p = new JPanel(); 353 p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS)); 354 if (c != null) { 355 p.add(lab, BorderLayout.EAST); 356 if (c instanceof JTextField) { 357 int height = lab.getMinimumSize().height+4; 358 int width = c.getMinimumSize().width; 359 Dimension d = new Dimension(width, height); 360 c.setMaximumSize(d); 361 } 362 p.add(c); 363 } else { 364 p.add(lab, BorderLayout.EAST); 365 } 366 p.add(Box.createHorizontalGlue()); 367 return p; 368 } 369 370 public void sendButtonActionPerformed(java.awt.event.ActionEvent e) { 371 String input = packetTextField.getText(); 372 // TODO check input + feedback on error. Too easy to cause NPE 373 CanMessage m = createPacket(input); 374 log.debug("sendButtonActionPerformed: {}",m); 375 tc.sendCanMessage(m, this); 376 } 377 378 public void sendCimPerformed(java.awt.event.ActionEvent e) { 379 String data = "[10700" + srcAliasField.getText() + "]"; // NOI18N 380 log.debug("sendCimPerformed: |{}|",data); 381 CanMessage m = createPacket(data); 382 log.debug("sendCimPerformed"); 383 tc.sendCanMessage(m, this); 384 } 385 386 NodeID destNodeID() { 387 return nodeSelector.getSelectedNodeID(); 388 } 389 390 EventID eventID() { 391 return new EventID(jmri.util.StringUtil.bytesFromHexString(sendEventField.getText() 392 .replace(".", " "))); 393 } 394 395 public void sendVerifyNodeGlobal(java.awt.event.ActionEvent e) { 396 Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID); 397 connection.put(m, null); 398 } 399 400 public void sendVerifyNodeGlobalID(java.awt.event.ActionEvent e) { 401 Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID, destNodeID()); 402 connection.put(m, null); 403 } 404 405 public void sendRequestEvents(java.awt.event.ActionEvent e) { 406 Message m = new IdentifyEventsAddressedMessage(srcNodeID, destNodeID()); 407 connection.put(m, null); 408 } 409 410 public void sendRequestPip(java.awt.event.ActionEvent e) { 411 Message m = new ProtocolIdentificationRequestMessage(srcNodeID, destNodeID()); 412 connection.put(m, null); 413 } 414 415 public void sendRequestSnip(java.awt.event.ActionEvent e) { 416 Message m = new SimpleNodeIdentInfoRequestMessage(srcNodeID, destNodeID()); 417 connection.put(m, null); 418 } 419 420 public void sendEventPerformed(java.awt.event.ActionEvent e) { 421 Message m = new ProducerConsumerEventReportMessage(srcNodeID, eventID()); 422 connection.put(m, null); 423 } 424 425 public void sendReqConsumers(java.awt.event.ActionEvent e) { 426 Message m = new IdentifyConsumersMessage(srcNodeID, eventID()); 427 connection.put(m, null); 428 } 429 430 EventState validity() { 431 switch (validitySelector.getSelectedIndex()) { 432 case 1 : return EventState.Valid; 433 case 2 : return EventState.Invalid; 434 case 0 : 435 default: return EventState.Unknown; 436 } 437 } 438 439 public void sendConsumerID(java.awt.event.ActionEvent e) { 440 Message m = new ConsumerIdentifiedMessage(srcNodeID, eventID(), validity()); 441 connection.put(m, null); 442 } 443 444 public void sendReqProducers(java.awt.event.ActionEvent e) { 445 Message m = new IdentifyProducersMessage(srcNodeID, eventID()); 446 connection.put(m, null); 447 } 448 449 public void sendProducerID(java.awt.event.ActionEvent e) { 450 Message m = new ProducerIdentifiedMessage(srcNodeID, eventID(), validity()); 451 connection.put(m, null); 452 } 453 454 public void sendDatagramPerformed(java.awt.event.ActionEvent e) { 455 Message m = new DatagramMessage(srcNodeID, destNodeID(), 456 jmri.util.StringUtil.bytesFromHexString(datagramContentsField.getText())); 457 connection.put(m, null); 458 } 459 460 public void sendDatagramReply(java.awt.event.ActionEvent e) { 461 Message m = new DatagramAcknowledgedMessage(srcNodeID, destNodeID()); 462 connection.put(m, null); 463 } 464 465 public void readPerformed(java.awt.event.ActionEvent e) { 466 int space = addrSpace.getMemorySpace(); 467 long addr = Integer.parseInt(configAddressField.getText(), 16); 468 int length = Integer.parseInt(configNumberField.getText()); 469 mcs.requestRead(destNodeID(), space, addr, 470 length, new MemoryConfigurationService.McsReadHandler() { 471 @Override 472 public void handleReadData(NodeID dest, int space, long address, byte[] data) { 473 log.debug("Read data received {} bytes",data.length); 474 readDataField.setText(jmri.util.StringUtil.hexStringFromBytes(data)); 475 } 476 477 @Override 478 public void handleFailure(int errorCode) { 479 log.warn("OpenLCB read failed: 0x{}", Integer.toHexString 480 (errorCode)); 481 } 482 }); 483 } 484 485 public void writePerformed(java.awt.event.ActionEvent e) { 486 int space = addrSpace.getMemorySpace(); 487 long addr = Integer.parseInt(configAddressField.getText(), 16); 488 byte[] content = jmri.util.StringUtil.bytesFromHexString(writeDataField.getText()); 489 mcs.requestWrite(destNodeID(), space, addr, content, new MemoryConfigurationService.McsWriteHandler() { 490 @Override 491 public void handleSuccess() { 492 // no action required on success 493 } 494 495 @Override 496 public void handleFailure(int errorCode) { 497 log.warn("OpenLCB write failed: 0x{}", Integer.toHexString 498 (errorCode)); 499 } 500 }); 501 } 502 503 public void openCdiPane() { 504 actions.openCdiWindow(destNodeID(), destNodeID().toString()); 505 } 506 507 // control sequence operation 508 int mNextSequenceElement = 0; 509 javax.swing.Timer timer = null; 510 511 /** 512 * Internal routine to handle timer starts and restarts 513 * @param delay milliseconds to delay 514 */ 515 protected void restartTimer(int delay) { 516 if (timer == null) { 517 timer = new javax.swing.Timer(delay, e -> sendNextItem()); 518 } 519 timer.stop(); 520 timer.setInitialDelay(delay); 521 timer.setRepeats(false); 522 timer.start(); 523 } 524 525 /** 526 * Internal routine to handle a timeout and send next item 527 */ 528 protected synchronized void timeout() { 529 sendNextItem(); 530 } 531 532 /** 533 * Run button pressed down, start the sequence operation 534 * @param e event from GUI 535 * 536 */ 537 public void runButtonActionPerformed(java.awt.event.ActionEvent e) { 538 if (!mRunButton.isSelected()) { 539 return; 540 } 541 // make sure at least one is checked 542 boolean ok = false; 543 for (int i = 0; i < MAXSEQUENCE; i++) { 544 if (mUseField[i].isSelected()) { 545 ok = true; 546 } 547 } 548 if (!ok) { 549 mRunButton.setSelected(false); 550 return; 551 } 552 // start the operation 553 mNextSequenceElement = 0; 554 sendNextItem(); 555 } 556 557 /** 558 * Echo has been heard, start delay for next packet 559 */ 560 void startSequenceDelay() { 561 // at the start, mNextSequenceElement contains index we're 562 // working on 563 int delay = Integer.parseInt(mDelayField[mNextSequenceElement].getText()); 564 // increment to next line at completion 565 mNextSequenceElement++; 566 // start timer 567 restartTimer(delay); 568 } 569 570 /** 571 * Send next item; may be used for the first item or when a delay has 572 * elapsed. 573 */ 574 void sendNextItem() { 575 // check if still running 576 if (!mRunButton.isSelected()) { 577 return; 578 } 579 // have we run off the end? 580 if (mNextSequenceElement >= MAXSEQUENCE) { 581 // past the end, go back 582 mNextSequenceElement = 0; 583 } 584 // is this one enabled? 585 if (mUseField[mNextSequenceElement].isSelected()) { 586 // make the packet 587 CanMessage m = createPacket(mPacketField[mNextSequenceElement].getText()); 588 // send it 589 tc.sendCanMessage(m, this); 590 startSequenceDelay(); 591 } else { 592 // ask for the next one 593 mNextSequenceElement++; 594 sendNextItem(); 595 } 596 } 597 598 /** 599 * Create a well-formed message from a String String is expected to be space 600 * seperated hex bytes or CbusAddress, e.g.: 12 34 56 +n4e1 601 * @param s string of spaced hex byte codes 602 * @return The packet, with contents filled-in 603 */ 604 CanMessage createPacket(String s) { 605 CanMessage m; 606 // Try to convert using CbusAddress class to reuse a little code 607 CbusAddress a = new CbusAddress(s); 608 if (a.check()) { 609 m = a.makeMessage(tc.getCanid()); 610 } else { 611 m = new CanMessage(tc.getCanid()); 612 // check for header 613 if (s.charAt(0) == '[') { // NOI18N 614 // extended header 615 m.setExtended(true); 616 int i = s.indexOf(']'); // NOI18N 617 String h = s.substring(1, i); 618 m.setHeader(Integer.parseInt(h, 16)); 619 s = s.substring(i + 1); 620 } else if (s.charAt(0) == '(') { // NOI18N 621 // standard header 622 int i = s.indexOf(')'); // NOI18N 623 String h = s.substring(1, i); 624 m.setHeader(Integer.parseInt(h, 16)); 625 s = s.substring(i + 1); 626 } 627 // Try to get hex bytes 628 byte[] b = StringUtil.bytesFromHexString(s); 629 m.setNumDataElements(b.length); 630 // Use &0xff to ensure signed bytes are stored as unsigned ints 631 for (int i = 0; i < b.length; i++) { 632 m.setElement(i, b[i] & 0xff); 633 } 634 } 635 return m; 636 } 637 638 /** 639 * Don't pay attention to messages 640 */ 641 @Override 642 public void message(CanMessage m) { 643 // ignore outgoing messages 644 } 645 646 /** 647 * Don't pay attention to replies 648 */ 649 @Override 650 public void reply(CanReply m) { 651 // ignore incoming replies 652 } 653 654 /** 655 * When the window closes, stop any sequences running 656 */ 657 @Override 658 public void dispose() { 659 mRunButton.setSelected(false); 660 super.dispose(); 661 } 662 663 // private data 664 private TrafficController tc = null; // was CanInterface 665 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OpenLcbCanSendPane.class); 666 667}