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