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        slowWrite.addActionListener(writeSpeedListener);
422
423        
424
425        return menuList;
426    }
427
428
429    /**
430     * Get the delay to be inserted between bootloader data writes.
431     * 
432     * For AN247, that has no handshaking can be slow or fast and then extended 
433     * for slow writes to EEPROM and CONFIG.
434     * 
435     * Only a single long timeout is used for CBUS protocol, which has full
436     * handshaking
437     *
438     * @return Delay in ms
439     */
440    int getWriteDelay() {
441        int delay = CbusNode.BOOT_PROG_TIMEOUT_FAST;
442        
443        if (bootProtocol == BootProtocol.AN247) {
444            if (slowWrite.isSelected()) {
445                delay = CbusNode.BOOT_PROG_TIMEOUT_SLOW;
446            }
447            if (bootAddress >= CONFIG_START) {
448                delay *= 8;
449            }
450        } else {
451            delay = CbusNode.BOOT_LONG_TIMEOUT_TIME;
452        }
453        
454        return delay;
455    }
456
457
458    /**
459     * Kick off the reading of parameters from the node, starting with parameter
460     * 0, the number of parameters
461     *
462     * @param e
463     */
464    private void readNodeParamsButtonActionPerformed(java.awt.event.ActionEvent e) {
465        try {
466            nodeNumber = Integer.parseInt(nodeNumberField.getText());
467        } catch (NumberFormatException e1) {
468            addToLog(Bundle.getMessage("BootInvalidNode"));
469            log.error("Invalid node number {}", nodeNumberField.getText());
470            return;
471        }
472        // Read the parameters from the chosen node
473        addToLog(Bundle.getMessage("BootReadingParams"));
474        hardwareParams = new CbusParameters();
475        nextParam = 0;
476        busyDialog = new BusyDialog(topFrame, Bundle.getMessage("BootReadingParams"), false);
477        busyDialog.start();
478        requestParam(nextParam);
479    }
480
481
482    /**
483     * Let the user choose the hex file and check that it is suitable for the
484     * selected node.
485     *
486     * @param e
487     */
488    private void openFileChooserButtonActionPerformed(java.awt.event.ActionEvent e) {
489        // start at current file, show dialog
490        int retVal = hexFileChooser.showOpenDialog(this);
491        // handle selection or cancel
492        if (retVal == JFileChooser.APPROVE_OPTION) {
493            hexFile = new CbusPicHexFile(hexFileChooser.getSelectedFile().getPath());
494            log.debug("hex file chosen: {}", hexFile.getName());
495            addToLog(MessageFormat.format(Bundle.getMessage("BootFileChosen"), hexFile.getName()));
496            try {
497                hexFile.openRd();
498                hexFile.read();
499            } catch (IOException ex) {
500                log.error("Error opening hex file");
501                addToLog(Bundle.getMessage("BootHexFileOpenFailed"));
502                return;
503            }
504            
505            fileParams = hexFile.getParams();
506            if (!moduleCheckBox.isSelected()) {
507                if (fileParams.validate(fileParams, hardwareParams)) {
508                    addToLog(MessageFormat.format(Bundle.getMessage("BootHexFileFoundParameters"), fileParams.toString()));
509                    addToLog(Bundle.getMessage("BootHexFileParametersMatch"));
510                    programButton.setEnabled(true);
511                } else {
512                    addToLog(Bundle.getMessage("BootHexFileParametersMismatch"));
513                }
514            } else {
515                addToLog(Bundle.getMessage("BootHexFileIgnoringParameters"));
516                programButton.setEnabled(true);
517            }
518            if ((hardwareParams.areValid()) && (hardwareParams.getLoadAddress() == 0)) {
519                // Special case of rewriting the bootloader for Pi-SPROG One
520                addToLog(Bundle.getMessage("BootBoot"));
521                hexForBootloader = true;
522                programButton.setEnabled(true);
523            }
524        }
525    }
526
527
528    /**
529     * Send BOOTM OPC to put module in boot mode
530     *
531     * @param e
532     */
533    private void programButtonActionPerformed(java.awt.event.ActionEvent e) {
534        if (hasActiveTimers()){
535            return;
536        }
537        openFileChooserButton.setEnabled(false);
538        programButton.setEnabled(false);
539        busyDialog = new BusyDialog(topFrame, Bundle.getMessage("BootLoading"), false);
540        busyDialog.start();
541        setStartBootTimeout();
542        bootState = BootState.START_BOOT;
543        CanMessage m = CbusMessage.getBootEntry(nodeNumber, 0);
544        tc.sendCanMessage(m, null);
545    }
546
547
548    /**
549     * Process some outgoing CAN frames
550     * <p>
551     * The CBUS bootloader was originally "fire and forget", with no positive
552     * acknowledgement. We had to wait an indeterminate time and assume the
553     * write was successful.
554     * <p>
555     * A PIC based node will halt execution for some time ((10+ ms with newer Q
556     * series devices) whilst FLASH operations (erase and/or write) complete,
557     * during which time I/O will not be serviced. This is probably OK with CAN 
558     * transport, assuming the ECAN continues to accept frames. With serial
559     * (UART) transport, as used by Pi-SPROG, the timing is much more critical 
560     * as a single missed character will corrupt the node firmware.
561     * <p>
562     * Furthermore, on some platforms, e.g., Raspberry Pi, there can be
563     * considerable delays between the call to the traffic controller
564     * sendMessage() method and the message being sent by the transmit thread.
565     * This may be due to Flash file system operations and could be affected by
566     * the speed of the SD card. Once the message leaves the transmit thread, we
567     * are at the mercy of the underlying OS, where there can be further delays.
568     * <p>
569     * We could set an overlong timeout, but that would slow down the bootloading
570     * process in all cases.
571     * <p>
572     * To improve things somewhat we wait until the message has definitely
573     * reached the TC transmit thread, by looking for bootloader data write
574     * messages here. Testing indicates this is a marked improvement with no
575     * failures observed.
576     *
577     * This is unnecessary, and not used, for the new protocol which has a
578     * positive acknowledge mechanism.
579     * 
580     * @param m CanMessage
581     */
582    @Override
583    public void message(CanMessage m) {
584        if (bootProtocol == BootProtocol.AN247) {
585            if ((bootState == BootState.PROG_DATA)) {
586                if (m.isExtended() ) {
587                    if (CbusMessage.isBootWriteData(m)) {
588                        log.debug("Boot data write message {}", m);
589                        setDataTimeout(dataTimeout);
590                    }
591                }
592            }
593        }
594    }
595
596
597    /**
598     * Processes incoming CAN replies
599     * <p>
600     * The bootloader is only interested in standard parameter responses and
601     * extended bootloader responses.
602     *
603     * {@inheritDoc}
604     */
605    @Override
606    public void reply(CanReply r) {
607
608        if ( r.isRtr() ) {
609            return;
610        }
611
612        if (!r.isExtended() ) {
613            log.debug("Standard Reply {}", r);
614
615            handleStandardReply(r);
616        } else {
617//            log.debug("Extended Reply {} in state {}", r, bootState);
618            // Extended messages are only used by the bootloader
619
620            handleExtendedReply(r);
621        }
622    }
623
624
625    /**
626     * Handle standard ID CAN replies
627     *
628     * @param r Can reply
629     */
630    private void handleStandardReply(CanReply r) {
631        int opc = CbusMessage.getOpcode(r);
632        if (bootState != BootState.GET_PARAMS) {
633            log.debug("Reply not for me");
634            return;
635        }
636
637        if ( opc == CbusConstants.CBUS_PARAN) { // response from node
638            clearAllParamTimeout();
639
640            hardwareParams.setParam(r.getElement(3), r.getElement(4));
641            if (++nextParam < (hardwareParams.getParam(0) + 1)) {
642                // Read next
643                requestParam(nextParam);
644            } else {
645                // Done reading
646                hardwareParams.setValid(true);
647                addToLog(MessageFormat.format(Bundle.getMessage("BootNodeParametersFinished"), hardwareParams.toString()));
648                busyDialog.finish();
649                busyDialog = null;
650                openFileChooserButton.setEnabled(true);
651                bootState = BootState.IDLE;
652            }
653        } else {
654            // ignoring OPC
655        }
656    }
657
658
659    /**
660     * Handle extended ID CAN replies
661     * <p>
662     * Handle the reply in the bootloader state machine.
663     *
664     * @param r Can reply
665     */
666    private void handleExtendedReply(CanReply r) {
667        switch (bootState) {
668            default:
669                break;
670
671            case CHECK_BOOT_MODE:
672                clearCheckBootTimeout();
673                if (CbusMessage.isBootConfirm(r)) {
674                    // The node is in boot mode so we can look for the device ID
675                    requestDevId();
676                }
677                break;
678
679            case WAIT_BOOT_DEVID:
680                clearDevIdTimeout();
681                if (CbusMessage.isBootDevId(r)) {
682                    // We had a response to the Device ID request so we can proceed with the new protocol
683                    showDevId(r);
684                    bootProtocol = BootProtocol.CBUS_2_0;
685                    requestBootId();
686                } else {
687                    protocolError();
688                }
689                break;
690                
691            case WAIT_BOOT_ID:
692                clearBootIdTimeout();
693                if (CbusMessage.isBootId(r)) {
694                    // We had a response to the bootloader ID request so send the write enables
695                    showBootId(r);
696                    sendBootEnables();
697                } else {
698                    protocolError();
699                }
700                break;
701                
702            case ENABLES_SENT:
703                clearAckTimeout();
704                if (CbusMessage.isBootOK(r)) {
705                    // We had a response to the enables so start programming.
706                    initialise();
707                } else {
708                    protocolError();
709                }
710                break;
711                        
712            case INIT_SENT:
713                clearAckTimeout();
714                if (CbusMessage.isBootOK(r)) {
715                    // We had a response to the initislise so start programming.
716                    writeNextData();
717                } else if (CbusMessage.isBootOutOfRange(r)) {
718                    log.error("INIT Address out of range");
719                    endProgramming(BootStatus.INIT_OUT_OF_RANGE);
720                } else {
721                    protocolError();
722                }
723                break;
724                        
725            case PROG_DATA:
726                clearAckTimeout();
727                if (CbusMessage.isBootDataOK(r)) {
728                    // Acknowledge received for CBUS protocol
729                    writeNextData();
730                } else if (CbusMessage.isBootError(r)){
731                    log.error("Data Error");
732                    endProgramming(BootStatus.DATA_ERROR);
733                } else if (CbusMessage.isBootDataOutOfRange(r)) {
734                    log.error("Data Address out of range");
735                    endProgramming(BootStatus.DATA_OUT_OF_RANGE);
736                } else {
737                    protocolError();
738                }
739                break;
740                
741            case NOP_SENT:
742                clearAckTimeout();
743                if (CbusMessage.isBootOK(r)) {
744                    // Acknowledge received for NOP
745                    bootState = BootState.PROG_DATA;
746                    writeNextData();
747                } else if (CbusMessage.isBootOutOfRange(r)) {
748                    log.error("NOP Address out of range");
749                    endProgramming(BootStatus.ADDRESS_OUT_OF_RANGE);
750                } else {
751                    protocolError();
752                }
753                break;
754                
755            case CHECK_SENT:
756                clearCheckTimeout();
757                if (CbusMessage.isBootOK(r)) {
758                    sendReset();
759                } else if (CbusMessage.isBootError(r)) {
760                    // Checksum verify failed
761                    log.error("Node {} checksum failed", nodeNumber);
762                    endProgramming(BootStatus.CHECKSUM_FAILED);
763                } else {
764                    protocolError();
765                }
766                break;
767        }
768    }
769
770    
771    /**
772     * Show the device ID
773     * 
774     * Manufacturere and device from cbusdefs.h, device ID from the device
775     * 
776     * @param r device ID reply
777     */
778    void showDevId(CanReply r) {
779        log.debug("Found device ID Manu: {} Dev: {} Device ID: {}",
780                r.getElement(1),
781                r.getElement(2),
782                (r.getElement(3)<<24) + (r.getElement(4)<<16) + (r.getElement(5)<<8) + r.getElement(4));
783        addToLog(MessageFormat.format(Bundle.getMessage("DevIdCbus"),
784                r.getElement(1),
785                r.getElement(2),
786                (r.getElement(3)<<24) + (r.getElement(4)<<16) + (r.getElement(5)<<8) + r.getElement(4)));
787    }
788    
789   
790    /**
791     * Show the bootloader ID
792     * 
793     * Major/Minor version number, checksum algorithm error report capability
794     * 
795     * @param r Bootloader ID reply
796     */
797    void showBootId(CanReply r) {
798        log.debug("Found bootloader Major: {} Minor: {} Algo: {} Reports: {}",
799                r.getElement(1),
800                r.getElement(2),
801                r.getElement(3),
802                r.getElement(4));
803        addToLog(MessageFormat.format(Bundle.getMessage("BootIdCbus"),
804                r.getElement(1),
805                r.getElement(2),
806                r.getElement(3),
807                r.getElement(4)));
808    }
809    
810    
811    /**
812     * Send the memory region write enable bit mask for CBUS bootloader protocol
813     */
814    void sendBootEnables() {
815        int enables = 1;    // Prog mem always enabled
816        
817        if (eepromCheckBox.isSelected()) {
818            enables |= 2;
819        }
820        if (configCheckBox.isSelected()) {
821            enables |= 4;
822        }
823        
824        bootState = BootState.ENABLES_SENT;
825        setAckTimeout();
826        CanMessage m = CbusMessage.getBootEnables(enables, 0);
827        log.debug("Send boot enables {}", enables);
828        addToLog(MessageFormat.format(Bundle.getMessage("BootEnables"), enables));
829        tc.sendCanMessage(m, null);
830    }
831    
832    
833    /**
834     * Protocol Error
835     */
836    void protocolError() {
837        log.error("Bootloader Protocol Error in state {}", bootState.toString());
838        addToLog(MessageFormat.format(Bundle.getMessage("BootProtocol"), bootState.toString()));
839        endProgramming(BootStatus.PROTOCOL_ERROR);
840    }
841    
842    
843    /**
844     * Is Programming Needed
845     * 
846     * Check if any data bytes actually need programming
847     * 
848     * @param d data bytes to check
849     * @return false if all bytes are 0xFF, else true
850     */
851    boolean isProgrammingNeeded(byte [] d) {
852        for (int i = 0; i < d.length; i++) {
853            if (d[i] != (byte)0xFF) {
854                return true;
855            }
856        }
857        return false;
858    }
859
860    
861    protected void logFrame(CanMessage m) {
862        log.debug("Write frame {} at address {} {}", dataFramesSent, Integer.toHexString(bootAddress), m);
863        if ((bootAddress & 0xFF) == 0) {
864            addToLog(MessageFormat.format(Bundle.getMessage("BootAddress"), Integer.toHexString(bootAddress)));
865        } else {
866            bootConsole.append(".");
867        }        
868    }
869    
870    
871    /**
872     * Check if data is filtered (e.g., EEPROM selection unticked)
873     * 
874     * Used only for AN247
875     * 
876     * @param address of data record
877     * @return true if data is filtered and should not be written
878     */
879    protected boolean dataIsFiltered(int address) {
880        if ((address >= 0x300000) && (address < 0x310000) && (!configCheckBox.isSelected())) {
881            // PIC18 Config space at 0x200000 is filtered
882            return true;
883        } else if ((address >= 0x310000) && (!eepromCheckBox.isSelected())) {
884            // PIC18 EEPROM space at 0x300000, 0x310000 or 0x380000 is filtered
885            return true;
886        }
887        return false;
888    }
889    
890    
891    /**
892     * Send data to the hardware and keep a running checksum
893     *
894     * @param timeout timeout for write operation
895     */
896    protected void sendData(int timeout) {
897
898        byte [] d = getDataFromRecord();
899        dataFramesSent++;
900        
901        CanMessage m = CbusMessage.getBootWriteData(d, 0);
902        if (bootProtocol == BootProtocol.CBUS_2_0) {
903            setAckTimeout();
904            updateChecksum(d);
905            logFrame(m);       
906            tc.sendCanMessage(m, null);
907        } else {
908            // For AN247 protocol, we need to filter data
909            if (!dataIsFiltered(bootAddress)) {
910                // Timeout will be set when we see the outgoing message
911                dataTimeout = timeout;
912                updateChecksum(d);
913                logFrame(m);       
914                tc.sendCanMessage(m, null);
915            } else {
916                // No data to send, set short timeout to trigger next data
917                setDataTimeout(10);
918            }
919        }
920        bootAddress += d.length;
921    }
922
923    
924    /**
925     * Extract data from the current hex record
926     * 
927     * Returns 8 byte array or whatever is left in the record if less than 8 bytes.
928     * 
929     * Sets recordDone flag if record is exhausted.
930     * 
931     * @return data array
932     */
933    private byte [] getDataFromRecord() {
934        byte [] d;
935        
936        if (currentRecord.len - recordIndex >= 8) {
937            d = new byte[8];
938            if (currentRecord.len - recordIndex == 8) {
939                recordDone = true;
940            }
941        } else {
942            d = new byte[currentRecord.len - recordIndex];
943            recordDone = true;
944        }
945        for (int i = 0; i < d.length; i++) {
946            d[i] = currentRecord.getData(recordIndex++);
947        }
948        return d;
949    }
950
951    
952    /**
953     * Write next data for AN247 protocol
954     */
955    void writeNextDataAn247() {
956//        log.debug("writeNextDataAn247()");
957        
958        if ((bootAddress == 0x7f8) && (hexForBootloader == true)) {
959            log.debug("Pause for bootloader reset");
960            // Special case for Pi-SPROG One, pause at end of bootloader code to allow time for node to reset
961            bootAddress = 0x800;
962            checksum = 0;
963            bootState = BootState.PROG_PAUSE;
964            setPauseTimeout();
965        } else {
966            // If the address has skipped we need to send a new address to the bootloader
967            // There's no ACK so send data immediately afterwards
968            if ((currentRecord.address + recordIndex) != bootAddress) {
969                bootAddress = currentRecord.address;
970                // Send NOP to adjust the address, no reply to this from AN247
971                log.debug("Start writing at new address {}", Integer.toHexString(bootAddress));
972                addToLog(MessageFormat.format(Bundle.getMessage("BootNewAddress"), Integer.toHexString(bootAddress)));
973                CanMessage m = CbusMessage.getBootNop(bootAddress, 0);
974                tc.sendCanMessage(m, null);
975            }
976            if ((bootAddress < CONFIG_START) && (currentRecord.len%8 != 0)) {
977                // AN247 bootloader always writes 8 bytes to FLASH so we need to pad the packet and adjust the length
978                int pad = 8 - currentRecord.len%8;
979                for (int i = 0; i < pad; i++) {
980                    currentRecord.data[currentRecord.len + pad] = (byte)0xFF;
981                }
982                currentRecord.len += pad;
983            }
984            sendData(getWriteDelay());
985        }
986    }
987
988
989    /**
990     * Write next data for CBUS protocol
991     */
992    void writeNextDataCbus() {
993//        log.debug("writeNextDataCbus()");
994
995        // If the address has skipped we need to send a new address to the bootloader
996        if ((currentRecord.address + recordIndex) != bootAddress) {
997            bootAddress = currentRecord.address;
998            // Send NOP to adjust the address 
999            log.debug("Start writing at new address {}", Integer.toHexString(bootAddress));
1000            addToLog(MessageFormat.format(Bundle.getMessage("BootNewAddress"), Integer.toHexString(bootAddress)));
1001            bootState = BootState.NOP_SENT;
1002            setAckTimeout();
1003            CanMessage m = CbusMessage.getBootNop(bootAddress, 0);
1004            tc.sendCanMessage(m, null);
1005        } else {
1006            // Extract the data, send it and update bootAddress for next packet
1007            sendData(getWriteDelay());
1008        }
1009    }
1010
1011
1012    /**
1013     * Write the next data frame for the bootloader
1014     */
1015    void writeNextData() {
1016        if (recordDone) {
1017            // Current record is exhausted, Get next ONE
1018            recordDone = false;
1019            recordIndex = 0;
1020            currentRecord = hexFile.getNextRecord();
1021            if (currentRecord.type == HexRecord.END) {
1022                // No more data to send so send checksum
1023                bootState = BootState.CHECK_SENT;
1024                addToLog(Bundle.getMessage("BootVerifyChecksum"));
1025                log.debug("Sending checksum {} as 2s complement {}", checksum, 0 - checksum);
1026                setCheckTimeout();
1027                CanMessage m = CbusMessage.getBootCheck(0 - checksum, 0);
1028                tc.sendCanMessage(m, null);
1029                return;
1030            }
1031        }
1032        
1033        bootState = BootState.PROG_DATA;
1034        if (bootProtocol == BootProtocol.AN247) {
1035            writeNextDataAn247();
1036        } else {
1037            writeNextDataCbus();
1038        }
1039    }
1040
1041
1042    /**
1043     * Initialise programming
1044     * 
1045     * We normally start at the address from the module parameters, or from the 
1046     * hex file, otherwise start at the beginning of the hex file. 
1047     */
1048    private void initialise() {
1049        Optional<HexRecord> hexRecord;
1050        
1051        if (hardwareParams.areValid()) {
1052            bootAddress = hardwareParams.getLoadAddress();
1053        } else if (fileParams.areValid()) {
1054            bootAddress = fileParams.getLoadAddress();
1055        } else {
1056            bootAddress = hexFile.getProgStart();
1057        }
1058        
1059        recordDone = false;
1060        recordIndex = 0;
1061        
1062        hexRecord = hexFile.getRecordForAddress(bootAddress);
1063        if (hexRecord.isPresent()) {
1064            currentRecord = hexRecord.get();
1065        } else {
1066            log.error("Did not find hex record for load address {}", "0x"+Integer.toHexString(bootAddress));
1067            endProgramming(BootStatus.ADDRESS_NOT_FOUND);
1068        }
1069        checksum = 0;
1070        dataFramesSent = 0;
1071        log.debug("Initialise at address {}", "0x"+Integer.toHexString(bootAddress));
1072        addToLog(MessageFormat.format(Bundle.getMessage("BootStartAddress"), Integer.toHexString(bootAddress)));
1073        // Initialise the bootloader, only CBUS protocol will ACK this
1074        if (bootProtocol == BootProtocol.CBUS_2_0) {
1075            setAckTimeout();
1076        }
1077        CanMessage m = CbusMessage.getBootInitialise(bootAddress, 0);
1078        bootState = BootState.INIT_SENT;
1079        tc.sendCanMessage(m, null);
1080        if (bootProtocol == BootProtocol.AN247) {
1081            // No wait for ACK so start sending data
1082            writeNextData();
1083        }
1084    }
1085
1086    
1087    protected void requestDevId() {
1088        CanMessage m = CbusMessage.getBootDevId(0);
1089        log.debug("Requesting bootloader device ID...");
1090        addToLog(Bundle.getMessage("ReqDevId"));
1091        bootState = BootState.WAIT_BOOT_DEVID;
1092        setDevIdTimeout();
1093        tc.sendCanMessage(m, null);
1094    }
1095
1096    
1097    protected void requestBootId() {
1098        CanMessage m = CbusMessage.getBootId(0);
1099        log.debug("Requesting bootloader ID...");
1100        addToLog(Bundle.getMessage("ReqBootId"));
1101        bootState = BootState.WAIT_BOOT_ID;
1102        setBootIdTimeout();
1103        tc.sendCanMessage(m, null);
1104    }
1105
1106
1107    /**
1108     * Send bootloader reset frame to put the node back into operating mode.
1109     *
1110     * There will be no reply to this.
1111     */
1112    protected void sendReset() {
1113        CanMessage m = CbusMessage.getBootReset(0);
1114        log.debug("Done. Resetting node...");
1115        addToLog(Bundle.getMessage("BootFinished"));
1116        tc.sendCanMessage(m, null);
1117        endProgramming(BootStatus.COMPLETE);
1118    }
1119
1120
1121    /**
1122     * Tidy up after programming success or failure
1123     */
1124    private void endProgramming(BootStatus status) {
1125        log.debug("Boot status is {}", status.toString());
1126        addToLog(MessageFormat.format(Bundle.getMessage("BootStatus"), status.toString()));
1127        if (busyDialog != null) {
1128            busyDialog.finish();
1129            busyDialog = null;
1130        }
1131        openFileChooserButton.setEnabled(true);
1132        programButton.setEnabled(false);
1133        bootState = BootState.IDLE;
1134    }
1135
1136
1137    /**
1138     * Add array of bytes to checksum
1139     *
1140     * @param d the array of bytes
1141     */
1142    protected void updateChecksum(byte [] d) {
1143        for (int i = 0; i < d.length; i++) {
1144            // bytes are signed so Cast to int and take the 8 LSBs
1145            checksum += d[i] & 0xFF;
1146        }
1147    }
1148
1149
1150    /**
1151     * Request a single Parameter from a Physical Node
1152     * <p>
1153     * Will not send the request if there are existing active timers.
1154     * Starts Parameter timeout
1155     *
1156     * @param param Parameter Index Number, Index 0 is total parameters
1157     */
1158    public void requestParam(int param){
1159        if (hasActiveTimers()){
1160            return;
1161        }
1162        bootState = BootState.GET_PARAMS;
1163        setAllParamTimeout();
1164        send.rQNPN(nodeNumber, param);
1165    }
1166
1167
1168    /**
1169     * See if any timers are running, ie waiting for a response from a physical Node.
1170     *
1171     * @return true if timers are running else false
1172     */
1173    protected boolean hasActiveTimers() {
1174        return allParamTask != null
1175            || startBootTask != null
1176            || checkBootTask != null
1177            || devIdTask != null
1178            || bootIdTask != null
1179            || pauseTask != null
1180            || dataTask != null
1181            || ackTask != null
1182            || checkTask != null;
1183    }
1184
1185
1186    private TimerTask allParamTask;
1187    private TimerTask startBootTask;
1188    private TimerTask checkBootTask;
1189    private TimerTask pauseTask;
1190    private TimerTask dataTask;
1191    private TimerTask ackTask;
1192    private TimerTask devIdTask;
1193    private TimerTask bootIdTask;
1194    private TimerTask checkTask;
1195
1196
1197    /**
1198     * Stop timer for a single parameter fetch
1199     */
1200    private void clearAllParamTimeout() {
1201        if (allParamTask != null) {
1202            allParamTask.cancel();
1203            allParamTask = null;
1204        }
1205    }
1206
1207
1208    /**
1209     * Start timer for a Parameter request
1210     * 
1211     * On timeout, attempt to find module already in boot mode.
1212     */
1213    private void setAllParamTimeout() {
1214        clearAllParamTimeout(); // resets if timer already running
1215        allParamTask = new TimerTask() {
1216            @Override
1217            public void run() {
1218                allParamTask = null;
1219                if (busyDialog != null) {
1220                    busyDialog.finish();
1221                    busyDialog = null;
1222                    log.debug("Failed to read module parameters from node {}", nodeNumber);
1223                    hardwareParams.setValid(false);
1224                    moduleCheckBox.setSelected(true);
1225                    openFileChooserButton.setEnabled(true);
1226                    endProgramming(BootStatus.PARAMETER_TIMEOUT);
1227                }
1228            }
1229        };
1230        TimerUtil.schedule(allParamTask, CbusNode.SINGLE_MESSAGE_TIMEOUT_TIME);
1231    }
1232
1233
1234    /**
1235     * Stop timer for boot mode entry request
1236     */
1237    private void clearStartBootTimeout() {
1238        if (startBootTask != null) {
1239            startBootTask.cancel();
1240            startBootTask = null;
1241        }
1242    }
1243
1244
1245    /**
1246     * Start timer for boot mode entry request
1247     * <p>
1248     * We don't get a response, so timeout is expected, assume module is in boot
1249     * mode and start check for boot mode
1250     */
1251    private void setStartBootTimeout() {
1252        clearStartBootTimeout(); // resets if timer already running
1253        startBootTask = new TimerTask() {
1254            @Override
1255            public void run() {
1256                startBootTask = null;
1257                setCheckBootTimeout();
1258                bootState = BootState.CHECK_BOOT_MODE;
1259                CanMessage m = CbusMessage.getBootTest(0);
1260                tc.sendCanMessage(m, null);
1261            }
1262        };
1263        TimerUtil.schedule(startBootTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1264    }
1265
1266
1267    /**
1268     * Stop timer for boot mode check
1269     */
1270    private void clearCheckBootTimeout() {
1271        if (checkBootTask != null) {
1272            checkBootTask.cancel();
1273            checkBootTask = null;
1274        }
1275    }
1276
1277
1278    /**
1279     * Start timer for boot mode check
1280     */
1281    private void setCheckBootTimeout() {
1282        clearCheckBootTimeout(); // resets if timer already running
1283        checkBootTask = new TimerTask() {
1284            @Override
1285            public void run() {
1286                checkBootTask = null;
1287                log.error("Timeout checking for boot mode");
1288                endProgramming(BootStatus.BOOT_TIMEOUT);
1289            }
1290        };
1291        TimerUtil.schedule(checkBootTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1292    }
1293
1294
1295    /**
1296     * Stop timer for bootloader device ID request
1297     */
1298    private void clearDevIdTimeout() {
1299        if (devIdTask != null) {
1300            devIdTask.cancel();
1301            devIdTask = null;
1302        }
1303    }
1304
1305
1306    /**
1307     * Start timer for bootloader device ID request
1308     * <p>
1309     * If we don't get a response we start programming with the old AN247 protocol.
1310     */
1311    private void setDevIdTimeout() {
1312        clearDevIdTimeout(); // resets if timer already running
1313        devIdTask = new TimerTask() {
1314            @Override
1315            public void run() {
1316                devIdTask = null;
1317                bootProtocol = BootProtocol.AN247;
1318                log.debug("Found AN247 bootloader");
1319                addToLog(Bundle.getMessage("BootIdAn247"));
1320                initialise();
1321            }
1322        };
1323        TimerUtil.schedule(devIdTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1324    }
1325
1326
1327    /**
1328     * Stop timer for bootloader ID request
1329     */
1330    private void clearBootIdTimeout() {
1331        if (bootIdTask != null) {
1332            bootIdTask.cancel();
1333            bootIdTask = null;
1334        }
1335    }
1336
1337
1338    /**
1339     * Start timer for bootloader ID request
1340     * <p>
1341     */
1342    private void setBootIdTimeout() {
1343        clearBootIdTimeout(); // resets if timer already running
1344        bootIdTask = new TimerTask() {
1345            @Override
1346            public void run() {
1347                bootIdTask = null;
1348                protocolError();
1349            }
1350        };
1351        TimerUtil.schedule(bootIdTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1352    }
1353
1354
1355    /**
1356     * Stop timer for bootloader reset pause
1357     */
1358    private void clearPauseTimeout() {
1359        if (pauseTask != null) {
1360            pauseTask.cancel();
1361            pauseTask = null;
1362        }
1363    }
1364
1365
1366    /**
1367     * Start timer for bootloader reset pause
1368     * <p>
1369     * Special case for Pi-SPROG One AN247 protocol only
1370     * <p>
1371     * No reply so timeout is expected. Initialise to new address for application.
1372     * The init is now sent from writeNextData for AN247.
1373     */
1374    private void setPauseTimeout() {
1375        clearPauseTimeout(); // resets if timer already running
1376        pauseTask = new TimerTask() {
1377            @Override
1378            public void run() {
1379                pauseTask = null;
1380                hexForBootloader = false;
1381                log.debug("Start writing at address {}", Integer.toHexString(bootAddress));
1382                addToLog(MessageFormat.format(Bundle.getMessage("BootStartAddress"), Integer.toHexString(bootAddress)));
1383                bootState = BootState.PROG_DATA;
1384                writeNextData();
1385            }
1386        };
1387        TimerUtil.schedule(pauseTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1388    }
1389
1390
1391    /**
1392     * Stop timer for data writes
1393     */
1394    private void clearDataTimeout() {
1395        if (dataTask != null) {
1396            dataTask.cancel();
1397            dataTask = null;
1398        }
1399    }
1400
1401
1402    /**
1403     * Start timer for data writes
1404     * 
1405     * Only used for AN247 prototocl
1406     * <p>
1407     * No reply so timeout is expected. Send more data.
1408     */
1409    private void setDataTimeout(int timeout) {
1410        clearDataTimeout(); // resets if timer already running
1411        dataTask = new TimerTask() {
1412            @Override
1413            public void run() {
1414                dataTask = null;
1415                writeNextData();
1416            }
1417        };
1418        TimerUtil.schedule(dataTask, timeout);
1419    }
1420
1421
1422    /**
1423     * Stop timer for ACK timeout
1424     */
1425    private void clearAckTimeout() {
1426        if (ackTask != null) {
1427            ackTask.cancel();
1428            ackTask = null;
1429        }
1430    }
1431
1432
1433    /**
1434     * Start timer for ACK timeout
1435     * <p>
1436     * Error condition if no ACK received
1437     */
1438    private void setAckTimeout() {
1439        clearAckTimeout(); // resets if timer already running
1440        ackTask = new TimerTask() {
1441            @Override
1442            public void run() {
1443                ackTask = null;
1444                endProgramming(BootStatus.ACK_TIMEOUT);
1445                bootAddress -= 8;
1446                log.error("Timeout waiting for data write ACK at address {}", Integer.toHexString(bootAddress));
1447            }
1448        };
1449        TimerUtil.schedule(ackTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1450    }
1451
1452
1453    /**
1454     * Stop timer for checksum verification
1455     */
1456    private void clearCheckTimeout() {
1457        if (checkTask != null) {
1458            checkTask.cancel();
1459            checkTask = null;
1460        }
1461    }
1462
1463
1464    /**
1465     * Start timer for checksum verification
1466     */
1467    private void setCheckTimeout() {
1468        clearCheckTimeout(); // resets if timer already running
1469        checkTask = new TimerTask() {
1470            @Override
1471            public void run() {
1472                checkTask = null;
1473                endProgramming(BootStatus.CHECKSUM_TIMEOUT);
1474                log.error("Timeout verifying checksum");
1475            }
1476        };
1477        TimerUtil.schedule(checkTask, CbusNode.BOOT_LONG_TIMEOUT_TIME);
1478    }
1479
1480
1481    /**
1482     * Add to boot loader Log
1483     *
1484     * @param boottext String console message
1485     */
1486    public void addToLog(String boottext){
1487        ThreadingUtil.runOnGUI( ()->{
1488            bootConsole.append("\n"+boottext);
1489        });
1490    }
1491
1492
1493    /**
1494     * disconnect from the CBUS
1495     */
1496    @Override
1497    public void dispose() {
1498        if (hexFile != null) {
1499            hexFile.dispose();
1500        }
1501        // stop timers if running
1502
1503        bootConsole.dispose();
1504        tc.removeCanListener(this);
1505    }
1506
1507
1508    /**
1509     * Nested class to create one of these using old-style defaults.
1510     */
1511    static public class Default extends jmri.jmrix.can.swing.CanNamedPaneAction {
1512
1513        public Default() {
1514            super(Bundle.getMessage("MenuItemBootloader"),
1515                    new jmri.util.swing.sdi.JmriJFrameInterface(),
1516                    CbusBootloaderPane.class.getName(),
1517                    jmri.InstanceManager.getDefault(CanSystemConnectionMemo.class));
1518        }
1519    }
1520
1521
1522    private final static Logger log = LoggerFactory.getLogger(CbusBootloaderPane.class);
1523
1524}