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}