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