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