001package jmri.jmrix.sprog.console;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.awt.Dimension;
006import java.awt.FlowLayout;
007
008import javax.swing.BorderFactory;
009import javax.swing.BoxLayout;
010import javax.swing.ButtonGroup;
011import javax.swing.JCheckBox;
012import javax.swing.JPanel;
013import javax.swing.JRadioButton;
014
015import jmri.jmrix.sprog.SprogConstants;
016import jmri.jmrix.sprog.SprogListener;
017import jmri.jmrix.sprog.SprogMessage;
018import jmri.jmrix.sprog.SprogReply;
019import jmri.jmrix.sprog.SprogSystemConnectionMemo;
020import jmri.jmrix.sprog.SprogTrafficController;
021import jmri.jmrix.sprog.update.SprogType;
022import jmri.jmrix.sprog.update.SprogVersion;
023import jmri.jmrix.sprog.update.SprogVersionListener;
024import jmri.util.swing.JmriJOptionPane;
025
026/**
027 * Frame for Sprog Console
028 * <p>
029 * Updated Jan 2010 by Andrew Berridge - fixed errors caused by trying to send
030 * some commands while slot manager is active
031 * <p>
032 * Updated April 2016 by Andrew Crosland - remove the checks on slot manager
033 * status, implement a timeout and look for the correct replies which may be
034 * delayed by replies for slot manager.
035 * <p>
036 * Refactored, I18N
037 *
038 * @author Andrew Crosland Copyright (C) 2008, 2016
039 */
040public class SprogConsoleFrame extends jmri.jmrix.AbstractMonFrame implements SprogListener, SprogVersionListener {
041
042    private SprogSystemConnectionMemo _memo = null;
043    // member declarations
044    protected javax.swing.JLabel cmdLabel = new javax.swing.JLabel();
045    protected javax.swing.JLabel currentLabel = new javax.swing.JLabel();
046    protected javax.swing.JButton sendButton = new javax.swing.JButton();
047    protected javax.swing.JButton saveButton = new javax.swing.JButton();
048    protected javax.swing.JTextField cmdTextField = new javax.swing.JTextField(12);
049    protected javax.swing.JTextField currentTextField = new javax.swing.JTextField(12);
050
051    protected JCheckBox ztcCheckBox = new JCheckBox();
052    protected JCheckBox blueCheckBox = new JCheckBox();
053    protected JCheckBox unlockCheckBox = new JCheckBox();
054
055    protected ButtonGroup speedGroup = new ButtonGroup();
056    protected javax.swing.JLabel speedLabel = new javax.swing.JLabel();
057    protected JRadioButton speed14Button = new JRadioButton(Bundle.getMessage("ButtonXStep", 14)); // i18n using shared sprogBundle
058    protected JRadioButton speed28Button = new JRadioButton(Bundle.getMessage("ButtonXStep", 28));
059    protected JRadioButton speed128Button = new JRadioButton(Bundle.getMessage("ButtonXStep", 128));
060
061    protected int modeWord;
062    protected int currentLimit = SprogConstants.DEFAULT_I;
063
064    // members for handling the SPROG interface
065    SprogTrafficController tc = null;
066    String replyString;
067    String tmpString = null;
068    State state = State.IDLE;
069
070    SprogVersion sv;
071
072    enum State {
073
074        IDLE,
075        CURRENTQUERYSENT, // awaiting reply to "I"
076        MODEQUERYSENT, // awaiting reply to "M"
077        CURRENTSENT, // awaiting reply to "I xxx"
078        MODESENT, // awaiting reply to "M xxx"
079        WRITESENT         // awaiting reply to "W"
080    }
081
082    public SprogConsoleFrame(SprogSystemConnectionMemo memo) {
083        super();
084        _memo = memo;
085    }
086
087    /**
088     * {@inheritDoc}
089     */
090    @Override
091    protected String title() {
092        return Bundle.getMessage("SprogConsoleTitle");
093    }
094
095    /**
096     * {@inheritDoc}
097     */
098    @Override
099    protected void init() {
100        // connect to TrafficController
101        tc = _memo.getSprogTrafficController();
102        tc.addSprogListener(this);
103    }
104
105    /**
106     * {@inheritDoc}
107     */
108    @Override
109    public void dispose() {
110        if (tc != null) {
111            tc.removeSprogListener(this);
112        }
113        super.dispose();
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
120    // Ignore unsynchronized access to state
121    @Override
122    public void initComponents() {
123        //SprogMessage msg;
124        super.initComponents();
125
126        // Add a nice border to super class
127        super.jScrollPane1.setBorder(BorderFactory.createTitledBorder(
128                BorderFactory.createEtchedBorder(), Bundle.getMessage("CommandHistoryTitle")));
129
130        // Let user press return to enter message
131        entryField.addActionListener((java.awt.event.ActionEvent e) -> {
132            enterButtonActionPerformed(e);
133        });
134
135        /*
136         * Command panel
137         */
138        JPanel cmdPane1 = new JPanel();
139        cmdPane1.setBorder(BorderFactory.createTitledBorder(
140                BorderFactory.createEtchedBorder(), Bundle.getMessage("SendCommandTitle")));
141        cmdPane1.setLayout(new FlowLayout());
142
143        cmdLabel.setText(Bundle.getMessage("CommandLabel"));
144        cmdLabel.setVisible(true);
145
146        sendButton.setText(Bundle.getMessage("ButtonSend"));
147        sendButton.setVisible(true);
148        sendButton.setToolTipText(Bundle.getMessage("SendPacketTooltip"));
149
150        cmdTextField.setText("");
151        cmdTextField.setToolTipText(Bundle.getMessage("EnterSPROGCommandTooltip", Bundle.getMessage("ButtonSend")));
152        cmdTextField.setMaximumSize(
153                new Dimension(cmdTextField.getMaximumSize().width,
154                        cmdTextField.getPreferredSize().height)
155        );
156
157        cmdTextField.addActionListener((java.awt.event.ActionEvent e) -> {
158            sendButtonActionPerformed(e);
159        });
160
161        sendButton.addActionListener((java.awt.event.ActionEvent e) -> {
162            sendButtonActionPerformed(e);
163        });
164
165        cmdPane1.add(cmdLabel);
166        cmdPane1.add(cmdTextField);
167        cmdPane1.add(sendButton);
168
169        getContentPane().add(cmdPane1);
170
171        /*
172         * Speed Step Panel
173         */
174        JPanel speedPanel = new JPanel();
175        speedPanel.setBorder(BorderFactory.createEtchedBorder());
176        speedLabel.setText(Bundle.getMessage("SpeedStepModeLabel"));
177        speedPanel.add(speedLabel);
178        speedPanel.add(speed14Button);
179        speedPanel.add(speed28Button);
180        speedPanel.add(speed128Button);
181        speedGroup.add(speed14Button);
182        speedGroup.add(speed28Button);
183        speedGroup.add(speed128Button);
184        speed14Button.setToolTipText(Bundle.getMessage("ButtonXStepTooltip", 14));
185        speed28Button.setToolTipText(Bundle.getMessage("ButtonXStepTooltip", 28));
186        speed128Button.setToolTipText(Bundle.getMessage("ButtonXStepTooltip", 128));
187
188        /*
189         * Configuration panel
190         */
191        JPanel configPanel = new JPanel();
192        // *** Which versions support current limit ???
193        currentLabel.setText(Bundle.getMessage("CurrentLimitLabel"));
194        currentLabel.setVisible(true);
195
196        currentTextField.setText("");
197        currentTextField.setEnabled(false);
198        currentTextField.setToolTipText(Bundle.getMessage("CurrentLimitFieldTooltip"));
199        currentTextField.setMaximumSize(
200                new Dimension(currentTextField.getMaximumSize().width,
201                        currentTextField.getPreferredSize().height
202                )
203        );
204
205        ztcCheckBox.setText(Bundle.getMessage("ButtonSetZTCMode"));
206        ztcCheckBox.setVisible(true);
207        ztcCheckBox.setToolTipText(Bundle.getMessage("ButtonSetZTCModeTooltip"));
208
209        blueCheckBox.setText(Bundle.getMessage("ButtonSetBluelineMode"));
210        blueCheckBox.setVisible(true);
211        blueCheckBox.setEnabled(false);
212        blueCheckBox.setToolTipText(Bundle.getMessage("ButtonSetBluelineModeTooltip"));
213
214        unlockCheckBox.setText(Bundle.getMessage("ButtonUnlockFirmware"));
215        unlockCheckBox.setVisible(true);
216        unlockCheckBox.setEnabled(false);
217        unlockCheckBox.setToolTipText(Bundle.getMessage("ButtonUnlockFirmwareTooltip"));
218
219        configPanel.add(currentLabel);
220        configPanel.add(currentTextField);
221        configPanel.add(ztcCheckBox);
222        configPanel.add(blueCheckBox);
223        configPanel.add(unlockCheckBox);
224
225        /*
226         * Status Panel
227         */
228        JPanel statusPanel = new JPanel();
229        statusPanel.setBorder(BorderFactory.createTitledBorder(
230                BorderFactory.createEtchedBorder(), Bundle.getMessage("ConfigurationTitle")));
231        statusPanel.setLayout(new BoxLayout(statusPanel, BoxLayout.Y_AXIS));
232
233        saveButton.setText(Bundle.getMessage("ButtonApply"));
234        saveButton.setVisible(true);
235        saveButton.setToolTipText(Bundle.getMessage("ButtonApplyTooltip"));
236
237        saveButton.addActionListener((java.awt.event.ActionEvent e) -> {
238            saveButtonActionPerformed(e);
239        });
240
241        statusPanel.add(speedPanel);
242        statusPanel.add(configPanel);
243        statusPanel.add(saveButton);
244
245        getContentPane().add(statusPanel);
246
247        // pack for display
248        pack();
249        cmdPane1.setMaximumSize(statusPanel.getSize());
250        statusPanel.setMaximumSize(statusPanel.getSize());
251        pack();
252
253        // Now the GUI is all setup we can get the SPROG version
254        _memo.getSprogVersionQuery().requestVersion(this);
255    }
256
257    /**
258     * {@inheritDoc}
259     */
260    @Override
261    protected void setHelp() {
262        addHelpMenu("package.jmri.jmrix.sprog.console.SprogConsoleFrame", true);
263    }
264
265    public void sendButtonActionPerformed(java.awt.event.ActionEvent e) {
266        SprogMessage m = new SprogMessage(cmdTextField.getText());
267        // Messages sent by us will not be forwarded back so add to display manually
268        nextLine("cmd: \"" + m.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
269        tc.sendSprogMessage(m, this);
270    }
271
272    /**
273     * Validate the current limit value entered by the user, depending on the
274     * SPROG version.
275     */
276    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
277    // validateCurrent() is called from synchronised code
278    public void validateCurrent() {
279        String currentRange = "200 - 996";
280        int validLimit = 996;
281        if (_memo.getSprogVersion().sprogType.sprogType > SprogType.SPROGIIv3) {
282            currentRange = "200 - 2499";
283            validLimit = 2499;
284        }
285        try {
286            currentLimit = Integer.parseInt(currentTextField.getText());
287        }
288        catch (NumberFormatException e) {
289            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("CurrentLimitDialogString", currentRange),
290                    Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
291            currentLimit = validLimit;
292            return;
293        }
294        if ((currentLimit > validLimit) || (currentLimit < 200)) {
295            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("CurrentLimitDialogString", currentRange),
296                    Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
297            currentLimit = validLimit;
298        }
299    }
300
301    synchronized public void saveButtonActionPerformed(java.awt.event.ActionEvent e) {
302        SprogMessage saveMsg;
303        int currentLimitForHardware;
304        // Send Current Limit if possible
305        state = State.CURRENTSENT;
306        if (isCurrentLimitPossible()) {
307            validateCurrent();
308            // Value written is scaled from mA to hardware units
309            currentLimitForHardware = (int) (currentLimit * (1 / sv.sprogType.getCurrentMultiplier()));
310            if (sv.sprogType.sprogType < SprogType.SPROGIIv3) {
311                // Hack for SPROG bug where MSbyte of value must be non-zero
312                currentLimitForHardware += 256;
313            }
314            tmpString = String.valueOf(currentLimitForHardware);
315            saveMsg = new SprogMessage("I " + tmpString);
316        } else {
317            // Else send blank message to kick things off
318            saveMsg = new SprogMessage(" " + tmpString);
319        }
320        nextLine("cmd: \"" + saveMsg.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
321        tc.sendSprogMessage(saveMsg, this);
322
323        // Further messages will be sent from state machine
324    }
325
326    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
327    // Called from synchronised code
328    public boolean isCurrentLimitPossible() {
329        return sv.hasCurrentLimit();
330    }
331
332    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
333    // Called from synchronised code
334    public boolean isBlueLineSupportPossible() {
335        return sv.hasBlueLine();
336    }
337
338    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
339    // Called from synchronised code
340    public boolean isFirmwareUnlockPossible() {
341        return sv.hasFirmwareLock();
342    }
343
344    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
345    // Called from synchronised code
346    public boolean isZTCModePossible() {
347        return sv.hasZTCMode();
348    }
349
350    /**
351     * Handle a SprogVersion notification.
352     * <p>
353     * Decode the SPROG version and populate the console gui appropriately with
354     * the features applicable to the version.
355     *
356     * @param v The SprogVersion being handled
357     */
358    @Override
359    synchronized public void notifyVersion(SprogVersion v) {
360        SprogMessage msg;
361        sv = v;
362        // Save it for others
363        _memo.setSprogVersion(v);
364        log.debug("Found: {}", sv );
365        if (sv.sprogType.isSprog() == false) {
366            // Didn't recognize a SPROG so check if it is in boot mode already
367            JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("TypeNoSprogPromptFound"),
368                    Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
369        } else {
370            if ((sv.sprogType.sprogType > SprogType.SPROGIIv3) && (sv.sprogType.sprogType < SprogType.NANO)) {
371                currentTextField.setToolTipText(Bundle.getMessage("CurrentLimitFieldTooltip2500"));
372            }
373            // We know what we're connected to
374            setTitle(title() + " - Connected to " + sv.toString());
375
376            // Enable blueline & firmware unlock check boxes
377            if (isBlueLineSupportPossible()) {
378                log.debug("Enable blueline check box");
379                blueCheckBox.setEnabled(true);
380                if (log.isDebugEnabled()) {
381                    log.debug("blueCheckBox isEnabled: {}", blueCheckBox.isEnabled() );
382                }
383            }
384            if (isFirmwareUnlockPossible()) {
385                log.debug("Enable firmware check box");
386                unlockCheckBox.setEnabled(true);
387                if (log.isDebugEnabled()) {
388                    log.debug("unlockCheckBox isEnabled: {}", unlockCheckBox.isEnabled() );
389                }
390            }
391
392            ztcCheckBox.setEnabled(isZTCModePossible());
393
394            // Get Current Limit if available
395            if (isCurrentLimitPossible()) {
396                state = State.CURRENTQUERYSENT;
397                msg = new SprogMessage(1);
398                msg.setOpCode('I');
399                nextLine("cmd: \"" + msg + "\"\n", "");
400                tc.sendSprogMessage(msg, this);
401                startTimer();
402            } else {
403                // Set default and get the mode word
404                currentLimit = (int) (SprogConstants.DEFAULT_I * sv.sprogType.getCurrentMultiplier());
405                currentTextField.setText(String.valueOf(SprogConstants.DEFAULT_I));
406                //currentField.setValue(Integer.valueOf(SprogConstants.DEFAULT_I)); // TODO use JSpinner so int
407                state = State.MODEQUERYSENT;
408                msg = new SprogMessage(1);
409                msg.setOpCode('M');
410                nextLine("cmd: \"" + msg + "\"\n", "");
411                tc.sendSprogMessage(msg, this);
412                startTimer();
413            }
414        }
415    }
416
417    /**
418     * {@inheritDoc}
419     */
420    @Override
421    public synchronized void notifyMessage(SprogMessage l) { // receive a message and log it
422        nextLine("cmd: \"" + l.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
423    }
424
425    /**
426     * Handle a SprogReply in a console specific way.
427     * <p>
428     * Parse replies from the SPROG using a state machine to determine what we
429     * are expecting in response to commands sent to the SPROG. Extract data to
430     * populate various fields in the gui.
431     *
432     * @param l The SprogReply to be parsed
433     */
434    @Override
435    public synchronized void notifyReply(SprogReply l) { // receive a reply message and log it
436        SprogMessage msg;
437        int currentLimitFromHardware;
438        replyString = l.toString();
439        nextLine("rep: \"" + replyString + "\"\n", "");
440
441        // *** Check for error reply
442        switch (state) {
443            case IDLE:
444                log.debug("reply in IDLE state: {}", replyString);
445                break;
446            case CURRENTQUERYSENT:
447                // Look for an "I=" reply
448                log.debug("reply in CURRENTQUERYSENT state: {}", replyString);
449                if (replyString.contains("I=")) {
450                    stopTimer();
451                    int valueLength = 4;
452                    if (sv.sprogType.sprogType >= SprogType.SPROGIIv3) {
453                        valueLength = 6;
454                    }
455                    tmpString = replyString.substring(replyString.indexOf("=")
456                            + 1, replyString.indexOf("=") + valueLength);
457                    log.debug("Current limit string: {}", tmpString);
458                    try {
459                        currentLimitFromHardware = Integer.parseInt(tmpString);
460                    }
461                    catch (NumberFormatException e) {
462                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorFrameDialogLimit"),
463                                Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
464                        state = State.IDLE;
465                        return;
466                    }
467                    // Value written is scaled from hardware units to mA
468                    currentLimit = (int) (currentLimitFromHardware * sv.sprogType.getCurrentMultiplier());
469                    log.debug("Current limit scale factor: {}", sv.sprogType.getCurrentMultiplier());
470                    log.debug("Current limit from hardware: {} scaled to: {}mA", currentLimitFromHardware, currentLimit);
471                    currentTextField.setText(String.valueOf(currentLimit));
472                    currentTextField.setEnabled(true);
473
474                    // Next get the mode word
475                    state = State.MODEQUERYSENT;
476                    msg = new SprogMessage(1);
477                    msg.setOpCode('M');
478                    nextLine("cmd: \"" + msg + "\"\n", "");
479                    tc.sendSprogMessage(msg, this);
480                    startTimer();
481                }
482                break;
483            case MODEQUERYSENT:
484                log.debug("reply in MODEQUERYSENT state: {}", replyString);
485                if (replyString.contains("M=")) {
486                    stopTimer();
487                    tmpString = replyString.substring(replyString.indexOf("=")
488                            + 2, replyString.indexOf("=") + 6);
489                    // Value returned is in hex
490                    try {
491                        modeWord = Integer.parseInt(tmpString, 16);
492                    }
493                    catch (NumberFormatException e) {
494                        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("ErrorFrameDialogWord"),
495                                Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
496                        state = State.IDLE;
497                        return;
498                    }
499                    state = State.IDLE;
500                    // Set Speed step radio buttons, etc., according to mode word
501                    if ((modeWord & SprogConstants.STEP14_BIT) != 0) {
502                        speed14Button.setSelected(true);
503                    } else if ((modeWord & SprogConstants.STEP28_BIT) != 0) {
504                        speed28Button.setSelected(true);
505                    } else {
506                        speed128Button.setSelected(true);
507                    }
508                    if ((modeWord & SprogConstants.ZTC_BIT) != 0) {
509                        ztcCheckBox.setSelected(true);
510                    }
511                    if ((modeWord & SprogConstants.BLUE_BIT) != 0) {
512                        blueCheckBox.setSelected(true);
513                    }
514                }
515                break;
516            case CURRENTSENT:
517                // Any reply will do here
518                log.debug("reply in CURRENTSENT state: {}", replyString);
519                // Get new mode word - assume 128 steps
520                modeWord = SprogConstants.STEP128_BIT;
521                if (speed14Button.isSelected()) {
522                    modeWord = modeWord & ~SprogConstants.STEP_MASK | SprogConstants.STEP14_BIT;
523                } else if (speed28Button.isSelected()) {
524                    modeWord = modeWord & ~SprogConstants.STEP_MASK | SprogConstants.STEP28_BIT;
525                }
526
527                // ZTC mode
528                if (ztcCheckBox.isSelected() == true) {
529                    modeWord = modeWord | SprogConstants.ZTC_BIT;
530                }
531
532                // Blueline mode
533                if (blueCheckBox.isSelected() == true) {
534                    modeWord = modeWord | SprogConstants.BLUE_BIT;
535                }
536
537                // firmware unlock
538                if (unlockCheckBox.isSelected() == true) {
539                    modeWord = modeWord | SprogConstants.UNLOCK_BIT;
540                }
541
542                // Send new mode word
543                state = State.MODESENT;
544                msg = new SprogMessage("M " + modeWord);
545                nextLine("cmd: \"" + msg.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
546                tc.sendSprogMessage(msg, this);
547                break;
548            case MODESENT:
549                // Any reply will do here
550                log.debug("reply in MODESENT state: {}", replyString);
551                // Write to EEPROM
552                state = State.WRITESENT;
553                msg = new SprogMessage("W");
554                nextLine("cmd: \"" + msg.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
555                tc.sendSprogMessage(msg, this);
556                break;
557            case WRITESENT:
558                // Any reply will do here
559                log.debug("reply in WRITESENT state: {}", replyString);
560                // All done
561                state = State.IDLE;
562                break;
563            default:
564                log.warn("Unhandled state: {}", state);
565                break;
566        }
567    }
568
569    /**
570     * Internal routine to handle a timeout.
571     */
572    synchronized protected void timeout() {
573        JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("TypeTimeoutTalkingToSPROG"),
574                Bundle.getMessage("Timeout"), JmriJOptionPane.ERROR_MESSAGE);
575        state = State.IDLE;
576    }
577
578    protected int TIMEOUT = 1000;
579
580    javax.swing.Timer timer = null;
581
582    /**
583     * Internal routine to start timer to protect the mode-change.
584     */
585    protected void startTimer() {
586        restartTimer(TIMEOUT);
587    }
588
589    /**
590     * Internal routine to stop timer, as all is well.
591     */
592    protected void stopTimer() {
593        if (timer != null) {
594            timer.stop();
595        }
596    }
597
598    /**
599     * Internal routine to handle timer starts and restarts.
600     *
601     * @param delay milliseconds to delay
602     */
603    protected void restartTimer(int delay) {
604        if (timer == null) {
605            timer = new javax.swing.Timer(delay, (java.awt.event.ActionEvent e) -> {
606                timeout();
607            });
608        }
609        timer.stop();
610        timer.setInitialDelay(delay);
611        timer.setRepeats(false);
612        timer.start();
613    }
614
615    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SprogConsoleFrame.class);
616
617}