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