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