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