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) == 0x095B0000) && (content.length > 0)) { 235 // EWP sections 236 switch (header & 0x7000) { 237 case 0x7000: 238 formatted = prefix + ": Events with Payload 1st frame"; 239 break; 240 case 0x5000: 241 formatted = prefix + ": Events with Payload last frame"; 242 break; 243 case 0x6000: 244 formatted = prefix + ": Events with Payload middle frame"; 245 break; 246 default: 247 formatted = prefix + ": Events with Payload unknown"; 248 break; 249 } 250 } else { 251 formatted = prefix + ": Unknown message " + raw; 252 } 253 } else { 254 Message msg = list.get(0); 255 StringBuilder sb = new StringBuilder(); 256 sb.append(prefix); 257 sb.append(": "); 258 sb.append(list.get(0).toString()); 259 if (nodeNameCheckBox.isSelected() && olcbInterface != null) { 260 var ptr = olcbInterface.getNodeStore().findNode(list.get(0).getSourceNodeID()); 261 if (ptr != null && ptr.getSimpleNodeIdent() != null) { 262 String name = ""; 263 var ident = ptr.getSimpleNodeIdent(); 264 if (ident != null) { 265 name = ident.getUserName(); 266 if (name.isEmpty()) { 267 name = ident.getMfgName()+" - "+ident.getModelName(); 268 } 269 } 270 if (!name.isBlank()) { 271 sb.append("\n Src: "); 272 sb.append(name); 273 } 274 } 275 if (list.get(0) instanceof AddressedMessage) { 276 ptr = olcbInterface.getNodeStore().findNode(((AddressedMessage)list.get(0)).getDestNodeID()); 277 if (ptr != null && ptr.getSimpleNodeIdent() != null) { 278 String name = ""; 279 var ident = ptr.getSimpleNodeIdent(); 280 if (ident != null) { 281 name = ident.getUserName(); 282 if (name.isEmpty()) { 283 name = ident.getMfgName()+" - "+ident.getModelName(); 284 } 285 } 286 if (!name.isBlank()) { 287 sb.append(" Dest: "); 288 sb.append(name); 289 } 290 } 291 } 292 } 293 if ((eventCheckBox.isSelected() || eventAllCheckBox.isSelected()) && olcbInterface != null 294 && msg instanceof EventMessage) { 295 EventID ev = ((EventMessage) msg).getEventID(); 296 log.debug("event message with event {}", ev); 297 298 // this could be converted to EventTablePane.isEventNameTagPresent 299 // but that would duplicate the retrieval of the bean and user name 300 var tag = tagManager.getIdTag(OlcbConstants.tagPrefix+ev.toShortString()); 301 String tagname = null; 302 if (tag != null 303 && (tagname = tag.getUserName()) != null) { 304 if (! tagname.isEmpty()) { 305 sb.append("\n Name: "); 306 sb.append(tagname); 307 } 308 } 309 310 // check for time message 311 if ((content[0] == 1) && (content[1] == 1) && (content[2] == 0) && (content[3] == 0) && (content[4] == 1)) { 312 sb.append("\n "); 313 sb.append(formatTimeMessage(content)); 314 } 315 316 EventTable.EventTableEntry[] descr = 317 olcbInterface.getEventTable().getEventInfo(ev).getAllEntries(); 318 if (descr.length > 0) { 319 sb.append("\n Uses: "); 320 sb.append(descr[0].getDescription()); 321 322 if (eventAllCheckBox.isSelected()) { 323 for (int i = 1; i < descr.length; i++) { // entry 0 done above, so skipped here 324 sb.append("\n "); 325 sb.append(descr[i].getDescription()); 326 } 327 } 328 } 329 } 330 formatted = sb.toString(); 331 } 332 } else { 333 // control type 334 String alias = String.format("0x%03X", header & 0xFFF); 335 if ((header & 0x07000000) == 0x00000000) { 336 int[] data = new int[len]; 337 System.arraycopy(content, 0, data, 0, len); 338 switch (header & 0x00FFF000) { 339 case 0x00700000: 340 formatted = prefix + ": Alias " + alias + " RID frame"; 341 break; 342 case 0x00701000: 343 formatted = prefix + ": Alias " + alias + " AMD frame for node " + org.openlcb.Utilities.toHexDotsString(data); 344 break; 345 case 0x00702000: 346 formatted = prefix + ": Alias " + alias + " AME frame for node " + org.openlcb.Utilities.toHexDotsString(data); 347 break; 348 case 0x00703000: 349 formatted = prefix + ": Alias " + alias + " AMR frame for node " + org.openlcb.Utilities.toHexDotsString(data); 350 break; 351 default: 352 formatted = prefix + ": Unknown CAN control frame: " + raw; 353 break; 354 } 355 } else { 356 formatted = prefix + ": Alias " + alias + " CID " + ((header & 0x7000000) / 0x1000000) + " frame"; 357 } 358 } 359 nextLine(formatted + "\n", raw); 360 } 361 362 /* 363 * format a time message 364 */ 365 String formatTimeMessage(int[] content) { 366 StringBuilder sb = new StringBuilder(); 367 int clock = content[5]; 368 switch (clock) { 369 case 0: 370 sb.append(Bundle.getMessage("TimeClockDefault")); 371 break; 372 case 1: 373 sb.append(Bundle.getMessage("TimeClockReal")); 374 break; 375 case 2: 376 sb.append(Bundle.getMessage("TimeClockAlt1")); 377 break; 378 case 3: 379 sb.append(Bundle.getMessage("TimeClockAlt2")); 380 break; 381 default: 382 sb.append(Bundle.getMessage("TimeClockUnkClock")); 383 sb.append(' '); 384 sb.append(jmri.util.StringUtil.twoHexFromInt(clock)); 385 break; 386 } 387 sb.append(' '); 388 int msgType = (0xF0 & content[6]) >> 4; 389 int nib = (0x0F & content[6]); 390 int hour = (content[6] & 0x1F); 391 switch (msgType) { 392 case 0: 393 case 1: 394 sb.append(Bundle.getMessage("TimeClockTimeMsg") + " "); 395 sb.append(hour); 396 sb.append(':'); 397 if (content[7] < 10) { 398 sb.append("0"); 399 sb.append(content[7]); 400 } else { 401 sb.append(content[7]); 402 } 403 break; 404 case 2: // month day 405 sb.append(Bundle.getMessage("TimeClockDateMsg") + " "); 406 if (nib < 10) { 407 sb.append('0'); 408 } 409 sb.append(nib); 410 sb.append('/'); 411 if (content[7] < 10) { 412 sb.append('0'); 413 } 414 sb.append(content[7]); 415 break; 416 case 3: // year 417 sb.append(Bundle.getMessage("TimeClockYearMsg") + " "); 418 sb.append(nib << 8 | content[7]); 419 break; 420 case 4: // rate 421 sb.append(Bundle.getMessage("TimeClockRateMsg") + " "); 422 sb.append(' '); 423 sb.append(cvtFastClockRate(content[6], content[7])); 424 break; 425 case 8: 426 case 9: 427 sb.append(Bundle.getMessage("TimeClockSetTimeMsg") + " "); 428 sb.append(hour); 429 sb.append(':'); 430 if (content[7] < 10) { 431 sb.append("0"); 432 sb.append(content[7]); 433 } else { 434 sb.append(content[7]); 435 } 436 break; 437 case 0xA: // set date 438 sb.append(Bundle.getMessage("TimeClockSetDateMsg") + " "); 439 if (nib < 10) { 440 sb.append('0'); 441 } 442 sb.append(nib); 443 sb.append('/'); 444 if (content[7] < 10) { 445 sb.append('0'); 446 } 447 sb.append(content[7]); 448 break; 449 case 0xB: // set year 450 sb.append(Bundle.getMessage("TimeClockSetYearMsg") + " "); 451 sb.append(nib << 8 | content[7]); 452 break; 453 case 0xC: // set rate 454 sb.append(Bundle.getMessage("TimeClockSetRateMsg") + " "); 455 sb.append(cvtFastClockRate(content[6], content[7])); 456 break; 457 case 0xF: // specials 458 if (nib == 0 && content[7] ==0) { 459 sb.append(Bundle.getMessage("TimeClockQueryMsg")); 460 } else if (nib == 0 && content[7] == 1) { 461 sb.append(Bundle.getMessage("TimeClockStopMsg")); 462 } else if (nib == 0 && content[7] == 2) { 463 sb.append(Bundle.getMessage("TimeClockStartMsg")); 464 } else if (nib == 0 && content[7] == 3) { 465 sb.append(Bundle.getMessage("TimeClockDateRollMsg")); 466 } else { 467 sb.append(Bundle.getMessage("TimeClockUnkData")); 468 sb.append(' '); 469 sb.append(jmri.util.StringUtil.twoHexFromInt(content[6])); 470 sb.append(' '); 471 sb.append(jmri.util.StringUtil.twoHexFromInt(content[7])); 472 } 473 break; 474 default: 475 sb.append(Bundle.getMessage("TimeClockUnkData")); 476 sb.append(' '); 477 sb.append(jmri.util.StringUtil.twoHexFromInt(content[6])); 478 sb.append(' '); 479 sb.append(jmri.util.StringUtil.twoHexFromInt(content[7])); 480 break; 481 } 482 return(sb.toString()); 483 } 484 485 /* 486 * Convert the 12 bit signed, fixed format rate value 487 * That's 11 data and 1 sign bit 488 * Values are increments of 0.25, between 511.75 and -512.00 489 */ 490 private float cvtFastClockRate(int byte6, int byte7) { 491 int data = 0; 492 boolean sign = false; 493 float rate = 0; 494 495 data = ((byte6 & 0x3) << 8 | byte7); 496 sign = (((byte6 & 0x4) >> 3) == 0) ? false : true; 497 if (sign) { 498 rate = (float) (data / 4.0); 499 } else { 500 rate = (float) ((-1 * (~data + 1)) /4.0); 501 } 502 return rate; 503 } 504 505 /** 506 * Check if the raw data starts with the filter string, 507 * with the comparison done in upper case. If matched, 508 * the line is filtered out. 509 */ 510 @Override 511 protected boolean isFiltered(String raw) { 512 String checkRaw = getOpCodeForFilter(raw); 513 //don't bother to check filter if no raw value passed 514 if (raw != null) { 515 // if first bytes are in the skip list, exit without adding to the Swing thread 516 String[] filters = filterField.getText().toUpperCase().split(" "); 517 518 for (String s : filters) { 519 if (! s.isEmpty() && checkRaw.toUpperCase().startsWith(s.toUpperCase())) { 520 synchronized (this) { 521 linesBuffer.setLength(0); 522 } 523 return true; 524 } 525 } 526 } 527 return false; 528 } 529 530 /** 531 * Get initial part of frame contents for filtering. 532 * 533 * @param raw byte sequence 534 * @return the string without the leading ] 535 */ 536 @Override 537 protected String getOpCodeForFilter(String raw) { 538 // note: LocoNet raw is formatted like "BB 01 00 45", so extract the correct bytes from it (BB) for comparison 539 if (raw != null && raw.length() >= 2) { 540 return raw.substring(1, raw.length()); 541 } else { 542 return null; 543 } 544 } 545 546 @Override 547 public synchronized void message(CanMessage l) { // receive a message and log it 548 log.debug("Message: {}", l); 549 format("S", 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("R", l.isExtended(), l.getHeader(), l.getNumDataElements(), l.getData()); 556 } 557 558 private final static Logger log = LoggerFactory.getLogger(MonitorPane.class); 559 560}