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 219 pane2 = new JPanel(); 220 pane2.setLayout(new WrapLayout()); 221 add(pane2); 222 b = new JButton("Send Datagram"); 223 b.addActionListener(this::sendDatagramPerformed); 224 pane2.add(b); 225 pane2.add(new JLabel("Contents: ")); 226 datagramContentsField.setColumns(45); 227 pane2.add(datagramContentsField); 228 b = new JButton("Send Datagram Reply"); 229 b.addActionListener(this::sendDatagramReply); 230 pane2.add(b); 231 232 // send OpenLCB Configuration message 233 add(new JSeparator()); 234 235 pane2 = new JPanel(); 236 pane2.setLayout(new WrapLayout()); 237 add(pane2); 238 pane2.add(new JLabel("Send OpenLCB memory request with address: ")); 239 pane2.add(configAddressField); 240 pane2.add(new JLabel("Address Space: ")); 241 pane2.add(addrSpace); 242 pane2 = new JPanel(); 243 pane2.setLayout(new WrapLayout()); 244 add(pane2); 245 pane2.add(new JLabel("Byte Count: ")); 246 pane2.add(configNumberField); 247 b = new JButton("Read"); 248 b.addActionListener(this::readPerformed); 249 pane2.add(b); 250 pane2.add(new JLabel("Data: ")); 251 pane2.add(readDataField); 252 253 pane2 = new JPanel(); 254 pane2.setLayout(new WrapLayout()); 255 add(pane2); 256 b = new JButton("Write"); 257 b.addActionListener(this::writePerformed); 258 pane2.add(b); 259 pane2.add(new JLabel("Data: ")); 260 writeDataField.setText("00 00"); // NOI18N 261 pane2.add(writeDataField); 262 263 cdiButton = new JButton("Open CDI Config Tool"); 264 add(cdiButton); 265 cdiButton.addActionListener(e -> openCdiPane()); 266 cdiButton.setToolTipText("If this button is disabled, please select another node."); 267 setCdiButton(); // get initial state 268 269 // listen for mimic store changes to set CDI button 270 store.addPropertyChangeListener(e -> { 271 setCdiButton(); 272 }); 273 jmri.util.ThreadingUtil.runOnGUIDelayed( ()->{ 274 setCdiButton(); 275 }, 500); 276 } 277 278 /** 279 * Set whether Open CDI button is enabled based on whether 280 * the selected node has CDI in its PIP 281 */ 282 protected void setCdiButton() { 283 var nodeID = nodeSelector.getSelectedNodeID(); 284 if (nodeID == null) { 285 cdiButton.setEnabled(false); 286 return; 287 } 288 var pip = store.getProtocolIdentification(nodeID); 289 if (pip == null || pip.getProtocols() == null) { 290 cdiButton.setEnabled(false); 291 return; 292 } 293 cdiButton.setEnabled( 294 pip.getProtocols() 295 .contains(org.openlcb.ProtocolIdentification.Protocol.ConfigurationDescription)); 296 } 297 298 private JPanel getSendSinglePacketJPanel() { 299 JPanel outer = new JPanel(); 300 outer.setLayout(new BoxLayout(outer, BoxLayout.X_AXIS)); 301 302 JPanel pane1 = new JPanel(); 303 pane1.setLayout(new BoxLayout(pane1, BoxLayout.Y_AXIS)); 304 305 jLabel1.setText("Single Frame: (Raw input format is [123] 12 34 56) "); 306 jLabel1.setVisible(true); 307 308 sendButton.setText("Send"); 309 sendButton.setVisible(true); 310 sendButton.setToolTipText("Send frame"); 311 312 packetTextField.setToolTipText("Frame as hex pairs, e.g. 82 7D; standard header in (), extended in []"); 313 packetTextField.setMaximumSize(packetTextField.getPreferredSize()); 314 315 pane1.add(jLabel1); 316 pane1.add(packetTextField); 317 pane1.add(sendButton); 318 pane1.add(Box.createVerticalGlue()); 319 320 sendButton.addActionListener(this::sendButtonActionPerformed); 321 322 outer.add(Box.createHorizontalGlue()); 323 outer.add(pane1); 324 outer.add(Box.createHorizontalGlue()); 325 return outer; 326 } 327 328 @Override 329 public String getHelpTarget() { 330 return "package.jmri.jmrix.openlcb.swing.send.OpenLcbCanSendFrame"; // NOI18N 331 } 332 333 @Override 334 public String getTitle() { 335 if (memo != null) { 336 return (memo.getUserName() + " Send Can Frame"); 337 } 338 return "Send CAN Frames and OpenLCB Messages"; 339 } 340 341 JComponent addLineLabel(String text) { 342 return addLineLabel(text, null); 343 } 344 345 JComponent addLineLabel(String text, JComponent c) { 346 JLabel lab = new JLabel(text); 347 JPanel p = new JPanel(); 348 p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS)); 349 if (c != null) { 350 p.add(lab, BorderLayout.EAST); 351 if (c instanceof JTextField) { 352 int height = lab.getMinimumSize().height+4; 353 int width = c.getMinimumSize().width; 354 Dimension d = new Dimension(width, height); 355 c.setMaximumSize(d); 356 } 357 p.add(c); 358 } else { 359 p.add(lab, BorderLayout.EAST); 360 } 361 p.add(Box.createHorizontalGlue()); 362 return p; 363 } 364 365 public void sendButtonActionPerformed(java.awt.event.ActionEvent e) { 366 String input = packetTextField.getText(); 367 // TODO check input + feedback on error. Too easy to cause NPE 368 CanMessage m = createPacket(input); 369 log.debug("sendButtonActionPerformed: {}",m); 370 tc.sendCanMessage(m, this); 371 } 372 373 public void sendCimPerformed(java.awt.event.ActionEvent e) { 374 String data = "[10700" + srcAliasField.getText() + "]"; // NOI18N 375 log.debug("sendCimPerformed: |{}|",data); 376 CanMessage m = createPacket(data); 377 log.debug("sendCimPerformed"); 378 tc.sendCanMessage(m, this); 379 } 380 381 NodeID destNodeID() { 382 return nodeSelector.getSelectedNodeID(); 383 } 384 385 EventID eventID() { 386 return new EventID(jmri.util.StringUtil.bytesFromHexString(sendEventField.getText() 387 .replace(".", " "))); 388 } 389 390 public void sendVerifyNodeGlobal(java.awt.event.ActionEvent e) { 391 Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID); 392 connection.put(m, null); 393 } 394 395 public void sendVerifyNodeGlobalID(java.awt.event.ActionEvent e) { 396 Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID, destNodeID()); 397 connection.put(m, null); 398 } 399 400 public void sendRequestEvents(java.awt.event.ActionEvent e) { 401 Message m = new IdentifyEventsAddressedMessage(srcNodeID, destNodeID()); 402 connection.put(m, null); 403 } 404 405 public void sendRequestPip(java.awt.event.ActionEvent e) { 406 Message m = new ProtocolIdentificationRequestMessage(srcNodeID, destNodeID()); 407 connection.put(m, null); 408 } 409 410 public void sendEventPerformed(java.awt.event.ActionEvent e) { 411 Message m = new ProducerConsumerEventReportMessage(srcNodeID, eventID()); 412 connection.put(m, null); 413 } 414 415 public void sendReqConsumers(java.awt.event.ActionEvent e) { 416 Message m = new IdentifyConsumersMessage(srcNodeID, eventID()); 417 connection.put(m, null); 418 } 419 420 EventState validity() { 421 switch (validitySelector.getSelectedIndex()) { 422 case 1 : return EventState.Valid; 423 case 2 : return EventState.Invalid; 424 case 0 : 425 default: return EventState.Unknown; 426 } 427 } 428 429 public void sendConsumerID(java.awt.event.ActionEvent e) { 430 Message m = new ConsumerIdentifiedMessage(srcNodeID, eventID(), validity()); 431 connection.put(m, null); 432 } 433 434 public void sendReqProducers(java.awt.event.ActionEvent e) { 435 Message m = new IdentifyProducersMessage(srcNodeID, eventID()); 436 connection.put(m, null); 437 } 438 439 public void sendProducerID(java.awt.event.ActionEvent e) { 440 Message m = new ProducerIdentifiedMessage(srcNodeID, eventID(), validity()); 441 connection.put(m, null); 442 } 443 444 public void sendDatagramPerformed(java.awt.event.ActionEvent e) { 445 Message m = new DatagramMessage(srcNodeID, destNodeID(), 446 jmri.util.StringUtil.bytesFromHexString(datagramContentsField.getText())); 447 connection.put(m, null); 448 } 449 450 public void sendDatagramReply(java.awt.event.ActionEvent e) { 451 Message m = new DatagramAcknowledgedMessage(srcNodeID, destNodeID()); 452 connection.put(m, null); 453 } 454 455 public void readPerformed(java.awt.event.ActionEvent e) { 456 int space = addrSpace.getMemorySpace(); 457 long addr = Integer.parseInt(configAddressField.getText(), 16); 458 int length = Integer.parseInt(configNumberField.getText()); 459 mcs.requestRead(destNodeID(), space, addr, 460 length, new MemoryConfigurationService.McsReadHandler() { 461 @Override 462 public void handleReadData(NodeID dest, int space, long address, byte[] data) { 463 log.debug("Read data received {} bytes",data.length); 464 readDataField.setText(jmri.util.StringUtil.hexStringFromBytes(data)); 465 } 466 467 @Override 468 public void handleFailure(int errorCode) { 469 log.warn("OpenLCB read failed: 0x{}", Integer.toHexString 470 (errorCode)); 471 } 472 }); 473 } 474 475 public void writePerformed(java.awt.event.ActionEvent e) { 476 int space = addrSpace.getMemorySpace(); 477 long addr = Integer.parseInt(configAddressField.getText(), 16); 478 byte[] content = jmri.util.StringUtil.bytesFromHexString(writeDataField.getText()); 479 mcs.requestWrite(destNodeID(), space, addr, content, new MemoryConfigurationService.McsWriteHandler() { 480 @Override 481 public void handleSuccess() { 482 // no action required on success 483 } 484 485 @Override 486 public void handleFailure(int errorCode) { 487 log.warn("OpenLCB write failed: 0x{}", Integer.toHexString 488 (errorCode)); 489 } 490 }); 491 } 492 493 public void openCdiPane() { 494 actions.openCdiWindow(destNodeID(), destNodeID().toString()); 495 } 496 497 // control sequence operation 498 int mNextSequenceElement = 0; 499 javax.swing.Timer timer = null; 500 501 /** 502 * Internal routine to handle timer starts and restarts 503 * @param delay milliseconds to delay 504 */ 505 protected void restartTimer(int delay) { 506 if (timer == null) { 507 timer = new javax.swing.Timer(delay, e -> sendNextItem()); 508 } 509 timer.stop(); 510 timer.setInitialDelay(delay); 511 timer.setRepeats(false); 512 timer.start(); 513 } 514 515 /** 516 * Internal routine to handle a timeout and send next item 517 */ 518 protected synchronized void timeout() { 519 sendNextItem(); 520 } 521 522 /** 523 * Run button pressed down, start the sequence operation 524 * @param e event from GUI 525 * 526 */ 527 public void runButtonActionPerformed(java.awt.event.ActionEvent e) { 528 if (!mRunButton.isSelected()) { 529 return; 530 } 531 // make sure at least one is checked 532 boolean ok = false; 533 for (int i = 0; i < MAXSEQUENCE; i++) { 534 if (mUseField[i].isSelected()) { 535 ok = true; 536 } 537 } 538 if (!ok) { 539 mRunButton.setSelected(false); 540 return; 541 } 542 // start the operation 543 mNextSequenceElement = 0; 544 sendNextItem(); 545 } 546 547 /** 548 * Echo has been heard, start delay for next packet 549 */ 550 void startSequenceDelay() { 551 // at the start, mNextSequenceElement contains index we're 552 // working on 553 int delay = Integer.parseInt(mDelayField[mNextSequenceElement].getText()); 554 // increment to next line at completion 555 mNextSequenceElement++; 556 // start timer 557 restartTimer(delay); 558 } 559 560 /** 561 * Send next item; may be used for the first item or when a delay has 562 * elapsed. 563 */ 564 void sendNextItem() { 565 // check if still running 566 if (!mRunButton.isSelected()) { 567 return; 568 } 569 // have we run off the end? 570 if (mNextSequenceElement >= MAXSEQUENCE) { 571 // past the end, go back 572 mNextSequenceElement = 0; 573 } 574 // is this one enabled? 575 if (mUseField[mNextSequenceElement].isSelected()) { 576 // make the packet 577 CanMessage m = createPacket(mPacketField[mNextSequenceElement].getText()); 578 // send it 579 tc.sendCanMessage(m, this); 580 startSequenceDelay(); 581 } else { 582 // ask for the next one 583 mNextSequenceElement++; 584 sendNextItem(); 585 } 586 } 587 588 /** 589 * Create a well-formed message from a String String is expected to be space 590 * seperated hex bytes or CbusAddress, e.g.: 12 34 56 +n4e1 591 * @param s string of spaced hex byte codes 592 * @return The packet, with contents filled-in 593 */ 594 CanMessage createPacket(String s) { 595 CanMessage m; 596 // Try to convert using CbusAddress class to reuse a little code 597 CbusAddress a = new CbusAddress(s); 598 if (a.check()) { 599 m = a.makeMessage(tc.getCanid()); 600 } else { 601 m = new CanMessage(tc.getCanid()); 602 // check for header 603 if (s.charAt(0) == '[') { // NOI18N 604 // extended header 605 m.setExtended(true); 606 int i = s.indexOf(']'); // NOI18N 607 String h = s.substring(1, i); 608 m.setHeader(Integer.parseInt(h, 16)); 609 s = s.substring(i + 1); 610 } else if (s.charAt(0) == '(') { // NOI18N 611 // standard header 612 int i = s.indexOf(')'); // NOI18N 613 String h = s.substring(1, i); 614 m.setHeader(Integer.parseInt(h, 16)); 615 s = s.substring(i + 1); 616 } 617 // Try to get hex bytes 618 byte[] b = StringUtil.bytesFromHexString(s); 619 m.setNumDataElements(b.length); 620 // Use &0xff to ensure signed bytes are stored as unsigned ints 621 for (int i = 0; i < b.length; i++) { 622 m.setElement(i, b[i] & 0xff); 623 } 624 } 625 return m; 626 } 627 628 /** 629 * Don't pay attention to messages 630 */ 631 @Override 632 public void message(CanMessage m) { 633 // ignore outgoing messages 634 } 635 636 /** 637 * Don't pay attention to replies 638 */ 639 @Override 640 public void reply(CanReply m) { 641 // ignore incoming replies 642 } 643 644 /** 645 * When the window closes, stop any sequences running 646 */ 647 @Override 648 public void dispose() { 649 mRunButton.setSelected(false); 650 super.dispose(); 651 } 652 653 // private data 654 private TrafficController tc = null; // was CanInterface 655 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OpenLcbCanSendPane.class); 656 657}