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