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