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}