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