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