001package jmri.jmrix.can.cbus.swing.bootloader;
002
003import static javax.swing.SwingUtilities.getWindowAncestor;
004
005import java.awt.BorderLayout;
006import java.awt.Dimension;
007import java.awt.event.ActionListener;
008import java.io.IOException;
009import java.text.MessageFormat;
010import java.util.*;
011
012import javax.swing.BorderFactory;
013import javax.swing.BoxLayout;
014import javax.swing.ButtonGroup;
015import javax.swing.JButton;
016import javax.swing.JCheckBox;
017import javax.swing.JFileChooser;
018import javax.swing.JFrame;
019import javax.swing.JMenu;
020import javax.swing.JPanel;
021import javax.swing.JRadioButtonMenuItem;
022import javax.swing.JScrollPane;
023import javax.swing.JTextField;
024import javax.swing.event.DocumentEvent;
025import javax.swing.event.DocumentListener;
026import javax.swing.filechooser.FileFilter;
027import javax.swing.filechooser.FileNameExtensionFilter;
028
029import jmri.jmrix.can.CanListener;
030import jmri.jmrix.can.CanMessage;
031import jmri.jmrix.can.CanReply;
032import jmri.jmrix.can.CanSystemConnectionMemo;
033import jmri.jmrix.can.TrafficController;
034import jmri.jmrix.can.cbus.CbusMessage;
035import jmri.jmrix.can.cbus.CbusSend;
036import jmri.jmrix.can.cbus.CbusConstants;
037import jmri.jmrix.can.cbus.CbusPreferences;
038import jmri.jmrix.can.cbus.node.CbusNode;
039import jmri.util.FileUtil;
040import jmri.util.ThreadingUtil;
041import jmri.util.TimerUtil;
042import jmri.util.swing.BusyDialog;
043import jmri.util.swing.TextAreaFIFO;
044
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048/**
049 * Bootloader client for uploading CBUS node firmware.
050 * <p>
051 * Update March 2022 A new CBUS bootloader protocol supports two new features:
052 * - Reading back device ID
053 * - Reading back bootloader ID 
054 * - Positive acknowledgement (or error) for write command
055 * - Possibility fro alternative checksum algorithms.
056 * <p>
057 * The module may buffer write commands in RAM, sending an immediate ACK and
058 * only writing when a FLASH page worth of data is received, which will result
059 * in a delayed ACK.
060 * <p>
061 * A new command, that will be ignored by the old bootloader, is used to request
062 * the bootloader ID. If no reply is received after a suitable timeout
063 * then the original protocol will be used.
064 * 
065 * The old protocol is only supported for older PIC18 K8x devices.
066 * 
067 * Modules based on any other devices are expected to support the new protocol.
068 *
069 * @author Andrew Crosland Copyright (C) 2020 Updates for new bootloader
070 * protocol
071 * @author Andrew Crosland Copyright (C) 2022
072 */
073public class CbusBootloaderPane extends jmri.jmrix.can.swing.CanPanel
074        implements CanListener {
075
076    private TrafficController tc;
077    private CbusSend send;
078    private CbusPreferences preferences;
079
080    private final JRadioButtonMenuItem slowWrite;
081    private final JRadioButtonMenuItem fastWrite;
082    protected JTextField nodeNumberField = new JTextField(6);
083    protected JCheckBox configCheckBox = new JCheckBox();
084    protected JCheckBox eepromCheckBox = new JCheckBox();
085    protected JCheckBox moduleCheckBox = new JCheckBox();
086    protected JButton programButton;
087    protected JButton openFileChooserButton;
088    protected JButton readNodeParamsButton;
089    private final TextAreaFIFO bootConsole;
090    private static final int MAX_LINES = 5000;
091    private final JFrame topFrame = (JFrame) getWindowAncestor(this);
092
093    // to find and remember the hex file
094    final javax.swing.JFileChooser hexFileChooser =
095            new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
096    // File to hold name of hex file
097    transient HexFile hexFile = null;
098
099    CbusParameters hardwareParams = null;
100    CbusParameters fileParams = null;
101
102    boolean hexForBootloader = false;
103    
104    int nodeNumber;
105    int nextParam;
106    
107    protected HexRecord currentRecord;
108    protected int recordIndex = 0;
109    protected boolean recordDone = false;
110
111    // Set Program memory upper limit for PIC18
112    // Only needed for old AN274 based bootloader, which had no acknowledge. Used
113    // to determine when to use a longer timeout for EEPROM and CONFIG.
114    // New modules should use the CBUS bootloader.
115    private static final int CONFIG_START = 0x200000;
116
117    BusyDialog busyDialog;
118
119    /**
120     * Bootloader protocol
121     */
122    protected enum BootProtocol {
123        UNKNOWN,
124        AN247,
125        CBUS_2_0
126    }
127    protected BootProtocol bootProtocol = BootProtocol.UNKNOWN;
128    
129    /**
130     * Bootloader checksum calculation
131     */
132    protected enum BootChecksum {
133        CHECK_2S_COMPLEMENT,
134        CHECK_CRC16
135    }
136    protected BootChecksum bootChecksum = BootChecksum.CHECK_2S_COMPLEMENT;
137    
138    /**
139     * Bootloader state machine states
140     */
141    protected enum BootState {
142        IDLE,
143        GET_PARAMS,
144        START_BOOT,
145        CHECK_BOOT_MODE,
146        WAIT_BOOT_DEVID,
147        WAIT_BOOT_ID,
148        ENABLES_SENT,
149        INIT_SENT,
150        PROG_DATA,
151        PROG_PAUSE,
152        CHECK_SENT,
153        NOP_SENT
154    }
155    protected BootState bootState = BootState.IDLE;
156    
157    /**
158     * Bootloader status values
159     */
160    protected enum BootStatus {
161        NONE,
162        PARAMETER_TIMEOUT,
163        INIT_OUT_OF_RANGE,
164        DATA_ERROR,
165        DATA_OUT_OF_RANGE,
166        ADDRESS_OUT_OF_RANGE,
167        CHECKSUM_FAILED,
168        ADDRESS_NOT_FOUND,
169        COMPLETE,
170        BOOT_TIMEOUT,
171        ACK_TIMEOUT,
172        CHECKSUM_TIMEOUT,
173        PROTOCOL_ERROR
174    }
175    
176    protected int bootAddress;
177    protected int checksum;
178    protected int dataFramesSent;
179    protected int dataTimeout;
180
181
182    public CbusBootloaderPane() {
183        super();
184        bootConsole = new TextAreaFIFO(MAX_LINES);
185        slowWrite = new JRadioButtonMenuItem(Bundle.getMessage("Slow"));
186        fastWrite = new JRadioButtonMenuItem(Bundle.getMessage("Fast"));
187    }
188
189    /**
190     * {@inheritDoc}
191     */
192    @Override
193    public void initComponents(CanSystemConnectionMemo memo) {
194        super.initComponents(memo);
195
196        // connect to the CanInterface
197        tc = memo.getTrafficController();
198        addTc(tc);
199
200        send = new CbusSend(memo, bootConsole);
201
202        preferences = memo.get(jmri.jmrix.can.cbus.CbusPreferences.class);
203
204        init();
205        setMenuOptions();
206    }
207
208
209    /**
210     * Not sure this comment really applies here as init() does not use the tc
211     * Don't use initComponent() as memo doesn't yet exist when that gets called.
212     * Instead, call init() function from initComponents(memo)
213     */
214    public void init() {
215        bootConsole.setEditable(false);
216
217        this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
218
219        // Node number selector
220        JPanel nnPane = new JPanel();
221        nnPane.setBorder(BorderFactory.createTitledBorder(
222            BorderFactory.createEtchedBorder(), Bundle.getMessage("BootNodeNumber")));
223        nnPane.add(nodeNumberField);
224
225        nodeNumberField.setText("");
226        nodeNumberField.setToolTipText(Bundle.getMessage("BootNodeNumberTT"));
227        nodeNumberField.setMaximumSize(nodeNumberField.getPreferredSize());
228        // Reset the buttons and clear parameters when a new node is selected
229        nodeNumberField.getDocument().addDocumentListener(
230                new DocumentListener() {
231                    @Override
232                    public void changedUpdate(DocumentEvent e) {
233                        resetButtons();
234                    }
235
236                    @Override
237                    public void removeUpdate(DocumentEvent e) {
238                        resetButtons();
239                    }
240
241                    @Override
242                    public void insertUpdate(DocumentEvent e) {
243                        resetButtons();
244                    }
245
246                    public void resetButtons() {
247                        openFileChooserButton.setEnabled(false);
248                        programButton.setEnabled(false);
249                        hardwareParams = null;
250                    }
251                }
252        );
253        nnPane.add(nodeNumberField);
254
255        // Memory options
256        configCheckBox.setText(Bundle.getMessage("BootWriteConfigWords"));
257        configCheckBox.setVisible(true);
258        configCheckBox.setEnabled(true);
259        configCheckBox.setSelected(false);
260        configCheckBox.setToolTipText(Bundle.getMessage("BootWriteConfigWordsTT"));
261
262        eepromCheckBox.setText(Bundle.getMessage("BootWriteEeprom"));
263        eepromCheckBox.setVisible(true);
264        eepromCheckBox.setEnabled(true);
265        eepromCheckBox.setSelected(false);
266        eepromCheckBox.setToolTipText(Bundle.getMessage("BootWriteEepromTT"));
267
268        JPanel memoryPane = new JPanel();
269        memoryPane.setBorder(BorderFactory.createTitledBorder(
270            BorderFactory.createEtchedBorder(), Bundle.getMessage("BootMemoryOptions")));
271        memoryPane.setLayout(new BoxLayout(memoryPane, BoxLayout.X_AXIS));
272        memoryPane.add(configCheckBox);
273        memoryPane.add(eepromCheckBox);
274
275        // Module sanity check
276        moduleCheckBox.setText(Bundle.getMessage("BootIgnoreParams"));
277        moduleCheckBox.setVisible(true);
278        moduleCheckBox.setEnabled(true);
279        moduleCheckBox.setSelected(false);
280        moduleCheckBox.setToolTipText(Bundle.getMessage("BootIgnoreParamsTT"));
281        
282        JPanel modulePane = new JPanel();
283        modulePane.setBorder(BorderFactory.createTitledBorder(
284            BorderFactory.createEtchedBorder(), Bundle.getMessage("BootModuleOptions")));
285        modulePane.setLayout(new BoxLayout(modulePane, BoxLayout.X_AXIS));
286        modulePane.add(moduleCheckBox);
287
288        JPanel selectPane = new JPanel();
289        selectPane.setLayout(new BoxLayout(selectPane, BoxLayout.X_AXIS));
290        selectPane.add(nnPane);
291        selectPane.add(modulePane);
292        selectPane.add(memoryPane);
293
294        // Create buttons
295        readNodeParamsButton = new JButton(Bundle.getMessage("BootReadNodeParams"));
296        readNodeParamsButton.setVisible(true);
297        readNodeParamsButton.setEnabled(true);
298        readNodeParamsButton.setToolTipText(Bundle.getMessage("BootReadNodeParamsTT"));
299        readNodeParamsButton.addActionListener((java.awt.event.ActionEvent e) -> {
300            readNodeParamsButtonActionPerformed(e);
301        });
302
303        FileFilter filter = new FileNameExtensionFilter("Hex file", new String[] {"hex"});
304        hexFileChooser.setFileFilter(filter);
305        hexFileChooser.addChoosableFileFilter(filter);
306        
307        openFileChooserButton = new JButton(Bundle.getMessage("BootChooseFile"));
308        openFileChooserButton.setVisible(true);
309        openFileChooserButton.setEnabled(false);
310        openFileChooserButton.setToolTipText(Bundle.getMessage("BootChooseFileTT"));
311        openFileChooserButton.addActionListener((java.awt.event.ActionEvent e) -> {
312            openFileChooserButtonActionPerformed(e);
313        });
314
315        programButton = new JButton(Bundle.getMessage("BootStartProgramming"));
316        programButton.setVisible(true);
317        programButton.setEnabled(false);
318        programButton.setToolTipText(Bundle.getMessage("BootStartProgrammingTT"));
319        programButton.addActionListener((java.awt.event.ActionEvent e) -> {
320            programButtonActionPerformed(e);
321        });
322
323        // add pane to hold buttons
324        JPanel buttonPane = new JPanel();
325        buttonPane.setBorder(BorderFactory.createTitledBorder(
326            BorderFactory.createEtchedBorder(), ""));
327        buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.X_AXIS));
328        buttonPane.add(readNodeParamsButton);
329        buttonPane.add(openFileChooserButton);
330        buttonPane.add(programButton);
331
332        JPanel topPane = new JPanel();
333        topPane.setLayout(new BoxLayout(topPane, BoxLayout.Y_AXIS));
334        topPane.add(selectPane);
335        topPane.add(buttonPane);
336
337        // Scroll pane for feedback area
338        JScrollPane feedbackScroll = new JScrollPane(bootConsole);
339        feedbackScroll.setBorder(BorderFactory.createTitledBorder(
340            BorderFactory.createEtchedBorder(), Bundle.getMessage("BootConsole")));
341        feedbackScroll.setPreferredSize(new Dimension(400, 200));
342
343        // Now add to a border layout so that scroll pane will absorb space
344        JPanel pane1 = new JPanel();
345        pane1.setLayout(new BorderLayout());
346        pane1.add(topPane, BorderLayout.PAGE_START);
347        pane1.add(feedbackScroll, BorderLayout.CENTER);
348
349        add(pane1);
350
351        setVisible(true);
352    }
353
354
355    /**
356     * {@inheritDoc}
357     */
358    @Override
359    public String getTitle() {
360        return prependConnToString(Bundle.getMessage("MenuItemBootloader"));
361    }
362
363
364    /**
365     * Set Menu Options, e.g., which checkboxes, etc., should be checked
366     */
367    private void setMenuOptions(){
368        slowWrite.setSelected(false);
369        fastWrite.setSelected(false);
370
371        switch (preferences.getBootWriteDelay()) {
372            case 10:
373                fastWrite.setSelected(true);
374                break;
375            case 50:
376                slowWrite.setSelected(true);
377                break;
378            default:
379                break;
380        }
381    }
382
383
384    /**
385     * Creates a Menu List.
386     *
387     * {@inheritDoc}
388     */
389    @Override
390    public List<JMenu> getMenus() {
391        List<JMenu> menuList = new ArrayList<>();
392
393        JMenu optionsMenu = new JMenu(Bundle.getMessage("Options"));
394
395        JMenu writeSpeedMenu = new JMenu(Bundle.getMessage("BootWriteSpeed"));
396        ButtonGroup backgroundFetchGroup = new ButtonGroup();
397
398        backgroundFetchGroup.add(slowWrite);
399        backgroundFetchGroup.add(fastWrite);
400
401        writeSpeedMenu.add(slowWrite);
402        writeSpeedMenu.add(fastWrite);
403
404        optionsMenu.add(writeSpeedMenu);
405
406        menuList.add(optionsMenu);
407
408        // saved preferences go through the cbus table model so they can be actioned immediately
409        // they'll be also saved by the table, not here.
410
411         // values need to match setMenuOptions()
412        ActionListener writeSpeedListener = ae -> {
413            if (slowWrite.isSelected()) {
414                preferences.setBootWriteDelay(CbusNode.BOOT_PROG_TIMEOUT_SLOW);
415            }
416            else if (fastWrite.isSelected()) {
417                preferences.setBootWriteDelay(CbusNode.BOOT_PROG_TIMEOUT_FAST);
418            }
419        };
420        slowWrite.addActionListener(writeSpeedListener);
421        fastWrite.addActionListener(writeSpeedListener);
422
423        return menuList;
424    }
425
426
427    /**
428     * Get the delay to be inserted between bootloader data writes.
429     * 
430     * For AN247, that has no handshaking can be slow or fast and then extended 
431     * for slow writes to EEPROM and CONFIG.
432     * 
433     * Only a single long timeout is used for CBUS protocol, which has full
434     * handshaking
435     *
436     * @return Delay in ms
437     */
438    int getWriteDelay() {
439        int delay = CbusNode.BOOT_PROG_TIMEOUT_FAST;
440        
441        if (bootProtocol == BootProtocol.AN247) {
442            if (slowWrite.isSelected()) {
443                delay = CbusNode.BOOT_PROG_TIMEOUT_SLOW;
444            }
445            if (bootAddress >= CONFIG_START) {
446                delay *= 8;
447            }
448        } else {
449            delay = CbusNode.BOOT_LONG_TIMEOUT_TIME;
450        }
451        
452        return delay;
453    }
454
455
456    /**
457     * Kick off the reading of parameters from the node, starting with parameter
458     * 0, the number of parameters
459     *
460     * @param e
461     */
462    private void readNodeParamsButtonActionPerformed(java.awt.event.ActionEvent e) {
463        try {
464            nodeNumber = Integer.parseInt(nodeNumberField.getText());
465        } catch (NumberFormatException e1) {
466            addToLog(Bundle.getMessage("BootInvalidNode"));
467            log.error("Invalid node number {}", nodeNumberField.getText());
468            return;
469        }
470        // Read the parameters from the chosen node
471        addToLog(Bundle.getMessage("BootReadingParams"));
472        hardwareParams = new CbusParameters();
473        nextParam = 0;
474        busyDialog = new BusyDialog(topFrame, Bundle.getMessage("BootReadingParams"), false);
475        busyDialog.start();
476        requestParam(nextParam);
477    }
478
479
480    /**
481     * Let the user choose the hex file and check that it is suitable for the
482     * selected node.
483     *
484     * @param e
485     */
486    private void openFileChooserButtonActionPerformed(java.awt.event.ActionEvent e) {
487        // start at current file, show dialog
488        int retVal = hexFileChooser.showOpenDialog(this);
489        // handle selection or cancel
490        if (retVal == JFileChooser.APPROVE_OPTION) {
491            // Nothing can be programmed until the new file has been read and checked
492            programButton.setEnabled(false);
493            hexForBootloader = false;
494            fileParams = new CbusParameters();
495            // Release any previously loaded file
496            if (hexFile != null) {
497                hexFile.dispose();
498                hexFile = null;
499            }
500
501            CbusPicHexFile newHexFile = new CbusPicHexFile(hexFileChooser.getSelectedFile().getPath());
502            log.debug("hex file chosen: {}", newHexFile.getName());
503            addToLog(MessageFormat.format(Bundle.getMessage("BootFileChosen"), newHexFile.getName()));
504            try {
505                newHexFile.openRd();
506                newHexFile.read();
507            } catch (IOException ex) {
508                log.error("Error opening hex file: {}", ex.getMessage());
509                addToLog(Bundle.getMessage("BootHexFileOpenFailed"));
510                return;
511            } finally {
512                // The whole file is held in memory so the file itself can be closed now
513                newHexFile.close();
514            }
515
516            // Only use the file once it has been read successfully
517            hexFile = newHexFile;
518            fileParams = hexFile.getParams();
519            boolean hardwareParamsValid = (hardwareParams != null) && hardwareParams.areValid();
520            if (!moduleCheckBox.isSelected()) {
521                if (fileParams.validate(fileParams, hardwareParams)) {
522                    addToLog(MessageFormat.format(Bundle.getMessage("BootHexFileFoundParameters"), fileParams.toString()));
523                    addToLog(Bundle.getMessage("BootHexFileParametersMatch"));
524                    programButton.setEnabled(true);
525                } else {
526                    addToLog(Bundle.getMessage("BootHexFileParametersMismatch"));
527                }
528            } else {
529                addToLog(Bundle.getMessage("BootHexFileIgnoringParameters"));
530                programButton.setEnabled(true);
531            }
532            if (hardwareParamsValid && (hardwareParams.getLoadAddress() == 0)) {
533                // Special case of rewriting the bootloader for Pi-SPROG One
534                addToLog(Bundle.getMessage("BootBoot"));
535                hexForBootloader = true;
536                programButton.setEnabled(true);
537            }
538        }
539    }
540
541
542    /**
543     * Send BOOTM OPC to put module in boot mode
544     *
545     * @param e
546     */
547    private void programButtonActionPerformed(java.awt.event.ActionEvent e) {
548        if (hasActiveTimers()){
549            return;
550        }
551        if (hexFile == null) {
552            log.error("No hex file loaded");
553            addToLog(Bundle.getMessage("BootHexFileOpenFailed"));
554            programButton.setEnabled(false);
555            return;
556        }
557        openFileChooserButton.setEnabled(false);
558        programButton.setEnabled(false);
559        busyDialog = new BusyDialog(topFrame, Bundle.getMessage("BootLoading"), false);
560        busyDialog.start();
561        setStartBootTimeout();
562        bootState = BootState.START_BOOT;
563        CanMessage m = CbusMessage.getBootEntry(nodeNumber, 0);
564        tc.sendCanMessage(m, null);
565    }
566
567
568    /**
569     * Process some outgoing CAN frames
570     * <p>
571     * The CBUS bootloader was originally "fire and forget", with no positive
572     * acknowledgement. We had to wait an indeterminate time and assume the
573     * write was successful.
574     * <p>
575     * A PIC based node will halt execution for some time ((10+ ms with newer Q
576     * series devices) whilst FLASH operations (erase and/or write) complete,
577     * during which time I/O will not be serviced. This is probably OK with CAN 
578     * transport, assuming the ECAN continues to accept frames. With serial
579     * (UART) transport, as used by Pi-SPROG, the timing is much more critical 
580     * as a single missed character will corrupt the node firmware.
581     * <p>
582     * Furthermore, on some platforms, e.g., Raspberry Pi, there can be
583     * considerable delays between the call to the traffic controller
584     * sendMessage() method and the message being sent by the transmit thread.
585     * This may be due to Flash file system operations and could be affected by
586     * the speed of the SD card. Once the message leaves the transmit thread, we
587     * are at the mercy of the underlying OS, where there can be further delays.
588     * <p>
589     * We could set an overlong timeout, but that would slow down the bootloading
590     * process in all cases.
591     * <p>
592     * To improve things somewhat we wait until the message has definitely
593     * reached the TC transmit thread, by looking for bootloader data write
594     * messages here. Testing indicates this is a marked improvement with no
595     * failures observed.
596     *
597     * This is unnecessary, and not used, for the new protocol which has a
598     * positive acknowledge mechanism.
599     * 
600     * @param m CanMessage
601     */
602    @Override
603    public void message(CanMessage m) {
604        if (bootProtocol == BootProtocol.AN247) {
605            if ((bootState == BootState.PROG_DATA)) {
606                if (m.isExtended() ) {
607                    if (CbusMessage.isBootWriteData(m)) {
608                        log.debug("Boot data write message {}", m);
609                        setDataTimeout(dataTimeout);
610                    }
611                }
612            }
613        }
614    }
615
616
617    /**
618     * Processes incoming CAN replies
619     * <p>
620     * The bootloader is only interested in standard parameter responses and
621     * extended bootloader responses.
622     *
623     * {@inheritDoc}
624     */
625    @Override
626    public void reply(CanReply r) {
627
628        if ( r.isRtr() ) {
629            return;
630        }
631
632        if (!r.isExtended() ) {
633            log.debug("Standard Reply {}", r);
634
635            handleStandardReply(r);
636        } else {
637//            log.debug("Extended Reply {} in state {}", r, bootState);
638            // Extended messages are only used by the bootloader
639
640            handleExtendedReply(r);
641        }
642    }
643
644
645    /**
646     * Handle standard ID CAN replies
647     *
648     * @param r Can reply
649     */
650    private void handleStandardReply(CanReply r) {
651        int opc = CbusMessage.getOpcode(r);
652        if (bootState != BootState.GET_PARAMS) {
653            log.debug("Reply not for me");
654            return;
655        }
656
657        if ( opc == CbusConstants.CBUS_PARAN) { // response from node
658            if (hardwareParams == null) {
659                log.debug("Ignoring PARAN reply, no parameters being read");
660                return;
661            }
662            // Only interested in replies from the node being programmed
663            int nn = (r.getElement(1) * 256) + r.getElement(2);
664            if (nn != nodeNumber) {
665                log.debug("Ignoring PARAN reply from node {}, expecting node {}", nn, nodeNumber);
666                return;
667            }
668            int index = r.getElement(3);
669            if (!hardwareParams.isValidIndex(index)) {
670                log.warn("Ignoring PARAN reply from node {} with out of range parameter index {}", nn, index);
671                return;
672            }
673            if (index != nextParam) {
674                log.debug("Ignoring PARAN reply for parameter {}, expecting parameter {}", index, nextParam);
675                return;
676            }
677            clearAllParamTimeout();
678
679            hardwareParams.setParam(index, r.getElement(4));
680            if ((++nextParam < (hardwareParams.getParam(0) + 1)) && hardwareParams.isValidIndex(nextParam)) {
681                // Read next
682                requestParam(nextParam);
683            } else {
684                // Done reading
685                hardwareParams.setValid(true);
686                addToLog(MessageFormat.format(Bundle.getMessage("BootNodeParametersFinished"), hardwareParams.toString()));
687                busyDialog.finish();
688                busyDialog = null;
689                openFileChooserButton.setEnabled(true);
690                bootState = BootState.IDLE;
691            }
692        } else {
693            // ignoring OPC
694        }
695    }
696
697
698    /**
699     * Handle extended ID CAN replies
700     * <p>
701     * Handle the reply in the bootloader state machine.
702     *
703     * @param r Can reply
704     */
705    private void handleExtendedReply(CanReply r) {
706        switch (bootState) {
707            default:
708                break;
709
710            case CHECK_BOOT_MODE:
711                clearCheckBootTimeout();
712                if (CbusMessage.isBootConfirm(r)) {
713                    // The node is in boot mode so we can look for the device ID
714                    requestDevId();
715                }
716                break;
717
718            case WAIT_BOOT_DEVID:
719                clearDevIdTimeout();
720                if (CbusMessage.isBootDevId(r)) {
721                    // We had a response to the Device ID request so we can proceed with the new protocol
722                    showDevId(r);
723                    bootProtocol = BootProtocol.CBUS_2_0;
724                    requestBootId();
725                } else {
726                    protocolError();
727                }
728                break;
729                
730            case WAIT_BOOT_ID:
731                clearBootIdTimeout();
732                if (CbusMessage.isBootId(r)) {
733                    // We had a response to the bootloader ID request so send the write enables
734                    showBootId(r);
735                    sendBootEnables();
736                } else {
737                    protocolError();
738                }
739                break;
740                
741            case ENABLES_SENT:
742                clearAckTimeout();
743                if (CbusMessage.isBootOK(r)) {
744                    // We had a response to the enables so start programming.
745                    initialise();
746                } else {
747                    protocolError();
748                }
749                break;
750                        
751            case INIT_SENT:
752                clearAckTimeout();
753                if (CbusMessage.isBootOK(r)) {
754                    // We had a response to the initislise so start programming.
755                    writeNextData();
756                } else if (CbusMessage.isBootOutOfRange(r)) {
757                    log.error("INIT Address out of range");
758                    endProgramming(BootStatus.INIT_OUT_OF_RANGE);
759                } else {
760                    protocolError();
761                }
762                break;
763                        
764            case PROG_DATA:
765                clearAckTimeout();
766                if (CbusMessage.isBootDataOK(r)) {
767                    // Acknowledge received for CBUS protocol
768                    writeNextData();
769                } else if (CbusMessage.isBootError(r)){
770                    log.error("Data Error");
771                    endProgramming(BootStatus.DATA_ERROR);
772                } else if (CbusMessage.isBootDataOutOfRange(r)) {
773                    log.error("Data Address out of range");
774                    endProgramming(BootStatus.DATA_OUT_OF_RANGE);
775                } else {
776                    protocolError();
777                }
778                break;
779                
780            case NOP_SENT:
781                clearAckTimeout();
782                if (CbusMessage.isBootOK(r)) {
783                    // Acknowledge received for NOP
784                    bootState = BootState.PROG_DATA;
785                    writeNextData();
786                } else if (CbusMessage.isBootOutOfRange(r)) {
787                    log.error("NOP Address out of range");
788                    endProgramming(BootStatus.ADDRESS_OUT_OF_RANGE);
789                } else {
790                    protocolError();
791                }
792                break;
793                
794            case CHECK_SENT:
795                clearCheckTimeout();
796                if (CbusMessage.isBootOK(r)) {
797                    sendReset();
798                } else if (CbusMessage.isBootError(r)) {
799                    // Checksum verify failed
800                    log.error("Node {} checksum failed", nodeNumber);
801                    endProgramming(BootStatus.CHECKSUM_FAILED);
802                } else {
803                    protocolError();
804                }
805                break;
806        }
807    }
808
809    
810    /**
811     * Show the device ID
812     * 
813     * Manufacturere and device from cbusdefs.h, device ID from the device
814     * 
815     * @param r device ID reply
816     */
817    void showDevId(CanReply r) {
818        long devId = getDevId(r);
819        log.debug("Found device ID Manu: {} Dev: {} Device ID: {}",
820                r.getElement(1),
821                r.getElement(2),
822                devId);
823        addToLog(MessageFormat.format(Bundle.getMessage("DevIdCbus"),
824                r.getElement(1),
825                r.getElement(2),
826                devId));
827    }
828
829
830    /**
831     * Extract the 4-byte device ID from a device ID reply
832     * <p>
833     * Reply data is [CBUS_EXT_DEVID, manufacturer, device, ID3, ID2, ID1, ID0]
834     *
835     * @param r device ID reply
836     * @return the device ID, as an unsigned value
837     */
838    static long getDevId(CanReply r) {
839        return ((long)(r.getElement(3) & 0xFF) << 24)
840                + ((r.getElement(4) & 0xFF) << 16)
841                + ((r.getElement(5) & 0xFF) << 8)
842                + (r.getElement(6) & 0xFF);
843    }
844    
845   
846    /**
847     * Show the bootloader ID
848     * 
849     * Major/Minor version number, checksum algorithm error report capability
850     * 
851     * @param r Bootloader ID reply
852     */
853    void showBootId(CanReply r) {
854        log.debug("Found bootloader Major: {} Minor: {} Algo: {} Reports: {}",
855                r.getElement(1),
856                r.getElement(2),
857                r.getElement(3),
858                r.getElement(4));
859        addToLog(MessageFormat.format(Bundle.getMessage("BootIdCbus"),
860                r.getElement(1),
861                r.getElement(2),
862                r.getElement(3),
863                r.getElement(4)));
864    }
865    
866    
867    /**
868     * Send the memory region write enable bit mask for CBUS bootloader protocol
869     */
870    void sendBootEnables() {
871        int enables = 1;    // Prog mem always enabled
872        
873        if (eepromCheckBox.isSelected()) {
874            enables |= 2;
875        }
876        if (configCheckBox.isSelected()) {
877            enables |= 4;
878        }
879        
880        bootState = BootState.ENABLES_SENT;
881        setAckTimeout();
882        CanMessage m = CbusMessage.getBootEnables(enables, 0);
883        log.debug("Send boot enables {}", enables);
884        addToLog(MessageFormat.format(Bundle.getMessage("BootEnables"), enables));
885        tc.sendCanMessage(m, null);
886    }
887    
888    
889    /**
890     * Protocol Error
891     */
892    void protocolError() {
893        log.error("Bootloader Protocol Error in state {}", bootState.toString());
894        addToLog(MessageFormat.format(Bundle.getMessage("BootProtocol"), bootState.toString()));
895        endProgramming(BootStatus.PROTOCOL_ERROR);
896    }
897    
898    
899    /**
900     * Is Programming Needed
901     * 
902     * Check if any data bytes actually need programming
903     * 
904     * @param d data bytes to check
905     * @return false if all bytes are 0xFF, else true
906     */
907    boolean isProgrammingNeeded(byte [] d) {
908        for (int i = 0; i < d.length; i++) {
909            if (d[i] != (byte)0xFF) {
910                return true;
911            }
912        }
913        return false;
914    }
915
916    
917    protected void logFrame(CanMessage m) {
918        log.debug("Write frame {} at address {} {}", dataFramesSent, Integer.toHexString(bootAddress), m);
919        if ((bootAddress & 0xFF) == 0) {
920            addToLog(MessageFormat.format(Bundle.getMessage("BootAddress"), Integer.toHexString(bootAddress)));
921        } else {
922            bootConsole.append(".");
923        }        
924    }
925    
926    
927    /**
928     * Check if data is filtered (e.g., EEPROM selection unticked)
929     * 
930     * Used only for AN247
931     * 
932     * @param address of data record
933     * @return true if data is filtered and should not be written
934     */
935    protected boolean dataIsFiltered(int address) {
936        if ((address >= 0x300000) && (address < 0x310000) && (!configCheckBox.isSelected())) {
937            // PIC18 Config space at 0x200000 is filtered
938            return true;
939        } else if ((address >= 0x310000) && (!eepromCheckBox.isSelected())) {
940            // PIC18 EEPROM space at 0x300000, 0x310000 or 0x380000 is filtered
941            return true;
942        }
943        return false;
944    }
945    
946    
947    /**
948     * Send data to the hardware and keep a running checksum
949     *
950     * @param timeout timeout for write operation
951     */
952    protected void sendData(int timeout) {
953
954        byte [] d = getDataFromRecord();
955        dataFramesSent++;
956        
957        CanMessage m = CbusMessage.getBootWriteData(d, 0);
958        if (bootProtocol == BootProtocol.CBUS_2_0) {
959            setAckTimeout();
960            updateChecksum(d);
961            logFrame(m);       
962            tc.sendCanMessage(m, null);
963        } else {
964            // For AN247 protocol, we need to filter data
965            if (!dataIsFiltered(bootAddress)) {
966                // Timeout will be set when we see the outgoing message
967                dataTimeout = timeout;
968                updateChecksum(d);
969                logFrame(m);       
970                tc.sendCanMessage(m, null);
971            } else {
972                // No data to send, set short timeout to trigger next data
973                setDataTimeout(10);
974            }
975        }
976        bootAddress += d.length;
977    }
978
979    
980    /**
981     * Extract data from the current hex record
982     * 
983     * Returns 8 byte array or whatever is left in the record if less than 8 bytes.
984     * 
985     * Sets recordDone flag if record is exhausted.
986     * 
987     * @return data array
988     */
989    private byte [] getDataFromRecord() {
990        byte [] d;
991        
992        if (currentRecord.len - recordIndex >= 8) {
993            d = new byte[8];
994            if (currentRecord.len - recordIndex == 8) {
995                recordDone = true;
996            }
997        } else {
998            d = new byte[currentRecord.len - recordIndex];
999            recordDone = true;
1000        }
1001        for (int i = 0; i < d.length; i++) {
1002            d[i] = currentRecord.getData(recordIndex++);
1003        }
1004        return d;
1005    }
1006
1007    
1008    /**
1009     * Write next data for AN247 protocol
1010     */
1011    void writeNextDataAn247() {
1012//        log.debug("writeNextDataAn247()");
1013        
1014        if ((bootAddress == 0x7f8) && (hexForBootloader == true)) {
1015            log.debug("Pause for bootloader reset");
1016            // Special case for Pi-SPROG One, pause at end of bootloader code to allow time for node to reset
1017            bootAddress = 0x800;
1018            checksum = 0;
1019            bootState = BootState.PROG_PAUSE;
1020            setPauseTimeout();
1021        } else {
1022            // If the address has skipped we need to send a new address to the bootloader
1023            // There's no ACK so send data immediately afterwards
1024            if ((currentRecord.address + recordIndex) != bootAddress) {
1025                bootAddress = currentRecord.address;
1026                // Send NOP to adjust the address, no reply to this from AN247
1027                log.debug("Start writing at new address {}", Integer.toHexString(bootAddress));
1028                addToLog(MessageFormat.format(Bundle.getMessage("BootNewAddress"), Integer.toHexString(bootAddress)));
1029                CanMessage m = CbusMessage.getBootNop(bootAddress, 0);
1030                tc.sendCanMessage(m, null);
1031            }
1032            if ((bootAddress < CONFIG_START) && (currentRecord.len%8 != 0)
1033                    && ((currentRecord.len + (8 - currentRecord.len%8)) <= currentRecord.data.length)) {
1034                // AN247 bootloader always writes 8 bytes to FLASH so we need to pad the packet and adjust the length
1035                int pad = 8 - currentRecord.len%8;
1036                for (int i = 0; i < pad; i++) {
1037                    currentRecord.data[currentRecord.len + i] = (byte)0xFF;
1038                }
1039                currentRecord.len += pad;
1040            }
1041            sendData(getWriteDelay());
1042        }
1043    }
1044
1045
1046    /**
1047     * Write next data for CBUS protocol
1048     */
1049    void writeNextDataCbus() {
1050//        log.debug("writeNextDataCbus()");
1051
1052        // If the address has skipped we need to send a new address to the bootloader
1053        if ((currentRecord.address + recordIndex) != bootAddress) {
1054            bootAddress = currentRecord.address;
1055            // Send NOP to adjust the address 
1056            log.debug("Start writing at new address {}", Integer.toHexString(bootAddress));
1057            addToLog(MessageFormat.format(Bundle.getMessage("BootNewAddress"), Integer.toHexString(bootAddress)));
1058            bootState = BootState.NOP_SENT;
1059            setAckTimeout();
1060            CanMessage m = CbusMessage.getBootNop(bootAddress, 0);
1061            tc.sendCanMessage(m, null);
1062        } else {
1063            // Extract the data, send it and update bootAddress for next packet
1064            sendData(getWriteDelay());
1065        }
1066    }
1067
1068
1069    /**
1070     * Write the next data frame for the bootloader
1071     */
1072    void writeNextData() {
1073        if (recordDone) {
1074            // Current record is exhausted, Get next ONE
1075            recordDone = false;
1076            recordIndex = 0;
1077            if (hexFile == null) {
1078                log.error("No hex file loaded");
1079                endProgramming(BootStatus.ADDRESS_NOT_FOUND);
1080                return;
1081            }
1082            currentRecord = hexFile.getNextRecord();
1083            if (currentRecord.type == HexRecord.END) {
1084                // No more data to send so send checksum
1085                bootState = BootState.CHECK_SENT;
1086                addToLog(Bundle.getMessage("BootVerifyChecksum"));
1087                log.debug("Sending checksum {} as 2s complement {}", checksum, 0 - checksum);
1088                setCheckTimeout();
1089                CanMessage m = CbusMessage.getBootCheck(0 - checksum, 0);
1090                tc.sendCanMessage(m, null);
1091                return;
1092            }
1093        }
1094        
1095        bootState = BootState.PROG_DATA;
1096        if (bootProtocol == BootProtocol.AN247) {
1097            writeNextDataAn247();
1098        } else {
1099            writeNextDataCbus();
1100        }
1101    }
1102
1103
1104    /**
1105     * Initialise programming
1106     * 
1107     * We normally start at the address from the module parameters, or from the 
1108     * hex file, otherwise start at the beginning of the hex file. 
1109     */
1110    private void initialise() {
1111        Optional<HexRecord> hexRecord;
1112
1113        if (hexFile == null) {
1114            log.error("No hex file loaded");
1115            endProgramming(BootStatus.ADDRESS_NOT_FOUND);
1116            return;
1117        }
1118
1119        if ((hardwareParams != null) && hardwareParams.areValid()) {
1120            bootAddress = hardwareParams.getLoadAddress();
1121        } else if ((fileParams != null) && fileParams.areValid()) {
1122            bootAddress = fileParams.getLoadAddress();
1123        } else {
1124            bootAddress = hexFile.getProgStart();
1125        }
1126
1127        recordDone = false;
1128        recordIndex = 0;
1129
1130        hexRecord = hexFile.getRecordForAddress(bootAddress);
1131        if (hexRecord.isPresent()) {
1132            currentRecord = hexRecord.get();
1133        } else {
1134            log.error("Did not find hex record for load address {}", "0x"+Integer.toHexString(bootAddress));
1135            endProgramming(BootStatus.ADDRESS_NOT_FOUND);
1136            return;
1137        }
1138        checksum = 0;
1139        dataFramesSent = 0;
1140        log.debug("Initialise at address {}", "0x"+Integer.toHexString(bootAddress));
1141        addToLog(MessageFormat.format(Bundle.getMessage("BootStartAddress"), Integer.toHexString(bootAddress)));
1142        // Initialise the bootloader, only CBUS protocol will ACK this
1143        if (bootProtocol == BootProtocol.CBUS_2_0) {
1144            setAckTimeout();
1145        }
1146        CanMessage m = CbusMessage.getBootInitialise(bootAddress, 0);
1147        bootState = BootState.INIT_SENT;
1148        tc.sendCanMessage(m, null);
1149        if (bootProtocol == BootProtocol.AN247) {
1150            // No wait for ACK so start sending data
1151            writeNextData();
1152        }
1153    }
1154
1155    
1156    protected void requestDevId() {
1157        CanMessage m = CbusMessage.getBootDevId(0);
1158        log.debug("Requesting bootloader device ID...");
1159        addToLog(Bundle.getMessage("ReqDevId"));
1160        bootState = BootState.WAIT_BOOT_DEVID;
1161        setDevIdTimeout();
1162        tc.sendCanMessage(m, null);
1163    }
1164
1165    
1166    protected void requestBootId() {
1167        CanMessage m = CbusMessage.getBootId(0);
1168        log.debug("Requesting bootloader ID...");
1169        addToLog(Bundle.getMessage("ReqBootId"));
1170        bootState = BootState.WAIT_BOOT_ID;
1171        setBootIdTimeout();
1172        tc.sendCanMessage(m, null);
1173    }
1174
1175
1176    /**
1177     * Send bootloader reset frame to put the node back into operating mode.
1178     *
1179     * There will be no reply to this.
1180     */
1181    protected void sendReset() {
1182        CanMessage m = CbusMessage.getBootReset(0);
1183        log.debug("Done. Resetting node...");
1184        addToLog(Bundle.getMessage("BootFinished"));
1185        tc.sendCanMessage(m, null);
1186        endProgramming(BootStatus.COMPLETE);
1187    }
1188
1189
1190    /**
1191     * Tidy up after programming success or failure
1192     */
1193    private void endProgramming(BootStatus status) {
1194        log.debug("Boot status is {}", status.toString());
1195        addToLog(MessageFormat.format(Bundle.getMessage("BootStatus"), status.toString()));
1196        if (busyDialog != null) {
1197            busyDialog.finish();
1198            busyDialog = null;
1199        }
1200        openFileChooserButton.setEnabled(true);
1201        programButton.setEnabled(false);
1202        bootState = BootState.IDLE;
1203    }
1204
1205
1206    /**
1207     * Add array of bytes to checksum
1208     *
1209     * @param d the array of bytes
1210     */
1211    protected void updateChecksum(byte [] d) {
1212        for (int i = 0; i < d.length; i++) {
1213            // bytes are signed so Cast to int and take the 8 LSBs
1214            checksum += d[i] & 0xFF;
1215        }
1216    }
1217
1218
1219    /**
1220     * Request a single Parameter from a Physical Node
1221     * <p>
1222     * Will not send the request if there are existing active timers.
1223     * Starts Parameter timeout
1224     *
1225     * @param param Parameter Index Number, Index 0 is total parameters
1226     */
1227    public void requestParam(int param){
1228        if (hasActiveTimers()){
1229            return;
1230        }
1231        bootState = BootState.GET_PARAMS;
1232        setAllParamTimeout();
1233        send.rQNPN(nodeNumber, param);
1234    }
1235
1236
1237    /**
1238     * See if any timers are running, ie waiting for a response from a physical Node.
1239     *
1240     * @return true if timers are running else false
1241     */
1242    protected boolean hasActiveTimers() {
1243        return allParamTask != null
1244            || startBootTask != null
1245            || checkBootTask != null
1246            || devIdTask != null
1247            || bootIdTask != null
1248            || pauseTask != null
1249            || dataTask != null
1250            || ackTask != null
1251            || checkTask != null;
1252    }
1253
1254
1255    private TimerTask allParamTask;
1256    private TimerTask startBootTask;
1257    private TimerTask checkBootTask;
1258    private TimerTask pauseTask;
1259    private TimerTask dataTask;
1260    private TimerTask ackTask;
1261    private TimerTask devIdTask;
1262    private TimerTask bootIdTask;
1263    private TimerTask checkTask;
1264
1265    // Set when the pane is disposed so that timer tasks that fire late, or
1266    // are already running, do not act on the disposed pane
1267    private volatile boolean disposed = false;
1268
1269
1270    /**
1271     * Cancel and forget all timers
1272     */
1273    private void cancelAllTimers() {
1274        clearAllParamTimeout();
1275        clearStartBootTimeout();
1276        clearCheckBootTimeout();
1277        clearDevIdTimeout();
1278        clearBootIdTimeout();
1279        clearPauseTimeout();
1280        clearDataTimeout();
1281        clearAckTimeout();
1282        clearCheckTimeout();
1283    }
1284
1285
1286    /**
1287     * Stop timer for a single parameter fetch
1288     */
1289    private void clearAllParamTimeout() {
1290        if (allParamTask != null) {
1291            allParamTask.cancel();
1292            allParamTask = null;
1293        }
1294    }
1295
1296
1297    /**
1298     * Start timer for a Parameter request
1299     *
1300     * On timeout, attempt to find module already in boot mode.
1301     */
1302    private void setAllParamTimeout() {
1303        clearAllParamTimeout(); // resets if timer already running
1304        allParamTask = new TimerTask() {
1305            @Override
1306            public void run() {
1307                allParamTask = null;
1308                if (disposed) {
1309                    return;
1310                }
1311                if (busyDialog != null) {
1312                    busyDialog.finish();
1313                    busyDialog = null;
1314                    log.debug("Failed to read module parameters from node {}", nodeNumber);
1315                    hardwareParams.setValid(false);
1316                    moduleCheckBox.setSelected(true);
1317                    openFileChooserButton.setEnabled(true);
1318                    endProgramming(BootStatus.PARAMETER_TIMEOUT);
1319                }
1320            }
1321        };
1322        TimerUtil.schedule(allParamTask, CbusNode.SINGLE_MESSAGE_TIMEOUT_TIME);
1323    }
1324
1325
1326    /**
1327     * Stop timer for boot mode entry request
1328     */
1329    private void clearStartBootTimeout() {
1330        if (startBootTask != null) {
1331            startBootTask.cancel();
1332            startBootTask = null;
1333        }
1334    }
1335
1336
1337    /**
1338     * Start timer for boot mode entry request
1339     * <p>
1340     * We don't get a response, so timeout is expected, assume module is in boot
1341     * mode and start check for boot mode
1342     */
1343    private void setStartBootTimeout() {
1344        clearStartBootTimeout(); // resets if timer already running
1345        startBootTask = new TimerTask() {
1346            @Override
1347            public void run() {
1348                startBootTask = null;
1349                if (disposed) {
1350                    return;
1351                }
1352                setCheckBootTimeout();
1353                bootState = BootState.CHECK_BOOT_MODE;
1354                CanMessage m = CbusMessage.getBootTest(0);
1355                tc.sendCanMessage(m, null);
1356            }
1357        };
1358        TimerUtil.schedule(startBootTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1359    }
1360
1361
1362    /**
1363     * Stop timer for boot mode check
1364     */
1365    private void clearCheckBootTimeout() {
1366        if (checkBootTask != null) {
1367            checkBootTask.cancel();
1368            checkBootTask = null;
1369        }
1370    }
1371
1372
1373    /**
1374     * Start timer for boot mode check
1375     */
1376    private void setCheckBootTimeout() {
1377        clearCheckBootTimeout(); // resets if timer already running
1378        checkBootTask = new TimerTask() {
1379            @Override
1380            public void run() {
1381                checkBootTask = null;
1382                if (disposed) {
1383                    return;
1384                }
1385                log.error("Timeout checking for boot mode");
1386                endProgramming(BootStatus.BOOT_TIMEOUT);
1387            }
1388        };
1389        TimerUtil.schedule(checkBootTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1390    }
1391
1392
1393    /**
1394     * Stop timer for bootloader device ID request
1395     */
1396    private void clearDevIdTimeout() {
1397        if (devIdTask != null) {
1398            devIdTask.cancel();
1399            devIdTask = null;
1400        }
1401    }
1402
1403
1404    /**
1405     * Start timer for bootloader device ID request
1406     * <p>
1407     * If we don't get a response we start programming with the old AN247 protocol.
1408     */
1409    private void setDevIdTimeout() {
1410        clearDevIdTimeout(); // resets if timer already running
1411        devIdTask = new TimerTask() {
1412            @Override
1413            public void run() {
1414                devIdTask = null;
1415                if (disposed) {
1416                    return;
1417                }
1418                bootProtocol = BootProtocol.AN247;
1419                log.debug("Found AN247 bootloader");
1420                addToLog(Bundle.getMessage("BootIdAn247"));
1421                initialise();
1422            }
1423        };
1424        TimerUtil.schedule(devIdTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1425    }
1426
1427
1428    /**
1429     * Stop timer for bootloader ID request
1430     */
1431    private void clearBootIdTimeout() {
1432        if (bootIdTask != null) {
1433            bootIdTask.cancel();
1434            bootIdTask = null;
1435        }
1436    }
1437
1438
1439    /**
1440     * Start timer for bootloader ID request
1441     * <p>
1442     */
1443    private void setBootIdTimeout() {
1444        clearBootIdTimeout(); // resets if timer already running
1445        bootIdTask = new TimerTask() {
1446            @Override
1447            public void run() {
1448                bootIdTask = null;
1449                if (disposed) {
1450                    return;
1451                }
1452                protocolError();
1453            }
1454        };
1455        TimerUtil.schedule(bootIdTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1456    }
1457
1458
1459    /**
1460     * Stop timer for bootloader reset pause
1461     */
1462    private void clearPauseTimeout() {
1463        if (pauseTask != null) {
1464            pauseTask.cancel();
1465            pauseTask = null;
1466        }
1467    }
1468
1469
1470    /**
1471     * Start timer for bootloader reset pause
1472     * <p>
1473     * Special case for Pi-SPROG One AN247 protocol only
1474     * <p>
1475     * No reply so timeout is expected. Initialise to new address for application.
1476     * The init is now sent from writeNextData for AN247.
1477     */
1478    private void setPauseTimeout() {
1479        clearPauseTimeout(); // resets if timer already running
1480        pauseTask = new TimerTask() {
1481            @Override
1482            public void run() {
1483                pauseTask = null;
1484                if (disposed) {
1485                    return;
1486                }
1487                hexForBootloader = false;
1488                log.debug("Start writing at address {}", Integer.toHexString(bootAddress));
1489                addToLog(MessageFormat.format(Bundle.getMessage("BootStartAddress"), Integer.toHexString(bootAddress)));
1490                bootState = BootState.PROG_DATA;
1491                writeNextData();
1492            }
1493        };
1494        TimerUtil.schedule(pauseTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1495    }
1496
1497
1498    /**
1499     * Stop timer for data writes
1500     */
1501    private void clearDataTimeout() {
1502        if (dataTask != null) {
1503            dataTask.cancel();
1504            dataTask = null;
1505        }
1506    }
1507
1508
1509    /**
1510     * Start timer for data writes
1511     * 
1512     * Only used for AN247 prototocl
1513     * <p>
1514     * No reply so timeout is expected. Send more data.
1515     */
1516    private void setDataTimeout(int timeout) {
1517        clearDataTimeout(); // resets if timer already running
1518        dataTask = new TimerTask() {
1519            @Override
1520            public void run() {
1521                dataTask = null;
1522                if (disposed) {
1523                    return;
1524                }
1525                writeNextData();
1526            }
1527        };
1528        TimerUtil.schedule(dataTask, timeout);
1529    }
1530
1531
1532    /**
1533     * Stop timer for ACK timeout
1534     */
1535    private void clearAckTimeout() {
1536        if (ackTask != null) {
1537            ackTask.cancel();
1538            ackTask = null;
1539        }
1540    }
1541
1542
1543    /**
1544     * Start timer for ACK timeout
1545     * <p>
1546     * Error condition if no ACK received
1547     */
1548    private void setAckTimeout() {
1549        clearAckTimeout(); // resets if timer already running
1550        ackTask = new TimerTask() {
1551            @Override
1552            public void run() {
1553                ackTask = null;
1554                if (disposed) {
1555                    return;
1556                }
1557                endProgramming(BootStatus.ACK_TIMEOUT);
1558                bootAddress -= 8;
1559                log.error("Timeout waiting for data write ACK at address {}", Integer.toHexString(bootAddress));
1560            }
1561        };
1562        TimerUtil.schedule(ackTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1563    }
1564
1565
1566    /**
1567     * Stop timer for checksum verification
1568     */
1569    private void clearCheckTimeout() {
1570        if (checkTask != null) {
1571            checkTask.cancel();
1572            checkTask = null;
1573        }
1574    }
1575
1576
1577    /**
1578     * Start timer for checksum verification
1579     */
1580    private void setCheckTimeout() {
1581        clearCheckTimeout(); // resets if timer already running
1582        checkTask = new TimerTask() {
1583            @Override
1584            public void run() {
1585                checkTask = null;
1586                if (disposed) {
1587                    return;
1588                }
1589                endProgramming(BootStatus.CHECKSUM_TIMEOUT);
1590                log.error("Timeout verifying checksum");
1591            }
1592        };
1593        TimerUtil.schedule(checkTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1594    }
1595
1596
1597    /**
1598     * Add to boot loader Log
1599     *
1600     * @param boottext String console message
1601     */
1602    public void addToLog(String boottext){
1603        ThreadingUtil.runOnGUI( ()->{
1604            bootConsole.append("\n"+boottext);
1605        });
1606    }
1607
1608
1609    /**
1610     * disconnect from the CBUS
1611     */
1612    @Override
1613    public void dispose() {
1614        disposed = true;
1615        // stop timers if running
1616        cancelAllTimers();
1617        if (busyDialog != null) {
1618            busyDialog.finish();
1619            busyDialog = null;
1620        }
1621        if (hexFile != null) {
1622            hexFile.dispose();
1623        }
1624        bootConsole.dispose();
1625        if (tc != null) {
1626            tc.removeCanListener(this);
1627        }
1628    }
1629
1630
1631    /**
1632     * Nested class to create one of these using old-style defaults.
1633     */
1634    public static class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
1635
1636        public Default() {
1637            super(Bundle.getMessage("MenuItemBootloader"),
1638                    new jmri.util.swing.sdi.JmriJFrameInterface(),
1639                    CbusBootloaderPane.class.getName(),
1640                    jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class));
1641        }
1642    }
1643
1644
1645    private static final Logger log = LoggerFactory.getLogger(CbusBootloaderPane.class);
1646
1647}