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