001package jmri.jmrix.openlcb.swing.monitor; 002 003import jmri.IdTagManager; 004import jmri.InstanceManager; 005import jmri.UserPreferencesManager; 006import jmri.jmrix.can.CanListener; 007import jmri.jmrix.can.CanMessage; 008import jmri.jmrix.can.CanReply; 009import jmri.jmrix.can.CanSystemConnectionMemo; 010import jmri.jmrix.can.swing.CanPanelInterface; 011import jmri.jmrix.openlcb.OlcbConstants; 012 013import org.openlcb.AddressedMessage; 014import org.openlcb.EventID; 015import org.openlcb.EventMessage; 016import org.openlcb.Message; 017import org.openlcb.OlcbInterface; 018import org.openlcb.can.AliasMap; 019import org.openlcb.can.MessageBuilder; 020import org.openlcb.can.OpenLcbCanFrame; 021import org.openlcb.implementations.EventTable; 022import org.slf4j.Logger; 023import org.slf4j.LoggerFactory; 024 025import javax.swing.BoxLayout; 026import javax.swing.JCheckBox; 027import javax.swing.JPanel; 028 029/** 030 * Frame displaying (and logging) OpenLCB (CAN) frames 031 * 032 * @author Bob Jacobsen Copyright (C) 2009, 2010 033 */ 034public class MonitorPane extends jmri.jmrix.AbstractMonPane implements CanListener, CanPanelInterface { 035 036 public MonitorPane() { 037 super(); 038 pm = InstanceManager.getDefault(UserPreferencesManager.class); 039 tagManager = InstanceManager.getDefault(IdTagManager.class); 040 } 041 042 CanSystemConnectionMemo memo; 043 AliasMap aliasMap; 044 MessageBuilder messageBuilder; 045 OlcbInterface olcbInterface; 046 047 IdTagManager tagManager; 048 049 /** show source node name on a separate line when available */ 050 final JCheckBox nodeNameCheckBox = new JCheckBox(); 051 052 /** Show the first EventID in the message on a separate line */ 053 final JCheckBox eventCheckBox = new JCheckBox(); 054 055 /** Show all EventIDs in the message each on a separate line */ 056 final JCheckBox eventAllCheckBox = new JCheckBox(); 057 058 /* Preferences setup */ 059 final String nodeNameCheck = this.getClass().getName() + ".NodeName"; 060 final String eventCheck = this.getClass().getName() + ".Event"; 061 final String eventAllCheck = this.getClass().getName() + ".EventAll"; 062 private final UserPreferencesManager pm; 063 064 @Override 065 public void initContext(Object context) { 066 if (context instanceof CanSystemConnectionMemo) { 067 initComponents((CanSystemConnectionMemo) context); 068 } 069 } 070 071 @Override 072 public void initComponents(CanSystemConnectionMemo memo) { 073 this.memo = memo; 074 075 memo.getTrafficController().addCanConsoleListener(this); 076 077 aliasMap = memo.get(org.openlcb.can.AliasMap.class); 078 messageBuilder = new MessageBuilder(aliasMap); 079 olcbInterface = memo.get(OlcbInterface.class); 080 081 setFixedWidthFont(); 082 } 083 084 @Override 085 public String getTitle() { 086 if (memo != null) { 087 return (memo.getUserName() + " Monitor"); 088 } 089 return Bundle.getMessage("MonitorTitle"); 090 } 091 092 /** 093 * {@inheritDoc} 094 */ 095 @Override 096 public String getHelpTarget() { 097 return "package.jmri.jmrix.openlcb.swing.monitor.MonitorPane"; // NOI18N 098 } 099 100 @Override 101 protected void init() { 102 } 103 104 @Override 105 public void dispose() { 106 try { 107 memo.getTrafficController().removeCanListener(this); 108 } catch(NullPointerException npe){ 109 log.debug("Null Pointer Exception while attempting to remove Can Listener",npe); 110 } 111 112 pm.setSimplePreferenceState(nodeNameCheck, nodeNameCheckBox.isSelected()); 113 pm.setSimplePreferenceState(eventCheck, eventCheckBox.isSelected()); 114 pm.setSimplePreferenceState(eventAllCheck, eventAllCheckBox.isSelected()); 115 116 super.dispose(); 117 } 118 119 @Override 120 protected void addCustomControlPanes(JPanel parent) { 121 JPanel p = new JPanel(); 122 p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS)); 123 124 nodeNameCheckBox.setText(Bundle.getMessage("CheckBoxShowNodeName")); 125 nodeNameCheckBox.setVisible(true); 126 nodeNameCheckBox.setSelected(pm.getSimplePreferenceState(nodeNameCheck)); 127 p.add(nodeNameCheckBox); 128 129 eventCheckBox.setText(Bundle.getMessage("CheckBoxShowEvent")); 130 eventCheckBox.setVisible(true); 131 eventCheckBox.setSelected(pm.getSimplePreferenceState(eventCheck)); 132 p.add(eventCheckBox); 133 134 eventAllCheckBox.setText(Bundle.getMessage("CheckBoxShowEventAll")); 135 eventAllCheckBox.setVisible(true); 136 eventAllCheckBox.setSelected(pm.getSimplePreferenceState(eventAllCheck)); 137 p.add(eventAllCheckBox); 138 139 parent.add(p); 140 super.addCustomControlPanes(parent); 141 } 142 143 String formatFrame(boolean extended, int header, int len, int[] content) { 144 StringBuilder formatted = new StringBuilder(); 145 formatted.append(extended ? "[" : "("); 146 formatted.append(Integer.toHexString(header)); 147 formatted.append((extended ? "]" : ")")); 148 for (int i = 0; i < len; i++) { 149 formatted.append(" "); 150 formatted.append(jmri.util.StringUtil.twoHexFromInt(content[i])); 151 } 152 for (int i = len; i < 8; i++) { 153 formatted.append(" "); 154 } 155 return new String(formatted); 156 } 157 158 // see jmri.jmrix.openlcb.OlcbConfigurationManager 159 java.util.List<Message> frameToMessages(int header, int len, int[] content) { 160 OpenLcbCanFrame frame = new OpenLcbCanFrame(header & 0xFFF); 161 frame.setHeader(header); 162 if (len != 0) { 163 byte[] data = new byte[len]; 164 for (int i = 0; i < data.length; i++) { 165 data[i] = (byte) content[i]; 166 } 167 frame.setData(data); 168 } 169 170 aliasMap.processFrame(frame); 171 return messageBuilder.processFrame(frame); 172 } 173 174 void format(String prefix, boolean extended, int header, int len, int[] content) { 175 String raw = formatFrame(extended, header, len, content); 176 String formatted; 177 if (extended && (header & 0x08000000) != 0) { 178 // is a message type 179 java.util.List<Message> list = frameToMessages(header, len, content); 180 if (list == null || list.isEmpty()) { 181 // didn't format, check for partial datagram 182 if ((header & 0x0F000000) == 0x0B000000) { 183 formatted = prefix + ": (Start of Datagram)"; 184 } else if ((header & 0x0F000000) == 0x0C000000) { 185 formatted = prefix + ": (Middle of Datagram)"; 186 } else if (((header & 0x0FFFF000) == 0x09A08000) && (content.length > 0)) { 187 // SNIP multi frame reply 188 switch (content[0] & 0xF0) { 189 case 0x10: 190 formatted = prefix + ": SNIP Reply 1st frame"; 191 break; 192 case 0x20: 193 formatted = prefix + ": SNIP Reply last frame"; 194 break; 195 case 0x30: 196 formatted = prefix + ": SNIP Reply middle frame"; 197 break; 198 default: 199 formatted = prefix + ": SNIP Reply unknown"; 200 break; 201 } 202 } else if (((header & 0x0FFFF000) == 0x095EB000) && (content.length > 0)) { 203 // Traction Control Command multi frame reply 204 switch (content[0] & 0xF0) { 205 case 0x10: 206 formatted = prefix + ": Traction Control Command 1st frame"; 207 break; 208 case 0x20: 209 formatted = prefix + ": Traction Control Command last frame"; 210 break; 211 case 0x30: 212 formatted = prefix + ": Traction Control Command middle frame"; 213 break; 214 default: 215 formatted = prefix + ": Traction Control Command unknown"; 216 break; 217 } 218 } else if (((header & 0x0FFFF000) == 0x091E9000) && (content.length > 0)) { 219 // Traction Control Reply multi frame reply 220 switch (content[0] & 0xF0) { 221 case 0x10: 222 formatted = prefix + ": Traction Control Reply 1st frame"; 223 break; 224 case 0x20: 225 formatted = prefix + ": Traction Control Reply last frame"; 226 break; 227 case 0x30: 228 formatted = prefix + ": Traction Control Reply middle frame"; 229 break; 230 default: 231 formatted = prefix + ": Traction Control Reply unknown"; 232 break; 233 } 234 } else if (((header & 0x0FFF8000) == 0x09F10000) && (content.length > 0)) { 235 // EWP sections 236 switch (header & 0x7000) { 237 case 0x6000: 238 formatted = prefix + ": Events with Payload 1st frame"; 239 break; 240 case 0x5000: 241 formatted = prefix + ": Events with Payload middle frame"; 242 break; 243 case 0x4000: 244 formatted = prefix + ": Events with Payload last frame"; 245 break; 246 default: 247 formatted = prefix + ": Events with Payload unknown"; 248 break; 249 } 250 } else if (((header & 0x0F000000) == 0x0F000000) && (content.length > 0)) { 251 formatted = prefix + ": Stream Frame " + raw; 252 } else { 253 formatted = prefix + ": Unknown message " + raw; 254 } 255 } else { 256 Message msg = list.get(0); 257 StringBuilder sb = new StringBuilder(); 258 sb.append(prefix); 259 sb.append(": "); 260 sb.append(list.get(0).toString()); 261 if (nodeNameCheckBox.isSelected() && olcbInterface != null) { 262 var ptr = olcbInterface.getNodeStore().findNode(list.get(0).getSourceNodeID()); 263 if (ptr != null && ptr.getSimpleNodeIdent() != null) { 264 String name = ""; 265 var ident = ptr.getSimpleNodeIdent(); 266 if (ident != null) { 267 name = ident.getUserName(); 268 if (name.isEmpty()) { 269 name = ident.getMfgName()+" - "+ident.getModelName(); 270 } 271 } 272 if (!name.isBlank()) { 273 sb.append("\n Src: "); 274 sb.append(name); 275 } 276 } 277 if (list.get(0) instanceof AddressedMessage) { 278 ptr = olcbInterface.getNodeStore().findNode(((AddressedMessage)list.get(0)).getDestNodeID()); 279 if (ptr != null && ptr.getSimpleNodeIdent() != null) { 280 String name = ""; 281 var ident = ptr.getSimpleNodeIdent(); 282 if (ident != null) { 283 name = ident.getUserName(); 284 if (name.isEmpty()) { 285 name = ident.getMfgName()+" - "+ident.getModelName(); 286 } 287 } 288 if (!name.isBlank()) { 289 sb.append(" Dest: "); 290 sb.append(name); 291 } 292 } 293 } 294 } 295 if ((eventCheckBox.isSelected() || eventAllCheckBox.isSelected()) && olcbInterface != null 296 && msg instanceof EventMessage) { 297 EventID ev = ((EventMessage) msg).getEventID(); 298 log.debug("event message with event {}", ev); 299 300 // this could be converted to EventTablePane.isEventNameTagPresent 301 // but that would duplicate the retrieval of the bean and user name 302 var tag = tagManager.getIdTag(OlcbConstants.tagPrefix+ev.toShortString()); 303 String tagname = null; 304 if (tag != null 305 && (tagname = tag.getUserName()) != null) { 306 if (! tagname.isEmpty()) { 307 sb.append("\n Name: "); 308 sb.append(tagname); 309 } 310 } 311 312 // check for time message 313 if ((content[0] == 1) && (content[1] == 1) && (content[2] == 0) && (content[3] == 0) && (content[4] == 1)) { 314 sb.append("\n "); 315 sb.append(formatTimeMessage(content)); 316 } 317 318 EventTable.EventTableEntry[] descr = 319 olcbInterface.getEventTable().getEventInfo(ev).getAllEntries(); 320 if (descr.length > 0) { 321 sb.append("\n Uses: "); 322 sb.append(descr[0].getDescription()); 323 324 if (eventAllCheckBox.isSelected()) { 325 for (int i = 1; i < descr.length; i++) { // entry 0 done above, so skipped here 326 sb.append("\n "); 327 sb.append(descr[i].getDescription()); 328 } 329 } 330 } 331 } 332 formatted = sb.toString(); 333 } 334 } else { 335 // control type 336 String alias = String.format("0x%03X", header & 0xFFF); 337 if ((header & 0x07000000) == 0x00000000) { 338 int[] data = new int[len]; 339 System.arraycopy(content, 0, data, 0, len); 340 switch (header & 0x00FFF000) { 341 case 0x00700000: 342 formatted = prefix + ": Alias " + alias + " RID frame"; 343 break; 344 case 0x00701000: 345 formatted = prefix + ": Alias " + alias + " AMD frame for node " + org.openlcb.Utilities.toHexDotsString(data); 346 break; 347 case 0x00702000: 348 formatted = prefix + ": Alias " + alias + " AME frame for node " + org.openlcb.Utilities.toHexDotsString(data); 349 break; 350 case 0x00703000: 351 formatted = prefix + ": Alias " + alias + " AMR frame for node " + org.openlcb.Utilities.toHexDotsString(data); 352 break; 353 default: 354 formatted = prefix + ": Unknown CAN control frame: " + raw; 355 break; 356 } 357 } else { 358 formatted = prefix + ": Alias " + alias + " CID " + ((header & 0x7000000) / 0x1000000) + " frame"; 359 } 360 } 361 nextLine(formatted + "\n", raw); 362 } 363 364 /* 365 * format a time message 366 */ 367 String formatTimeMessage(int[] content) { 368 StringBuilder sb = new StringBuilder(); 369 int clock = content[5]; 370 switch (clock) { 371 case 0: 372 sb.append(Bundle.getMessage("TimeClockDefault")); 373 break; 374 case 1: 375 sb.append(Bundle.getMessage("TimeClockReal")); 376 break; 377 case 2: 378 sb.append(Bundle.getMessage("TimeClockAlt1")); 379 break; 380 case 3: 381 sb.append(Bundle.getMessage("TimeClockAlt2")); 382 break; 383 default: 384 sb.append(Bundle.getMessage("TimeClockUnkClock")); 385 sb.append(' '); 386 sb.append(jmri.util.StringUtil.twoHexFromInt(clock)); 387 break; 388 } 389 sb.append(' '); 390 int msgType = (0xF0 & content[6]) >> 4; 391 int nib = (0x0F & content[6]); 392 int hour = (content[6] & 0x1F); 393 switch (msgType) { 394 case 0: 395 case 1: 396 sb.append(Bundle.getMessage("TimeClockTimeMsg") + " "); 397 sb.append(hour); 398 sb.append(':'); 399 if (content[7] < 10) { 400 sb.append("0"); 401 sb.append(content[7]); 402 } else { 403 sb.append(content[7]); 404 } 405 break; 406 case 2: // month day 407 sb.append(Bundle.getMessage("TimeClockDateMsg") + " "); 408 if (nib < 10) { 409 sb.append('0'); 410 } 411 sb.append(nib); 412 sb.append('/'); 413 if (content[7] < 10) { 414 sb.append('0'); 415 } 416 sb.append(content[7]); 417 break; 418 case 3: // year 419 sb.append(Bundle.getMessage("TimeClockYearMsg") + " "); 420 sb.append(nib << 8 | content[7]); 421 break; 422 case 4: // rate 423 sb.append(Bundle.getMessage("TimeClockRateMsg") + " "); 424 sb.append(' '); 425 sb.append(cvtFastClockRate(content[6], content[7])); 426 break; 427 case 8: 428 case 9: 429 sb.append(Bundle.getMessage("TimeClockSetTimeMsg") + " "); 430 sb.append(hour); 431 sb.append(':'); 432 if (content[7] < 10) { 433 sb.append("0"); 434 sb.append(content[7]); 435 } else { 436 sb.append(content[7]); 437 } 438 break; 439 case 0xA: // set date 440 sb.append(Bundle.getMessage("TimeClockSetDateMsg") + " "); 441 if (nib < 10) { 442 sb.append('0'); 443 } 444 sb.append(nib); 445 sb.append('/'); 446 if (content[7] < 10) { 447 sb.append('0'); 448 } 449 sb.append(content[7]); 450 break; 451 case 0xB: // set year 452 sb.append(Bundle.getMessage("TimeClockSetYearMsg") + " "); 453 sb.append(nib << 8 | content[7]); 454 break; 455 case 0xC: // set rate 456 sb.append(Bundle.getMessage("TimeClockSetRateMsg") + " "); 457 sb.append(cvtFastClockRate(content[6], content[7])); 458 break; 459 case 0xF: // specials 460 if (nib == 0 && content[7] ==0) { 461 sb.append(Bundle.getMessage("TimeClockQueryMsg")); 462 } else if (nib == 0 && content[7] == 1) { 463 sb.append(Bundle.getMessage("TimeClockStopMsg")); 464 } else if (nib == 0 && content[7] == 2) { 465 sb.append(Bundle.getMessage("TimeClockStartMsg")); 466 } else if (nib == 0 && content[7] == 3) { 467 sb.append(Bundle.getMessage("TimeClockDateRollMsg")); 468 } else { 469 sb.append(Bundle.getMessage("TimeClockUnkData")); 470 sb.append(' '); 471 sb.append(jmri.util.StringUtil.twoHexFromInt(content[6])); 472 sb.append(' '); 473 sb.append(jmri.util.StringUtil.twoHexFromInt(content[7])); 474 } 475 break; 476 default: 477 sb.append(Bundle.getMessage("TimeClockUnkData")); 478 sb.append(' '); 479 sb.append(jmri.util.StringUtil.twoHexFromInt(content[6])); 480 sb.append(' '); 481 sb.append(jmri.util.StringUtil.twoHexFromInt(content[7])); 482 break; 483 } 484 return(sb.toString()); 485 } 486 487 /* 488 * Convert the 12 bit signed, fixed format rate value 489 * That's 11 data and 1 sign bit 490 * Values are increments of 0.25, between 511.75 and -512.00 491 */ 492 private float cvtFastClockRate(int byte6, int byte7) { 493 int data = 0; 494 boolean sign = false; 495 float rate = 0; 496 497 data = ((byte6 & 0x3) << 8 | byte7); 498 sign = (((byte6 & 0x4) >> 3) == 0) ? false : true; 499 if (sign) { 500 rate = (float) (data / 4.0); 501 } else { 502 rate = (float) ((-1 * (~data + 1)) /4.0); 503 } 504 return rate; 505 } 506 507 /** 508 * Check if the raw data starts with the filter string, 509 * with the comparison done in upper case. If matched, 510 * the line is filtered out. 511 */ 512 @Override 513 protected boolean isFiltered(String raw) { 514 String checkRaw = getOpCodeForFilter(raw); 515 //don't bother to check filter if no raw value passed 516 if (raw != null) { 517 // if first bytes are in the skip list, exit without adding to the Swing thread 518 String[] filters = filterField.getText().toUpperCase().split(" "); 519 520 for (String s : filters) { 521 if (! s.isEmpty() && checkRaw.toUpperCase().startsWith(s.toUpperCase())) { 522 synchronized (this) { 523 linesBuffer.setLength(0); 524 } 525 return true; 526 } 527 } 528 } 529 return false; 530 } 531 532 /** 533 * Get initial part of frame contents for filtering. 534 * 535 * @param raw byte sequence 536 * @return the string without the leading ] 537 */ 538 @Override 539 protected String getOpCodeForFilter(String raw) { 540 // note: LocoNet raw is formatted like "BB 01 00 45", so extract the correct bytes from it (BB) for comparison 541 if (raw != null && raw.length() >= 2) { 542 return raw.substring(1, raw.length()); 543 } else { 544 return null; 545 } 546 } 547 548 @Override 549 public synchronized void message(CanMessage l) { // receive a message and log it 550 log.debug("Message: {}", l); 551 if ("H".equals(l.getSourceLetter())) { 552 log.debug("Suppressing message with source==H to avoid double counting"); 553 return; 554 } 555 format(l.getSourceLetter(), l.isExtended(), l.getHeader(), l.getNumDataElements(), l.getData()); 556 } 557 558 @Override 559 public synchronized void reply(CanReply l) { // receive a reply and log it 560 log.debug("Reply: {}", l); 561 format(l.getSourceLetter(), l.isExtended(), l.getHeader(), l.getNumDataElements(), l.getData()); 562 } 563 564 private final static Logger log = LoggerFactory.getLogger(MonitorPane.class); 565 566}