001package jmri.jmrix.openlcb.swing.send;
002
003import java.awt.event.ActionEvent;
004import java.awt.event.ActionListener;
005import java.awt.BorderLayout;
006import java.awt.Dimension;
007
008import javax.swing.Box;
009import javax.swing.BoxLayout;
010import javax.swing.JButton;
011import javax.swing.JCheckBox;
012import javax.swing.JComboBox;
013import javax.swing.JComponent;
014import javax.swing.JFormattedTextField;
015import javax.swing.JLabel;
016import javax.swing.JPanel;
017import javax.swing.JSeparator;
018import javax.swing.JTextField;
019import javax.swing.JToggleButton;
020
021import jmri.jmrix.can.CanListener;
022import jmri.jmrix.can.CanMessage;
023import jmri.jmrix.can.CanReply;
024import jmri.jmrix.can.CanSystemConnectionMemo;
025import jmri.jmrix.can.TrafficController;
026import jmri.jmrix.can.cbus.CbusAddress;
027import jmri.jmrix.openlcb.swing.ClientActions;
028import jmri.util.StringUtil;
029import jmri.util.javaworld.GridLayout2;
030import jmri.util.swing.WrapLayout;
031
032import org.openlcb.*;
033import org.openlcb.can.AliasMap;
034import org.openlcb.implementations.MemoryConfigurationService;
035import org.openlcb.swing.EventIdTextField;
036import org.openlcb.swing.NodeSelector;
037import org.openlcb.swing.MemorySpaceSelector;
038
039/**
040 * User interface for sending OpenLCB CAN frames to exercise the system
041 * <p>
042 * When sending a sequence of operations:
043 * <ul>
044 * <li>Send the next message and start a timer
045 * <li>When the timer trips, repeat if buttons still down.
046 * </ul>
047 *
048 * @author Bob Jacobsen Copyright (C) 2008, 2012
049 *
050 */
051public class OpenLcbCanSendPane extends jmri.jmrix.can.swing.CanPanel implements CanListener {
052
053    // member declarations
054    final JLabel jLabel1 = new JLabel();
055    final JButton sendButton = new JButton();
056    final JTextField packetTextField = new JTextField(60);
057
058    // internal members to hold sequence widgets
059    static final int MAXSEQUENCE = 4;
060    final JTextField[] mPacketField = new JTextField[MAXSEQUENCE];
061    final JCheckBox[] mUseField = new JCheckBox[MAXSEQUENCE];
062    final JTextField[] mDelayField = new JTextField[MAXSEQUENCE];
063    final JToggleButton mRunButton = new JToggleButton("Go");
064
065    final JTextField srcAliasField = new JTextField(4);
066    NodeSelector nodeSelector;
067    final JFormattedTextField sendEventField = new EventIdTextField();// NOI18N
068    final JTextField datagramContentsField = new JTextField("20 61 00 00 00 00 08");  // NOI18N
069    final JTextField configNumberField = new JTextField("40");                        // NOI18N
070    final JTextField configAddressField = new JTextField("000000");                   // NOI18N
071    final JTextField readDataField = new JTextField(60);
072    final JTextField writeDataField = new JTextField(60);
073    final MemorySpaceSelector addrSpace = new MemorySpaceSelector(0xFF);
074    final JComboBox<String> validitySelector = new JComboBox<String>(new String[]{"Unknown", "Valid", "Invalid"});
075    JButton cdiButton;
076    
077    Connection connection;
078    AliasMap aliasMap;
079    NodeID srcNodeID;
080    MemoryConfigurationService mcs;
081    MimicNodeStore store;
082    OlcbInterface iface;
083    ClientActions actions;
084
085    public OpenLcbCanSendPane() {
086        // most of the action is in initComponents
087    }
088
089    @Override
090    public void initComponents(CanSystemConnectionMemo memo) {
091        super.initComponents(memo);
092        iface = memo.get(OlcbInterface.class);
093        actions = new ClientActions(iface, memo);
094        tc = memo.getTrafficController();
095        tc.addCanListener(this);
096        connection = memo.get(org.openlcb.Connection.class);
097        srcNodeID = memo.get(org.openlcb.NodeID.class);
098        aliasMap = memo.get(org.openlcb.can.AliasMap.class);
099
100        // register request for notification
101        Connection.ConnectionListener cl = new Connection.ConnectionListener() {
102            @Override
103            public void connectionActive(Connection c) {
104                log.debug("connection active");
105                // load the alias field
106                srcAliasField.setText(Integer.toHexString(aliasMap.getAlias(srcNodeID)));
107            }
108        };
109        connection.registerStartNotification(cl);
110
111        mcs = memo.get(MemoryConfigurationService.class);
112        store = memo.get(MimicNodeStore.class);
113        nodeSelector = new NodeSelector(store);
114        nodeSelector.addActionListener (new ActionListener () {
115            public void actionPerformed(ActionEvent e) {
116                setCdiButton();
117            }
118        });
119
120        // start window layout
121        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
122
123        // handle single-packet part
124        add(getSendSinglePacketJPanel());
125
126        add(new JSeparator());
127
128        // Configure the sequence
129        add(new JLabel("Send sequence of frames:"));
130        JPanel pane2 = new JPanel();
131        pane2.setLayout(new GridLayout2(MAXSEQUENCE + 2, 4));
132        pane2.add(new JLabel(""));
133        pane2.add(new JLabel("Send"));
134        pane2.add(new JLabel("packet"));
135        pane2.add(new JLabel("wait (msec)"));
136        for (int i = 0; i < MAXSEQUENCE; i++) {
137            pane2.add(new JLabel(Integer.toString(i + 1)));
138            mUseField[i] = new JCheckBox();
139            mPacketField[i] = new JTextField(20);
140            mDelayField[i] = new JTextField(10);
141            pane2.add(mUseField[i]);
142            pane2.add(mPacketField[i]);
143            pane2.add(mDelayField[i]);
144        }
145        add(pane2);
146        add(mRunButton); // below rows
147
148        mRunButton.addActionListener(this::runButtonActionPerformed);
149
150        // special packet forms
151        add(new JSeparator());
152        
153        pane2 = new JPanel();
154        pane2.setLayout(new WrapLayout());
155        add(pane2);
156        pane2.add(new JLabel("Send control frame with source alias:"));
157        pane2.add(srcAliasField);
158        JButton b;
159        b = new JButton("Send CIM");
160        b.addActionListener(this::sendCimPerformed);
161        pane2.add(b);
162
163        // send OpenLCB messages
164        add(new JSeparator());
165
166        pane2 = new JPanel();
167        pane2.setLayout(new WrapLayout());
168        add(pane2);
169        pane2.add(new JLabel("Send OpenLCB global message:"));
170        b = new JButton("Send Verify Nodes Global");
171        b.addActionListener(this::sendVerifyNodeGlobal);
172        pane2.add(b);
173        b = new JButton("Send Verify Node Global with NodeID");
174        b.addActionListener(this::sendVerifyNodeGlobalID);
175        pane2.add(b);
176
177        // event messages 
178        add(new JSeparator());
179        
180        var insert = new JPanel();
181        insert.setLayout(new WrapLayout());
182        insert.add(sendEventField);
183        insert.add(validitySelector);
184        
185        
186        add(addLineLabel("Send OpenLCB event message with eventID:", insert));
187        pane2 = new JPanel();
188        pane2.setLayout(new WrapLayout());
189        add(pane2);
190        b = new JButton("Send Request Consumers");
191        b.addActionListener(this::sendReqConsumers);
192        pane2.add(b);
193        b = new JButton("Send Consumer Identified");
194        b.addActionListener(this::sendConsumerID);
195        pane2.add(b);
196        b = new JButton("Send Request Producers");
197        b.addActionListener(this::sendReqProducers);
198        pane2.add(b);
199        b = new JButton("Send Producer Identified");
200        b.addActionListener(this::sendProducerID);
201        pane2.add(b);
202        b = new JButton("Send Event Produced");
203        b.addActionListener(this::sendEventPerformed);
204        pane2.add(b);
205
206        // addressed messages
207        add(new JSeparator());
208        add(addLineLabel("Send OpenLCB addressed message to:", nodeSelector));
209        pane2 = new JPanel();
210        pane2.setLayout(new WrapLayout());
211        add(pane2);
212        b = new JButton("Send Request Events");
213        b.addActionListener(this::sendRequestEvents);
214        pane2.add(b);
215        b = new JButton("Send PIP Request");
216        b.addActionListener(this::sendRequestPip);
217        pane2.add(b);
218        b = new JButton("Send SNIP Request");
219        b.addActionListener(this::sendRequestSnip);
220        pane2.add(b);
221
222        add(new JSeparator());
223
224        pane2 = new JPanel();
225        pane2.setLayout(new WrapLayout());
226        add(pane2);
227        b = new JButton("Send Datagram");
228        b.addActionListener(this::sendDatagramPerformed);
229        pane2.add(b);
230        pane2.add(new JLabel("Contents: "));
231        datagramContentsField.setColumns(45);
232        pane2.add(datagramContentsField);
233        b = new JButton("Send Datagram Reply");
234        b.addActionListener(this::sendDatagramReply);
235        pane2.add(b);
236
237        // send OpenLCB Configuration message
238        add(new JSeparator());
239
240        pane2 = new JPanel();
241        pane2.setLayout(new WrapLayout());
242        add(pane2);
243        
244        pane2.add(new JLabel("Send OpenLCB memory request with address: "));
245        pane2.add(configAddressField);
246        pane2.add(new JLabel("Address Space: "));
247        pane2.add(addrSpace);
248        pane2 = new JPanel();
249        pane2.setLayout(new WrapLayout());
250        add(pane2);
251        pane2.add(new JLabel("Byte Count: "));
252        pane2.add(configNumberField);
253        b = new JButton("Read");
254        b.addActionListener(this::readPerformed);
255        pane2.add(b);
256        pane2.add(new JLabel("Data: "));
257        pane2.add(readDataField);
258
259        pane2 = new JPanel();
260        pane2.setLayout(new WrapLayout());
261        add(pane2);
262        b = new JButton("Write");
263        b.addActionListener(this::writePerformed);
264        pane2.add(b);
265        pane2.add(new JLabel("Data: "));
266        writeDataField.setText("00 00");   // NOI18N
267        pane2.add(writeDataField);
268
269        pane2 = new JPanel();
270        pane2.setLayout(new WrapLayout());
271        add(pane2);
272
273        var restartButton = new JButton("Restart");
274        pane2.add(restartButton);
275        restartButton.addActionListener(this::restartNode);
276        
277        cdiButton = new JButton("Open CDI Config Tool");
278        pane2.add(cdiButton);
279        cdiButton.addActionListener(e -> openCdiPane());
280        cdiButton.setToolTipText("If this button is disabled, please select another node.");
281        setCdiButton(); // get initial state
282
283        var clearCacheButton = new JButton("Clear CDI Cache");
284        pane2.add(clearCacheButton);
285        clearCacheButton.addActionListener(this::clearCache);
286        clearCacheButton.setToolTipText("Closes any open configuration windows and forces a CDI reload");
287
288        // listen for mimic store changes to set CDI button
289        store.addPropertyChangeListener(e -> {
290            setCdiButton();
291        });
292        jmri.util.ThreadingUtil.runOnGUIDelayed( ()->{ 
293            setCdiButton(); 
294        }, 500);
295    }
296
297    /**
298     * Set whether Open CDI button is enabled based on whether
299     * the selected node has CDI in its PIP
300     */
301    protected void setCdiButton() {
302        var nodeID = nodeSelector.getSelectedNodeID();
303        if (nodeID == null) { 
304            cdiButton.setEnabled(false);
305            return;
306        }
307        var pip = store.getProtocolIdentification(nodeID);
308        if (pip == null || pip.getProtocols() == null) { 
309            cdiButton.setEnabled(false);
310            return;
311        }
312        cdiButton.setEnabled(
313            pip.getProtocols()
314                .contains(org.openlcb.ProtocolIdentification.Protocol.ConfigurationDescription));
315    }
316    
317    private JPanel getSendSinglePacketJPanel() {
318        JPanel outer = new JPanel();
319        outer.setLayout(new BoxLayout(outer, BoxLayout.X_AXIS));
320        
321        JPanel pane1 = new JPanel();
322        pane1.setLayout(new BoxLayout(pane1, BoxLayout.Y_AXIS));
323
324        jLabel1.setText("Single Frame:  (Raw input format is [123] 12 34 56) ");
325        jLabel1.setVisible(true);
326
327        sendButton.setText("Send");
328        sendButton.setVisible(true);
329        sendButton.setToolTipText("Send frame");
330
331        packetTextField.setToolTipText("Frame as hex pairs, e.g. 82 7D; standard header in (), extended in []");
332        packetTextField.setMaximumSize(packetTextField.getPreferredSize());
333
334        pane1.add(jLabel1);
335        pane1.add(packetTextField);
336        pane1.add(sendButton);
337        pane1.add(Box.createVerticalGlue());
338
339        sendButton.addActionListener(this::sendButtonActionPerformed);
340        
341        outer.add(Box.createHorizontalGlue());
342        outer.add(pane1);
343        outer.add(Box.createHorizontalGlue());
344        return outer;
345    }
346
347    @Override
348    public String getHelpTarget() {
349        return "package.jmri.jmrix.openlcb.swing.send.OpenLcbCanSendFrame";  // NOI18N
350    }
351
352    @Override
353    public String getTitle() {
354        if (memo != null) {
355            return (memo.getUserName() + " Send CAN Frames and OpenLCB Messages");
356        }
357        return "Send CAN Frames and OpenLCB Messages";
358    }
359
360    JComponent addLineLabel(String text) {
361        return addLineLabel(text, null);
362    }
363
364    JComponent addLineLabel(String text, JComponent c) {
365        JLabel lab = new JLabel(text);
366        JPanel p = new JPanel();
367        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
368        if (c != null) {
369            p.add(lab, BorderLayout.EAST);
370            if (c instanceof JTextField) {
371                int height = lab.getMinimumSize().height+4;
372                int width = c.getMinimumSize().width;
373                Dimension d = new Dimension(width, height);
374                c.setMaximumSize(d);
375            }
376            p.add(c);
377        } else {
378            p.add(lab, BorderLayout.EAST);
379        }
380        p.add(Box.createHorizontalGlue());
381        return p;
382    }
383
384    public void sendButtonActionPerformed(java.awt.event.ActionEvent e) {
385        String input = packetTextField.getText();
386        // TODO check input + feedback on error. Too easy to cause NPE
387        CanMessage m = createPacket(input);
388        log.debug("sendButtonActionPerformed: {}",m);
389        tc.sendCanMessage(m, this);
390    }
391
392    public void sendCimPerformed(java.awt.event.ActionEvent e) {
393        String data = "[10700" + srcAliasField.getText() + "]";  // NOI18N
394        log.debug("sendCimPerformed: |{}|",data);
395        CanMessage m = createPacket(data);
396        log.debug("sendCimPerformed");
397        tc.sendCanMessage(m, this);
398    }
399
400    NodeID destNodeID() {
401        return nodeSelector.getSelectedNodeID();
402    }
403
404    EventID eventID() {
405        return new EventID(jmri.util.StringUtil.bytesFromHexString(sendEventField.getText()
406                .replace(".", " ")));
407    }
408
409    public void sendVerifyNodeGlobal(java.awt.event.ActionEvent e) {
410        Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID);
411        connection.put(m, null);
412    }
413
414    public void sendVerifyNodeGlobalID(java.awt.event.ActionEvent e) {
415        Message m = new VerifyNodeIDNumberGlobalMessage(srcNodeID, destNodeID());
416        connection.put(m, null);
417    }
418
419    public void sendRequestEvents(java.awt.event.ActionEvent e) {
420        Message m = new IdentifyEventsAddressedMessage(srcNodeID, destNodeID());
421        connection.put(m, null);
422    }
423
424    public void sendRequestPip(java.awt.event.ActionEvent e) {
425        Message m = new ProtocolIdentificationRequestMessage(srcNodeID, destNodeID());
426        connection.put(m, null);
427    }
428
429    public void sendRequestSnip(java.awt.event.ActionEvent e) {
430        Message m = new SimpleNodeIdentInfoRequestMessage(srcNodeID, destNodeID());
431        connection.put(m, null);
432    }
433
434    public void sendEventPerformed(java.awt.event.ActionEvent e) {
435        Message m = new ProducerConsumerEventReportMessage(srcNodeID, eventID());
436        connection.put(m, null);
437    }
438
439    public void sendReqConsumers(java.awt.event.ActionEvent e) {
440        Message m = new IdentifyConsumersMessage(srcNodeID, eventID());
441        connection.put(m, null);
442    }
443
444    EventState validity() {
445        switch (validitySelector.getSelectedIndex()) {
446            case 1 : return EventState.Valid;
447            case 2 : return EventState.Invalid;
448            case 0 : 
449            default: return EventState.Unknown;
450        }
451    }
452    
453    public void sendConsumerID(java.awt.event.ActionEvent e) {
454        Message m = new ConsumerIdentifiedMessage(srcNodeID, eventID(), validity());
455        connection.put(m, null);
456    }
457
458    public void sendReqProducers(java.awt.event.ActionEvent e) {
459        Message m = new IdentifyProducersMessage(srcNodeID, eventID());
460        connection.put(m, null);
461    }
462
463    public void sendProducerID(java.awt.event.ActionEvent e) {
464        Message m = new ProducerIdentifiedMessage(srcNodeID, eventID(), validity());
465        connection.put(m, null);
466    }
467
468    public void sendDatagramPerformed(java.awt.event.ActionEvent e) {
469        Message m = new DatagramMessage(srcNodeID, destNodeID(),
470                jmri.util.StringUtil.bytesFromHexString(datagramContentsField.getText()));
471        connection.put(m, null);
472    }
473
474    public void sendDatagramReply(java.awt.event.ActionEvent e) {
475        Message m = new DatagramAcknowledgedMessage(srcNodeID, destNodeID());
476        connection.put(m, null);
477    }
478
479    public void restartNode(java.awt.event.ActionEvent e) {
480        Message m = new DatagramMessage(srcNodeID, destNodeID(),
481                new byte[] {0x20, (byte) 0xA9});
482        connection.put(m, null);        
483    }
484    
485    public void clearCache(java.awt.event.ActionEvent e) {
486        jmri.jmrix.openlcb.swing.DropCdiCache.drop(destNodeID(), memo.get(OlcbInterface.class));
487    }
488    
489    public void readPerformed(java.awt.event.ActionEvent e) {
490        int space = addrSpace.getMemorySpace();
491        long addr = Integer.parseInt(configAddressField.getText(), 16);
492        int length = Integer.parseInt(configNumberField.getText());
493        mcs.requestRead(destNodeID(), space, addr,
494                length, new MemoryConfigurationService.McsReadHandler() {
495                    @Override
496                    public void handleReadData(NodeID dest, int space, long address, byte[] data) {
497                        log.debug("Read data received {} bytes",data.length);
498                        readDataField.setText(jmri.util.StringUtil.hexStringFromBytes(data));
499                    }
500
501                    @Override
502                    public void handleFailure(int errorCode) {
503                        log.warn("OpenLCB read failed: 0x{}", Integer.toHexString
504                                (errorCode));
505                    }
506                });
507    }
508
509    public void writePerformed(java.awt.event.ActionEvent e) {
510        int space = addrSpace.getMemorySpace();
511        long addr = Integer.parseInt(configAddressField.getText(), 16);
512        byte[] content = jmri.util.StringUtil.bytesFromHexString(writeDataField.getText());
513        mcs.requestWrite(destNodeID(), space, addr, content, new MemoryConfigurationService.McsWriteHandler() {
514            @Override
515            public void handleSuccess() {
516                // no action required on success
517            }
518
519            @Override
520            public void handleFailure(int errorCode) {
521                log.warn("OpenLCB write failed:  0x{}", Integer.toHexString
522                        (errorCode));
523            }
524        });
525    }
526
527    public void openCdiPane() {
528        actions.openCdiWindow(destNodeID(), destNodeID().toString());
529    }
530
531    // control sequence operation
532    int mNextSequenceElement = 0;
533    javax.swing.Timer timer = null;
534
535    /**
536     * Internal routine to handle timer starts and restarts
537     * @param delay milliseconds to delay
538     */
539    protected void restartTimer(int delay) {
540        if (timer == null) {
541            timer = new javax.swing.Timer(delay, e -> sendNextItem());
542        }
543        timer.stop();
544        timer.setInitialDelay(delay);
545        timer.setRepeats(false);
546        timer.start();
547    }
548
549    /**
550     * Internal routine to handle a timeout and send next item
551     */
552    protected synchronized void timeout() {
553        sendNextItem();
554    }
555
556    /**
557     * Run button pressed down, start the sequence operation
558     * @param e event from GUI
559     *
560     */
561    public void runButtonActionPerformed(java.awt.event.ActionEvent e) {
562        if (!mRunButton.isSelected()) {
563            return;
564        }
565        // make sure at least one is checked
566        boolean ok = false;
567        for (int i = 0; i < MAXSEQUENCE; i++) {
568            if (mUseField[i].isSelected()) {
569                ok = true;
570            }
571        }
572        if (!ok) {
573            mRunButton.setSelected(false);
574            return;
575        }
576        // start the operation
577        mNextSequenceElement = 0;
578        sendNextItem();
579    }
580
581    /**
582     * Echo has been heard, start delay for next packet
583     */
584    void startSequenceDelay() {
585        // at the start, mNextSequenceElement contains index we're
586        // working on
587        int delay = Integer.parseInt(mDelayField[mNextSequenceElement].getText());
588        // increment to next line at completion
589        mNextSequenceElement++;
590        // start timer
591        restartTimer(delay);
592    }
593
594    /**
595     * Send next item; may be used for the first item or when a delay has
596     * elapsed.
597     */
598    void sendNextItem() {
599        // check if still running
600        if (!mRunButton.isSelected()) {
601            return;
602        }
603        // have we run off the end?
604        if (mNextSequenceElement >= MAXSEQUENCE) {
605            // past the end, go back
606            mNextSequenceElement = 0;
607        }
608        // is this one enabled?
609        if (mUseField[mNextSequenceElement].isSelected()) {
610            // make the packet
611            CanMessage m = createPacket(mPacketField[mNextSequenceElement].getText());
612            // send it
613            tc.sendCanMessage(m, this);
614            startSequenceDelay();
615        } else {
616            // ask for the next one
617            mNextSequenceElement++;
618            sendNextItem();
619        }
620    }
621
622    /**
623     * Create a well-formed message from a String String is expected to be space
624     * seperated hex bytes or CbusAddress, e.g.: 12 34 56 +n4e1
625     * @param s string of spaced hex byte codes
626     * @return The packet, with contents filled-in
627     */
628    CanMessage createPacket(String s) {
629        CanMessage m;
630        // Try to convert using CbusAddress class to reuse a little code
631        CbusAddress a = new CbusAddress(s);
632        if (a.check()) {
633            m = a.makeMessage(tc.getCanid());
634        } else {
635            m = new CanMessage(tc.getCanid());
636            // check for header
637            if (s.charAt(0) == '[') {           // NOI18N
638                // extended header
639                m.setExtended(true);
640                int i = s.indexOf(']');       // NOI18N
641                String h = s.substring(1, i);
642                m.setHeader(Integer.parseInt(h, 16));
643                s = s.substring(i + 1);
644            } else if (s.charAt(0) == '(') {  // NOI18N
645                // standard header
646                int i = s.indexOf(')');       // NOI18N
647                String h = s.substring(1, i);
648                m.setHeader(Integer.parseInt(h, 16));
649                s = s.substring(i + 1);
650            }
651            // Try to get hex bytes
652            byte[] b = StringUtil.bytesFromHexString(s);
653            m.setNumDataElements(b.length);
654            // Use &0xff to ensure signed bytes are stored as unsigned ints
655            for (int i = 0; i < b.length; i++) {
656                m.setElement(i, b[i] & 0xff);
657            }
658        }
659        return m;
660    }
661
662    /**
663     * Don't pay attention to messages
664     */
665    @Override
666    public void message(CanMessage m) {
667        // ignore outgoing messages
668    }
669
670    /**
671     * Don't pay attention to replies
672     */
673    @Override
674    public void reply(CanReply m) {
675        // ignore incoming replies
676    }
677
678    /**
679     * When the window closes, stop any sequences running
680     */
681    @Override
682    public void dispose() {
683        mRunButton.setSelected(false);
684        super.dispose();
685    }
686
687    // private data
688    private TrafficController tc = null; // was CanInterface
689    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(OpenLcbCanSendPane.class);
690
691}