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