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