001package jmri.jmrix.can.swing.send;
002
003import java.awt.Color;
004import java.awt.event.ActionEvent;
005import java.awt.event.ActionListener;
006import java.awt.GridLayout;
007
008import javax.swing.BorderFactory;
009import javax.swing.BoxLayout;
010import javax.swing.JButton;
011import javax.swing.JCheckBox;
012import javax.swing.JLabel;
013import javax.swing.JPanel;
014import javax.swing.JSpinner;
015import javax.swing.JTextField;
016import javax.swing.JToggleButton;
017import javax.swing.SpinnerNumberModel;
018import javax.swing.SwingConstants;
019
020import jmri.jmrix.can.CanMessage;
021import jmri.jmrix.can.CanReply;
022import jmri.jmrix.can.CanSystemConnectionMemo;
023import jmri.jmrix.can.cbus.CbusConstants;
024import jmri.jmrix.can.cbus.CbusMessage;
025import jmri.jmrix.can.TrafficController;
026import jmri.jmrix.can.cbus.CbusAddress;
027import jmri.util.StringUtil;
028import jmri.util.swing.JmriJOptionPane;
029
030/**
031 * User interface for sending CAN frames to exercise the system
032 * <p>
033 * When sending a sequence of operations:
034 * <ul>
035 * <li>Send the next message and start a timer
036 * <li>When the timer trips, repeat if buttons still down.
037 * </ul>
038 *
039 * @author Bob Jacobsen Copyright (C) 2008
040 */
041public class CanSendPane extends jmri.jmrix.can.swing.CanPanel {
042
043    // member declarations
044    JLabel jLabel1 = new JLabel();
045    JButton sendButton = new JButton();
046    JTextField packetTextField = new JTextField(12);
047    JCheckBox cbusPriorityCheckbox = new JCheckBox(Bundle.getMessage("AddCbusPriorFull"));
048    JCheckBox sendAsMessage = new JCheckBox(Bundle.getMessage("SendAsMessage"));
049    JCheckBox sendAsReply = new JCheckBox(Bundle.getMessage("SendAsReply"));
050    
051    public CanSendPane() {
052        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));     
053        
054        // Handle single-packet part
055        JPanel topPane = new JPanel();
056        // Add a nice border
057        topPane.setBorder(BorderFactory.createTitledBorder(
058                BorderFactory.createEtchedBorder(), Bundle.getMessage("SendFrameTitle")));
059        
060        JPanel pane1 = new JPanel();
061        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
062        
063        JPanel entry = new JPanel();
064        jLabel1.setText(Bundle.getMessage("FrameLabel"));
065        jLabel1.setVisible(true);
066
067        sendButton.setText(Bundle.getMessage("ButtonSend"));
068        sendButton.setVisible(true);
069        sendButton.setToolTipText(Bundle.getMessage("SendToolTip"));
070
071        entry.add(jLabel1);
072        entry.add(packetTextField);
073        packetTextField.setToolTipText(Bundle.getMessage("EnterFrameToolTip"));
074        topPane.add(entry);
075        topPane.add(sendButton);
076        
077        ActionListener l = ae -> {
078            sendButtonActionPerformed(ae);
079        };
080        sendButton.addActionListener(l);
081        packetTextField.addActionListener(l);
082        
083        // Configure the sequence
084        JPanel bottomPane = new JPanel();
085        // Add a nice border
086        bottomPane.setBorder(BorderFactory.createTitledBorder(
087                BorderFactory.createEtchedBorder(), Bundle.getMessage("SendSeqTitle")));
088        bottomPane.setLayout(new BoxLayout(bottomPane, BoxLayout.Y_AXIS));
089        
090        JPanel pane2 = new JPanel();
091        pane2.setLayout(new GridLayout(MAXSEQUENCE + 2, 3));
092        pane2.add(new JLabel(" "));
093        pane2.add(new JLabel(Bundle.getMessage("PacketLabel")));
094        pane2.add(new JLabel(Bundle.getMessage("WaitLabel")));
095        for (int i = 0; i < MAXSEQUENCE; i++) {
096            JPanel numbercheckboxpane = new JPanel();
097            numbercheckboxpane.add(new JLabel(Integer.toString(i + 1)+" ",SwingConstants.RIGHT));
098            mUseField[i] = new JCheckBox();
099            mPacketField[i] = new JTextField(14);
100            numberSpinner[i] = new JSpinner(new SpinnerNumberModel(1500, 1, 1000000, 1));
101            numbercheckboxpane.add(mUseField[i]);
102            pane2.add(numbercheckboxpane);
103            pane2.add(mPacketField[i]);
104            mPacketField[i].setToolTipText(Bundle.getMessage("EnterFrameToolTip"));
105            pane2.add(numberSpinner[i]);
106        }
107        
108        pane2.add(new JLabel(" "));
109        pane2.add(mRunButton);
110        bottomPane.add(pane2);
111        
112        JPanel optionholder = new JPanel();
113        optionholder.setBorder(BorderFactory.createTitledBorder(
114            BorderFactory.createEtchedBorder(), Bundle.getMessage("Options")));
115        JPanel optionlist = new JPanel();
116            
117        optionlist.setLayout(new BoxLayout(optionlist, BoxLayout.Y_AXIS));
118        optionlist.add(cbusPriorityCheckbox);
119        optionlist.add(sendAsMessage);
120        optionlist.add(sendAsReply);
121        
122        cbusPriorityCheckbox.setSelected(true);
123        sendAsMessage.setSelected(true);
124        
125        optionholder.add(optionlist);
126        
127        add(topPane);
128        add(bottomPane);
129        add(optionholder);     
130        
131        mRunButton.setToolTipText(Bundle.getMessage("StartToolTip"));
132        mRunButton.addActionListener(this::runButtonActionPerformed);
133    }
134
135    // internal members to hold sequence widgets
136    static final int MAXSEQUENCE = 4;
137    JTextField mPacketField[] = new JTextField[MAXSEQUENCE];
138    JCheckBox mUseField[] = new JCheckBox[MAXSEQUENCE];
139    JSpinner numberSpinner[] =  new JSpinner[MAXSEQUENCE];
140    JToggleButton mRunButton = new JToggleButton(Bundle.getMessage("ButtonStart"));
141    static final Color[] FILTERCOLORS = {
142        new Color(110, 235, 131), // green ish as will have black text on top
143        new Color(68, 235, 255), // cyan ish
144        new Color(228, 255, 26), // yellow ish
145        new Color(255, 132, 84) // orange ish
146    };
147        
148    @Override
149    public void initComponents(CanSystemConnectionMemo memo) {
150        super.initComponents(memo);
151        tc = memo.getTrafficController();
152    }
153
154    @Override
155    public String getHelpTarget() {
156        return "package.jmri.jmrix.can.swing.send.CanSendFrame";
157    }
158
159    @Override
160    public String getTitle() {
161        return prependConnToString(Bundle.getMessage("MenuItemSendFrame"));
162    }
163
164    public void sendButtonActionPerformed(java.awt.event.ActionEvent e) {
165        String input = packetTextField.getText();
166        // TODO check input + feedback on error. Too easy to cause NPE
167        try {
168            CanMessage m = createPacket(input.replaceAll("\\s", ""));
169            if (cbusPriorityCheckbox.isSelected()) {
170                CbusMessage.setPri(m, CbusConstants.DEFAULT_DYNAMIC_PRIORITY * 4 + CbusConstants.DEFAULT_MINOR_PRIORITY);
171            }
172            if (sendAsMessage.isSelected()) {
173                tc.sendCanMessage(m, null);
174            }
175            if (sendAsReply.isSelected()) {
176                CanReply mr = new CanReply(m);
177                tc.sendCanReply(mr, null);
178            }
179        } catch (StringIndexOutOfBoundsException | IllegalArgumentException ex) {
180            JmriJOptionPane.showMessageDialog(this, 
181            (Bundle.getMessage("NoMakeFrame",ex.getMessage())), Bundle.getMessage("WarningTitle"),
182                JmriJOptionPane.ERROR_MESSAGE);
183        }
184    }
185
186    // control sequence operation
187    int mNextSequenceElement = 0;
188    javax.swing.Timer timer = null;
189
190    /**
191     * Internal routine to handle timer starts and restarts
192     * @param delay in ms
193     */
194    protected void restartTimer(int delay) {
195        if (timer == null) {
196            timer = new javax.swing.Timer(delay, (ActionEvent e) -> {
197                sendNextItem();
198            });
199        }
200        timer.stop();
201        timer.setInitialDelay(delay);
202        timer.setRepeats(false);
203        timer.start();
204    }
205
206    /**
207     * Internal routine to handle a timeout and send next item
208     */
209    synchronized protected void timeout() {
210        sendNextItem();
211    }
212
213    /**
214     * Run button pressed down, start the sequence operation.
215     * @param e Button Event
216     */
217    public void runButtonActionPerformed(ActionEvent e) {
218        if (!mRunButton.isSelected()) {            
219            mRunButton.setText(Bundle.getMessage("ButtonStart"));
220            return;
221        }
222        // make sure at least one is checked
223        boolean ok = false;
224        for (int i = 0; i < MAXSEQUENCE; i++) {
225            if (mUseField[i].isSelected()) {
226                ok = true;
227            }
228        }
229        if (!ok) {
230            mRunButton.setSelected(false);
231            mRunButton.setText(Bundle.getMessage("ButtonStart"));
232            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("NoSelectionDialog"),
233                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.ERROR_MESSAGE);
234            return;
235        }
236        // start the operation
237        mNextSequenceElement = 0;
238        mRunButton.setText(Bundle.getMessage("ButtonStop"));
239        sendNextItem();
240    }
241
242    /**
243     * Echo has been heard, start delay for next packet.
244     */
245    private void startSequenceDelay() {
246        // at the start, mNextSequenceElement contains index we're
247        // working on
248        int delay = (Integer) numberSpinner[mNextSequenceElement].getValue();
249        // increment to next line at completion
250        mNextSequenceElement++;
251        // start timer
252        restartTimer(delay);
253    }
254
255    /**
256     * Send next item; may be used for the first item or when a delay has
257     * elapsed.
258     */
259    private void sendNextItem() {
260        // reset all backgrounds
261        for (int i = 0; i < MAXSEQUENCE; i++) {
262            mPacketField[i].setBackground(packetTextField.getBackground()); // known unaltered textfield
263        }
264        // check if still running
265        if (!mRunButton.isSelected()) {
266            mRunButton.setText(Bundle.getMessage("ButtonStart"));
267            return;
268        }
269        
270        // have we run off the end?
271        if (mNextSequenceElement >= MAXSEQUENCE) {
272            // past the end, go back
273            mNextSequenceElement = 0;
274        }
275        // is this one enabled?
276        if (mUseField[mNextSequenceElement].isSelected()) {
277            
278            mPacketField[mNextSequenceElement].setBackground(FILTERCOLORS[mNextSequenceElement]);
279            
280            try {
281                // make the packet
282                CanMessage m = createPacket(mPacketField[mNextSequenceElement].getText().replaceAll("\\s", ""));
283                if (cbusPriorityCheckbox.isSelected()) {
284                    CbusMessage.setPri(m, CbusConstants.DEFAULT_DYNAMIC_PRIORITY * 4 + CbusConstants.DEFAULT_MINOR_PRIORITY);
285                }
286                
287                // send it
288                if (sendAsMessage.isSelected()) {
289                    tc.sendCanMessage(m, null);
290                }
291                if (sendAsReply.isSelected()) {
292                    CanReply mr = new CanReply(m);
293                    tc.sendCanReply(mr, null);
294                }
295                startSequenceDelay();
296            } catch (StringIndexOutOfBoundsException | IllegalArgumentException ex) {
297                JmriJOptionPane.showMessageDialog(this, 
298                (Bundle.getMessage("NoMakeFrame", ex.getMessage())), Bundle.getMessage("WarningTitle"),
299                    JmriJOptionPane.ERROR_MESSAGE);
300                mRunButton.setSelected(false);
301                mRunButton.setText(Bundle.getMessage("ButtonStart"));
302            }
303        } else {
304            // ask for the next one
305            mNextSequenceElement++;
306            sendNextItem();
307        }
308    }
309
310    /**
311     * Create a well-formed message from a String. String is expected to be space
312     * seperated hex bytes or CbusAddress, e.g.: 12 34 56 or +n4e1
313     * @param s Input information
314     * @return The packet, with contents filled-in
315     */
316    CanMessage createPacket(String s) {
317        CanMessage m;
318        // Try to convert using CbusAddress class
319        CbusAddress a = new CbusAddress(s);
320        if (a.check()) {
321            m = a.makeMessage(tc.getCanid());
322        } else {
323            m = new CanMessage(tc.getCanid());
324            // check for header
325            if (s.charAt(0) == '[') {
326                // extended header
327                m.setExtended(true);
328                int i = s.indexOf(']');
329                String h = s.substring(1, i);
330                m.setHeader(Integer.parseInt(h, 16));
331                s = s.substring(i + 1, s.length());
332            } else if (s.charAt(0) == '(') {
333                // standard header
334                int i = s.indexOf(')');
335                String h = s.substring(1, i);
336                m.setHeader(Integer.parseInt(h, 16));
337                s = s.substring(i + 1, s.length());
338            }
339            // Try to get hex bytes
340            byte b[] = StringUtil.bytesFromHexString(s);
341            m.setNumDataElements(b.length);
342            // Use &0xff to ensure signed bytes are stored as unsigned ints
343            for (int i = 0; i < b.length; i++) {
344                m.setElement(i, b[i] & 0xff);
345            }
346        }
347        return m;
348    }
349
350    /**
351     * When the window closes, stop any sequences running
352     */
353    @Override
354    public void dispose() {
355        mRunButton.setSelected(false);
356        super.dispose();
357    }
358
359    // private data
360    private TrafficController tc = null;
361
362    /**
363     * Nested class to create one of these using old-style defaults.
364     */
365    static public class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
366
367        public Default() {
368            super(Bundle.getMessage("MenuItemSendFrame"),
369                    new jmri.util.swing.sdi.JmriJFrameInterface(),
370                    CanSendPane.class.getName(),
371                    jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class));
372        }
373    }
374
375    // private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CanSendPane.class);
376
377}