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