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    // 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    // Called from synchronised code
327    public boolean isCurrentLimitPossible() {
328        return sv.hasCurrentLimit();
329    }
330
331    @SuppressFBWarnings(value = "IS2_INCONSISTENT_SYNC")
332    // Called from synchronised code
333    public boolean isBlueLineSupportPossible() {
334        return sv.hasBlueLine();
335    }
336
337    // Called from synchronised code
338    public boolean isFirmwareUnlockPossible() {
339        return sv.hasFirmwareLock();
340    }
341
342    // Called from synchronised code
343    public boolean isZTCModePossible() {
344        return sv.hasZTCMode();
345    }
346
347    /**
348     * Handle a SprogVersion notification.
349     * <p>
350     * Decode the SPROG version and populate the console gui appropriately with
351     * the features applicable to the version.
352     *
353     * @param v The SprogVersion being handled
354     */
355    @Override
356    public synchronized void notifyVersion(SprogVersion v) {
357        SprogMessage msg;
358        sv = v;
359        // Save it for others
360        _memo.setSprogVersion(v);
361        log.debug("Found: {}", sv );
362        if (sv.sprogType.isSprog() == false) {
363            // Didn't recognize a SPROG so check if it is in boot mode already
364            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("TypeNoSprogPromptFound"),
365                    Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
366        } else {
367            if ((sv.sprogType.sprogType > SprogType.SPROGIIv3) && (sv.sprogType.sprogType < SprogType.NANO)) {
368                currentTextField.setToolTipText(Bundle.getMessage("CurrentLimitFieldTooltip2500"));
369            }
370            // We know what we're connected to
371            setTitle(title() + " - Connected to " + sv.toString());
372
373            // Enable blueline & firmware unlock check boxes
374            if (isBlueLineSupportPossible()) {
375                log.debug("Enable blueline check box");
376                blueCheckBox.setEnabled(true);
377                if (log.isDebugEnabled()) {
378                    log.debug("blueCheckBox isEnabled: {}", blueCheckBox.isEnabled() );
379                }
380            }
381            if (isFirmwareUnlockPossible()) {
382                log.debug("Enable firmware check box");
383                unlockCheckBox.setEnabled(true);
384                if (log.isDebugEnabled()) {
385                    log.debug("unlockCheckBox isEnabled: {}", unlockCheckBox.isEnabled() );
386                }
387            }
388
389            ztcCheckBox.setEnabled(isZTCModePossible());
390
391            // Get Current Limit if available
392            if (isCurrentLimitPossible()) {
393                state = State.CURRENTQUERYSENT;
394                msg = new SprogMessage(1);
395                msg.setOpCode('I');
396                nextLine("cmd: \"" + msg + "\"\n", "");
397                tc.sendSprogMessage(msg, this);
398                startTimer();
399            } else {
400                // Set default and get the mode word
401                currentLimit = (int) (SprogConstants.DEFAULT_I * sv.sprogType.getCurrentMultiplier());
402                currentTextField.setText(String.valueOf(SprogConstants.DEFAULT_I));
403                //currentField.setValue(Integer.valueOf(SprogConstants.DEFAULT_I)); // TODO use JSpinner so int
404                state = State.MODEQUERYSENT;
405                msg = new SprogMessage(1);
406                msg.setOpCode('M');
407                nextLine("cmd: \"" + msg + "\"\n", "");
408                tc.sendSprogMessage(msg, this);
409                startTimer();
410            }
411        }
412    }
413
414    /**
415     * {@inheritDoc}
416     */
417    @Override
418    public synchronized void notifyMessage(SprogMessage l) { // receive a message and log it
419        nextLine("cmd: \"" + l.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
420    }
421
422    /**
423     * Handle a SprogReply in a console specific way.
424     * <p>
425     * Parse replies from the SPROG using a state machine to determine what we
426     * are expecting in response to commands sent to the SPROG. Extract data to
427     * populate various fields in the gui.
428     *
429     * @param l The SprogReply to be parsed
430     */
431    @Override
432    public synchronized void notifyReply(SprogReply l) { // receive a reply message and log it
433        SprogMessage msg;
434        int currentLimitFromHardware;
435        replyString = l.toString();
436        nextLine("rep: \"" + replyString + "\"\n", "");
437
438        // *** Check for error reply
439        switch (state) {
440            case IDLE:
441                log.debug("reply in IDLE state: {}", replyString);
442                break;
443            case CURRENTQUERYSENT:
444                // Look for an "I=" reply
445                log.debug("reply in CURRENTQUERYSENT state: {}", replyString);
446                if (replyString.contains("I=")) {
447                    stopTimer();
448                    int valueLength = 4;
449                    if (sv.sprogType.sprogType >= SprogType.SPROGIIv3) {
450                        valueLength = 6;
451                    }
452                    tmpString = replyString.substring(replyString.indexOf("=")
453                            + 1, replyString.indexOf("=") + valueLength);
454                    log.debug("Current limit string: {}", tmpString);
455                    try {
456                        currentLimitFromHardware = Integer.parseInt(tmpString);
457                    }
458                    catch (NumberFormatException e) {
459                        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorFrameDialogLimit"),
460                                Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
461                        state = State.IDLE;
462                        return;
463                    }
464                    // Value written is scaled from hardware units to mA
465                    currentLimit = (int) (currentLimitFromHardware * sv.sprogType.getCurrentMultiplier());
466                    log.debug("Current limit scale factor: {}", sv.sprogType.getCurrentMultiplier());
467                    log.debug("Current limit from hardware: {} scaled to: {}mA", currentLimitFromHardware, currentLimit);
468                    currentTextField.setText(String.valueOf(currentLimit));
469                    currentTextField.setEnabled(true);
470
471                    // Next get the mode word
472                    state = State.MODEQUERYSENT;
473                    msg = new SprogMessage(1);
474                    msg.setOpCode('M');
475                    nextLine("cmd: \"" + msg + "\"\n", "");
476                    tc.sendSprogMessage(msg, this);
477                    startTimer();
478                }
479                break;
480            case MODEQUERYSENT:
481                log.debug("reply in MODEQUERYSENT state: {}", replyString);
482                if (replyString.contains("M=")) {
483                    stopTimer();
484                    tmpString = replyString.substring(replyString.indexOf("=")
485                            + 2, replyString.indexOf("=") + 6);
486                    // Value returned is in hex
487                    try {
488                        modeWord = Integer.parseInt(tmpString, 16);
489                    }
490                    catch (NumberFormatException e) {
491                        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorFrameDialogWord"),
492                                Bundle.getMessage("SprogConsoleTitle"), JmriJOptionPane.ERROR_MESSAGE);
493                        state = State.IDLE;
494                        return;
495                    }
496                    state = State.IDLE;
497                    // Set Speed step radio buttons, etc., according to mode word
498                    if ((modeWord & SprogConstants.STEP14_BIT) != 0) {
499                        speed14Button.setSelected(true);
500                    } else if ((modeWord & SprogConstants.STEP28_BIT) != 0) {
501                        speed28Button.setSelected(true);
502                    } else {
503                        speed128Button.setSelected(true);
504                    }
505                    if ((modeWord & SprogConstants.ZTC_BIT) != 0) {
506                        ztcCheckBox.setSelected(true);
507                    }
508                    if ((modeWord & SprogConstants.BLUE_BIT) != 0) {
509                        blueCheckBox.setSelected(true);
510                    }
511                }
512                break;
513            case CURRENTSENT:
514                // Any reply will do here
515                log.debug("reply in CURRENTSENT state: {}", replyString);
516                // Get new mode word - assume 128 steps
517                modeWord = SprogConstants.STEP128_BIT;
518                if (speed14Button.isSelected()) {
519                    modeWord = modeWord & ~SprogConstants.STEP_MASK | SprogConstants.STEP14_BIT;
520                } else if (speed28Button.isSelected()) {
521                    modeWord = modeWord & ~SprogConstants.STEP_MASK | SprogConstants.STEP28_BIT;
522                }
523
524                // ZTC mode
525                if (ztcCheckBox.isSelected() == true) {
526                    modeWord = modeWord | SprogConstants.ZTC_BIT;
527                }
528
529                // Blueline mode
530                if (blueCheckBox.isSelected() == true) {
531                    modeWord = modeWord | SprogConstants.BLUE_BIT;
532                }
533
534                // firmware unlock
535                if (unlockCheckBox.isSelected() == true) {
536                    modeWord = modeWord | SprogConstants.UNLOCK_BIT;
537                }
538
539                // Send new mode word
540                state = State.MODESENT;
541                msg = new SprogMessage("M " + modeWord);
542                nextLine("cmd: \"" + msg.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
543                tc.sendSprogMessage(msg, this);
544                break;
545            case MODESENT:
546                // Any reply will do here
547                log.debug("reply in MODESENT state: {}", replyString);
548                // Write to EEPROM
549                state = State.WRITESENT;
550                msg = new SprogMessage("W");
551                nextLine("cmd: \"" + msg.toString(_memo.getSprogTrafficController().isSIIBootMode()) + "\"\n", "");
552                tc.sendSprogMessage(msg, this);
553                break;
554            case WRITESENT:
555                // Any reply will do here
556                log.debug("reply in WRITESENT state: {}", replyString);
557                // All done
558                state = State.IDLE;
559                break;
560            default:
561                log.warn("Unhandled state: {}", state);
562                break;
563        }
564    }
565
566    /**
567     * Internal routine to handle a timeout.
568     */
569    protected synchronized void timeout() {
570        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("TypeTimeoutTalkingToSPROG"),
571                Bundle.getMessage("Timeout"), JmriJOptionPane.ERROR_MESSAGE);
572        state = State.IDLE;
573    }
574
575    protected int TIMEOUT = 1000;
576
577    javax.swing.Timer timer = null;
578
579    /**
580     * Internal routine to start timer to protect the mode-change.
581     */
582    protected void startTimer() {
583        restartTimer(TIMEOUT);
584    }
585
586    /**
587     * Internal routine to stop timer, as all is well.
588     */
589    protected void stopTimer() {
590        if (timer != null) {
591            timer.stop();
592        }
593    }
594
595    /**
596     * Internal routine to handle timer starts and restarts.
597     *
598     * @param delay milliseconds to delay
599     */
600    protected void restartTimer(int delay) {
601        if (timer == null) {
602            timer = new javax.swing.Timer(delay, (java.awt.event.ActionEvent e) -> {
603                timeout();
604            });
605        }
606        timer.stop();
607        timer.setInitialDelay(delay);
608        timer.setRepeats(false);
609        timer.start();
610    }
611
612    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SprogConsoleFrame.class);
613
614}