001package jmri.jmrix.loconet.swing.lncvprog;
002
003import javax.swing.*;
004import javax.swing.border.Border;
005import javax.swing.table.TableRowSorter;
006import java.awt.*;
007import java.awt.event.*;
008import java.util.Objects;
009
010import jmri.InstanceManager;
011import jmri.UserPreferencesManager;
012import jmri.jmrix.loconet.*;
013import jmri.jmrix.loconet.uhlenbrock.LncvDevice;
014import jmri.jmrix.loconet.uhlenbrock.LncvMessageContents;
015import jmri.swing.JTablePersistenceManager;
016import jmri.util.JmriJFrame;
017import jmri.util.swing.JmriJOptionPane;
018import jmri.util.table.ButtonEditor;
019import jmri.util.table.ButtonRenderer;
020
021/**
022 * Frame for discovery and display of LocoNet LNCV boards.
023 * Derived from xbee node config. Verified with Digikeijs DR5033 hardware.
024 * <p>
025 * Some of the message formats used in this class are Copyright Uhlenbrock.de
026 * and used with permission as part of the JMRI project. That permission does
027 * not extend to uses in other software products. If you wish to use this code,
028 * algorithm or these message formats outside of JMRI, please contact Uhlenbrock.
029 * <p>
030 * Buttons in table row allows to add roster entry for device, and switch to the
031 * DecoderPro ops mode programmer.
032 *
033 * @author Egbert Broerse Copyright (C) 2021, 2022
034 */
035public class LncvProgPane extends jmri.jmrix.loconet.swing.LnPanel implements LocoNetListener {
036
037    private LocoNetSystemConnectionMemo memo;
038    protected JToggleButton allProgButton = new JToggleButton();
039    protected JToggleButton modProgButton = new JToggleButton();
040    protected JButton readButton = new JButton(Bundle.getMessage("ButtonRead"));
041    protected JButton writeButton = new JButton(Bundle.getMessage("ButtonWrite"));
042    protected JTextField articleField = new JTextField(4);
043    protected JTextField addressField = new JTextField(4);
044    protected JTextField cvField = new JTextField(4);
045    protected JTextField valueField = new JTextField(4);
046    protected JCheckBox directCheckBox = new JCheckBox(Bundle.getMessage("DirectModeBox"));
047    protected JCheckBox rawCheckBox = new JCheckBox(Bundle.getMessage("ButtonShowRaw"));
048    protected JTable moduleTable = null;
049    protected LncvProgTableModel moduleTableModel = null;
050    public static final int ROW_HEIGHT = (new JButton("X").getPreferredSize().height)*9/10;
051
052    protected JPanel tablePanel = null;
053    protected JLabel statusText1 = new JLabel();
054    protected JLabel statusText2 = new JLabel();
055    protected JLabel articleFieldLabel = new JLabel(Bundle.getMessage("LabelArticleNum", JLabel.RIGHT));
056    protected JLabel addressFieldLabel = new JLabel(Bundle.getMessage("LabelModuleAddress", JLabel.RIGHT));
057    protected JLabel cvFieldLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("HeadingCv")), JLabel.RIGHT);
058    protected JLabel valueFieldLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("HeadingValue")), JLabel.RIGHT);
059    protected JTextArea result = new JTextArea(6,50);
060    protected String reply = "";
061    protected int art;
062    protected int adr = 1;
063    protected int cv = 0;
064    protected int val;
065    boolean writeConfirmed = false;
066    private final String rawDataCheck = this.getClass().getName() + ".RawData"; // NOI18N
067    private final String dontWarnOnClose = this.getClass().getName() + ".DontWarnOnClose"; // NOI18N
068    private UserPreferencesManager pm;
069    private transient TableRowSorter<LncvProgTableModel> sorter;
070    private LncvDevicesManager lncvdm;
071
072    private boolean allProgRunning = false;
073    private int moduleProgRunning = -1; // stores module address as int during moduleProgramming session, -1 = no session
074
075    /**
076     * Constructor method
077     */
078    public LncvProgPane() {
079        super();
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    @Override
086    public String getHelpTarget() {
087        return "package.jmri.jmrix.loconet.swing.lncvprog.LncvProgPane"; // NOI18N
088    }
089
090    @Override
091    public String getTitle() {
092        return Bundle.getMessage("MenuItemLncvProg");
093    }
094
095    /**
096     * Initialize the config window
097     */
098    @Override
099    public void initComponents() {
100        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
101        // buttons at top, like SE8c pane
102        add(initButtonPanel()); // requires presence of memo.
103        add(initDirectPanel()); // starts hidden, to set bits in Direct Mode only
104        add(initStatusPanel()); // positioned after ButtonPanel so to keep it simple also delayed
105        // creation of table must wait for memo + tc to be available, see initComponents(memo) next
106
107        // only way to get notice of the tool being closed, as a JPanel is silently embedded in some JFrame
108        addHierarchyListener(e -> {
109            if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
110                Component comp = e.getChanged();
111                if (comp instanceof JmriJFrame) {
112                    JmriJFrame toolFrame = (JmriJFrame) comp;
113                    if ((Objects.equals(toolFrame.getTitle(), this.getTitle()) &&
114                            !toolFrame.isVisible())) { // it was closed/hidden a moment ago
115                        handleCloseEvent();
116                        log.debug("Component hidden: {}", comp);
117                    }
118                }
119            }
120        });
121    }
122
123    @Override
124    public synchronized void initComponents(LocoNetSystemConnectionMemo memo) {
125        super.initComponents(memo);
126        this.memo = memo;
127        lncvdm = memo.getLncvDevicesManager();
128        pm = InstanceManager.getDefault(UserPreferencesManager.class);
129        // connect to the LnTrafficController
130        if (memo.getLnTrafficController() == null) {
131            log.error("No traffic controller is available");
132        } else {
133            // add listener
134            memo.getLnTrafficController().addLocoNetListener(~0, this);
135        }
136
137        // create the data model and its table
138        moduleTableModel = new LncvProgTableModel(this, memo);
139        moduleTable = new JTable(moduleTableModel);
140        moduleTable.setRowSelectionAllowed(false);
141        moduleTable.setPreferredScrollableViewportSize(new Dimension(300, 200));
142        moduleTable.setRowHeight(ROW_HEIGHT);
143        moduleTable.setDefaultEditor(JButton.class, new ButtonEditor(new JButton()));
144        moduleTable.setDefaultRenderer(JButton.class, new ButtonRenderer());
145        moduleTable.setRowSelectionAllowed(true);
146        moduleTable.getSelectionModel().addListSelectionListener(event -> {
147            synchronized (this) {
148                if (moduleTable.getSelectedRow() > -1 && moduleTable.getSelectedRow() < moduleTable.getRowCount()) {
149                    // print first column value from selected row
150                    copyEntry((int) moduleTable.getValueAt(moduleTable.getSelectedRow(), 1), (int) moduleTable.getValueAt(moduleTable.getSelectedRow(), 2));
151                }
152            }
153        });
154        // establish row sorting for the table
155        sorter = new TableRowSorter<>(moduleTableModel);
156        moduleTable.setRowSorter(sorter);
157         // establish table physical characteristics persistence
158        moduleTable.setName("LNCV Device Management"); // NOI18N
159        // Reset and then persist the table's ui state
160        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((tpm) -> {
161            synchronized (this) {
162                tpm.resetState(moduleTable);
163                tpm.persist(moduleTable, true);
164            }
165        });
166
167        JScrollPane tableScrollPane = new JScrollPane(moduleTable);
168        tablePanel = new JPanel();
169        Border resultBorder = BorderFactory.createEtchedBorder();
170        Border resultTitled = BorderFactory.createTitledBorder(resultBorder, Bundle.getMessage("LncvTableTitle"));
171        tablePanel.setBorder(resultTitled);
172        tablePanel.setLayout(new BoxLayout(tablePanel, BoxLayout.Y_AXIS));
173        tablePanel.add(tableScrollPane, BorderLayout.CENTER);
174
175        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tablePanel, getMonitorPanel());
176        splitPane.setOneTouchExpandable(true);
177        splitPane.setAlignmentX(Component.CENTER_ALIGNMENT);
178        splitPane.setBorder(BorderFactory.createEmptyBorder());
179        add(splitPane);
180
181        rawCheckBox.setSelected(pm.getSimplePreferenceState(rawDataCheck));
182    }
183
184    /*
185     * Initialize the LNCV Monitor panel.
186     */
187    protected JPanel getMonitorPanel() {
188        JPanel panel3 = new JPanel();
189        panel3.setLayout(new BoxLayout(panel3, BoxLayout.Y_AXIS));
190
191        JPanel panel31 = new JPanel();
192        panel31.setLayout(new BoxLayout(panel31, BoxLayout.Y_AXIS));
193        JScrollPane resultScrollPane = new JScrollPane(result);
194        panel31.add(resultScrollPane);
195
196        panel31.add(rawCheckBox);
197        rawCheckBox.setVisible(true);
198        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw"));
199        panel3.add(panel31);
200        Border panel3Border = BorderFactory.createEtchedBorder();
201        Border panel3Titled = BorderFactory.createTitledBorder(panel3Border, Bundle.getMessage("LncvMonitorTitle"));
202        panel3.setBorder(panel3Titled);
203        return panel3;
204    }
205
206    /*
207     * Initialize the Button panel. Requires presence of memo to send and receive.
208     */
209    protected JPanel initButtonPanel() {
210        // Set up buttons and entry fields
211        JPanel panel4 = new JPanel();
212        panel4.setLayout(new BoxLayout(panel4, BoxLayout.X_AXIS));
213        panel4.add(Box.createHorizontalGlue()); // this will expand/contract
214
215        JPanel panel41 = new JPanel();
216        panel41.setLayout(new BoxLayout(panel41, BoxLayout.PAGE_AXIS));
217        allProgButton.setText(allProgRunning ?
218                Bundle.getMessage("ButtonStopAllProg") : Bundle.getMessage("ButtonStartAllProg"));
219        allProgButton.setToolTipText(Bundle.getMessage("TipAllProgButton"));
220        allProgButton.addActionListener(e -> allProgButtonActionPerformed());
221        panel41.add(allProgButton);
222
223        modProgButton.setText((moduleProgRunning >= 0) ?
224                Bundle.getMessage("ButtonStopModProg") : Bundle.getMessage("ButtonStartModProg"));
225        modProgButton.setToolTipText(Bundle.getMessage("TipModuleProgButton"));
226        modProgButton.addActionListener(e -> modProgButtonActionPerformed());
227        panel41.add(modProgButton);
228        panel4.add(panel41);
229
230        JPanel panel42 = new JPanel();
231        panel42.setLayout(new BoxLayout(panel42, BoxLayout.PAGE_AXIS));
232
233        JPanel panel421 = new JPanel(); // default FlowLayout
234        panel421.add(articleFieldLabel);
235        // entry field (decimal)
236        articleField.setToolTipText(Bundle.getMessage("TipModuleArticleField"));
237        panel421.add(articleField);
238        panel42.add(panel421);
239
240        JPanel panel422 = new JPanel();
241        panel422.add(addressFieldLabel);
242        // entry field (decimal) for Module Address
243        addressField.setText("1");
244        panel422.add(addressField);
245        panel42.add(panel422);
246
247        panel42.add(directCheckBox);
248        directCheckBox.addActionListener(e -> directActionPerformed());
249        directCheckBox.setToolTipText(Bundle.getMessage("TipDirectMode"));
250        panel4.add(panel42);
251
252        JPanel panel43 = new JPanel();
253        Border panel43Border = BorderFactory.createEtchedBorder();
254        panel43.setBorder(panel43Border);
255        panel43.setLayout(new BoxLayout(panel43, BoxLayout.LINE_AXIS));
256
257        JPanel panel431 = new JPanel(); // labels
258        panel431.setLayout(new BoxLayout(panel431, BoxLayout.PAGE_AXIS));
259        cvFieldLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
260        cvFieldLabel.setMinimumSize(new Dimension(60, new JTextField("X").getHeight() + 5));
261        panel431.add(cvFieldLabel);
262        //cvField.setToolTipText(Bundle.getMessage("TipModuleCvField"));
263        valueFieldLabel.setMinimumSize(new Dimension(60, new JTextField("X").getHeight() + 5));
264        valueFieldLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
265        panel431.add(valueFieldLabel);
266        panel43.add(panel431);
267
268        JPanel panel432 = new JPanel(); // entry fields
269        panel432.setMaximumSize(new Dimension(50, 50));
270        panel432.setPreferredSize(new Dimension(50, 50));
271        panel432.setMinimumSize(new Dimension(50, 50));
272        panel432.setLayout(new BoxLayout(panel432, BoxLayout.PAGE_AXIS));
273        cvField.setText("0");
274        panel432.add(cvField); // entry field (decimal) for CV value
275        //valueField.setToolTipText(Bundle.getMessage("TipModuleValueField"));
276        valueField.setText("1");
277        panel432.add(valueField); // entry field (decimal) for CV number to read/write
278        panel43.add(panel432);
279
280        JPanel panel433 = new JPanel(); // read/write buttons
281        panel433.setLayout(new BoxLayout(panel433, BoxLayout.PAGE_AXIS));
282        panel433.add(readButton);
283        readButton.setEnabled(false);
284        readButton.addActionListener(e -> readButtonActionPerformed());
285
286        panel433.add(writeButton);
287        writeButton.setEnabled(false);
288        writeButton.addActionListener(e -> writeButtonActionPerformed());
289        panel43.add(panel433);
290        panel4.add(panel43);
291
292        panel4.add(Box.createHorizontalGlue()); // this will expand/contract
293        panel4.setAlignmentX(Component.CENTER_ALIGNMENT);
294
295        return panel4;
296    }
297
298    /*
299     * Initialize the Status panel.
300     */
301    protected JPanel initStatusPanel() {
302        JPanel panel2 = new JPanel();
303        panel2.setLayout(new BoxLayout(panel2, BoxLayout.PAGE_AXIS));
304
305        statusText1.setText("   ");
306        statusText1.setHorizontalAlignment(JLabel.CENTER);
307        panel2.add(statusText1);
308
309        statusText2.setText("   ");
310        statusText2.setHorizontalAlignment(JLabel.CENTER);
311        panel2.add(statusText2);
312
313        panel2.setAlignmentX(Component.CENTER_ALIGNMENT);
314        return panel2;
315    }
316
317    /**
318     * GENERALPROG button.
319     */
320    public void allProgButtonActionPerformed() {
321        if (moduleProgRunning >= 0) {
322            statusText1.setText(Bundle.getMessage("FeedBackModProgRunning"));
323            return;
324        }
325        if (directCheckBox.isSelected()) {
326            statusText1.setText(Bundle.getMessage("FeedBackDirectRunning"));
327            return;
328        }
329        // provide user feedback
330        readButton.setEnabled(!allProgRunning);
331        writeButton.setEnabled(!allProgRunning);
332        log.debug("AllProg pressed, allProgRunning={}", allProgRunning);
333        if (allProgRunning) {
334            log.debug("Session was running, closing");
335            // send LncvAllProgEnd command on LocoNet
336            memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createAllProgEndRequest(art));
337            statusText1.setText(Bundle.getMessage("FeedBackStopAllProg"));
338            allProgButton.setText(Bundle.getMessage("ButtonStartAllProg"));
339            articleField.setEditable(true);
340            addressField.setEditable(true);
341            allProgRunning = false;
342            return;
343        }
344        articleField.setEditable(false);
345        addressField.setEditable(false);
346        art = -1;
347        if (!articleField.getText().isEmpty()) {
348            try {
349                art = inDomain(articleField.getText(), 9999);
350            } catch (NumberFormatException e) {
351                // fine, will do broadcast all
352            }
353        }
354        // show dialog to protect unwanted ALL messages
355        Object[] dialogBoxButtonOptions = {
356                Bundle.getMessage("ButtonProceed"),
357                Bundle.getMessage("ButtonCancel")};
358        int userReply = JmriJOptionPane.showOptionDialog(this.getParent(),
359                Bundle.getMessage("DialogAllWarning"),
360                Bundle.getMessage("WarningTitle"),
361                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE,
362                null, dialogBoxButtonOptions, dialogBoxButtonOptions[1]);
363        if (userReply != 0 ) { // not array position 0 ButtonProceed
364            return;
365        }
366        statusText1.setText(Bundle.getMessage("FeedBackStartAllProg"));
367        // send LncvProgSessionStart command on LocoNet
368        LocoNetMessage m = LncvMessageContents.createAllProgStartRequest(art);
369        memo.getLnTrafficController().sendLocoNetMessage(m);
370        // stop and inform user
371        statusText1.setText(Bundle.getMessage("FeedBackStartAllProg"));
372        allProgButton.setText(Bundle.getMessage("ButtonStopAllProg"));
373        allProgRunning = true;
374        log.debug("AllProgRunning=TRUE, allProgButtonActionPerformed ready");
375    }
376
377    // MODULEPROG button
378    /**
379     * Handle Start/End Module Prog button.
380     */
381    public void modProgButtonActionPerformed() {
382        if (allProgRunning) {
383            statusText1.setText(Bundle.getMessage("FeedBackAllProgRunning"));
384            return;
385        }
386        if (directCheckBox.isSelected()) {
387            statusText1.setText(Bundle.getMessage("FeedBackDirectRunning"));
388            return;
389        }
390        if (articleField.getText().isEmpty()) {
391            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
392            articleField.setBackground(Color.RED);
393            modProgButton.setSelected(false);
394            return;
395        }
396        if (addressField.getText().isEmpty()) {
397            statusText1.setText(Bundle.getMessage("FeedBackEnterAddress"));
398            addressField.setBackground(Color.RED);
399            modProgButton.setSelected(false);
400            return;
401        }
402        // provide user feedback
403        articleField.setBackground(Color.WHITE); // reset
404        readButton.setEnabled(moduleProgRunning < 0);
405        writeButton.setEnabled(moduleProgRunning < 0);
406        if (moduleProgRunning >= 0) { // stop prog
407            try {
408                art = inDomain(articleField.getText(), 9999);
409                adr = moduleProgRunning; // use module address that was used to start Modprog
410                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createModProgEndRequest(art, adr));
411                statusText1.setText(Bundle.getMessage("FeedBackModProgClosed", adr));
412                modProgButton.setText(Bundle.getMessage("ButtonStartModProg"));
413                moduleProgRunning = -1;
414                articleField.setEditable(true);
415                addressField.setEditable(true);
416            } catch (NumberFormatException e) {
417                statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
418                modProgButton.setSelected(true);
419            }
420            return;
421        }
422        if ((!articleField.getText().isEmpty()) && (!addressField.getText().isEmpty())) {
423            try {
424                art = inDomain(articleField.getText(), 9999);
425                adr = inDomain(addressField.getText(), 65535); // goes in d5-d6 as module address
426                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createModProgStartRequest(art, adr));
427                statusText1.setText(Bundle.getMessage("FeedBackModProgOpen", adr));
428                modProgButton.setText(Bundle.getMessage("ButtonStopModProg"));
429                moduleProgRunning = adr; // store address during modProg, so next line is mostly as UI indication:
430                articleField.setEditable(false);
431                addressField.setEditable(false); // lock address field to prevent accidentally changing it
432
433            } catch (NumberFormatException e) {
434                log.error("invalid entry, must be number");
435            }
436        }
437        // stop and inform user
438    }
439
440    // READCV button
441    /**
442     * Handle Read CV button, assemble LNCV read message. Requires presence of memo.
443     */
444    public void readButtonActionPerformed() {
445        String sArt = "65535"; // LncvMessageContents.LNCV_ALL = broadcast
446        if (moduleProgRunning >= 0) {
447            sArt = articleField.getText();
448            articleField.setBackground(Color.WHITE); // reset
449        }
450        if ((sArt != null) && (addressField.getText() != null) && (cvField.getText() != null)) {
451            try {
452                art = inDomain(sArt, 9999); // limited according to Uhlenbrock info
453                adr = inDomain(addressField.getText(), 65535); // used as address for monitor reply
454                cv = inDomain(cvField.getText(), 9999); // decimal entry
455                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createCvReadRequest(art, adr, cv));
456            } catch (NumberFormatException e) {
457                log.error("invalid entry, must be number");
458            }
459        } else {
460            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
461            articleField.setBackground(Color.RED);
462            return;
463        }
464        // stop and inform user
465        statusText1.setText(Bundle.getMessage("FeedBackRead", "LNCV"));
466    }
467
468    // WriteCV button
469    /**
470     * Handle Write button click, assemble LNCV write message. Requires presence of memo.
471     */
472    public void writeButtonActionPerformed() {
473        String sArt = "65535"; // LncvMessageContents.LNCV_ALL;
474        if (moduleProgRunning >= 0) {
475            sArt = articleField.getText();
476        }
477        if ((sArt != null) && (cvField.getText() != null) && (valueField.getText() != null)) {
478            articleField.setBackground(Color.WHITE);
479            try {
480                art = inDomain(sArt, 9999);
481                cv = inDomain(cvField.getText(), 9999); // decimal entry
482                val = inDomain(valueField.getText(), 65535); // decimal entry
483                if (cv == 0 && (val > 65534 || val < 1)) {
484                    // reserved general module address, warn in status and abort
485                    statusText1.setText(Bundle.getMessage("FeedBackValidAddressRange"));
486                    valueField.setBackground(Color.RED);
487                    return;
488                }
489                writeConfirmed = false;
490                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createCvWriteRequest(art, cv, val));
491                valueField.setBackground(Color.ORANGE);
492            } catch (NumberFormatException e) {
493                log.error("invalid entry, must be number");
494            }
495        } else {
496            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
497            articleField.setBackground(Color.RED);
498            return;
499        }
500        // stop and inform user
501        statusText1.setText(Bundle.getMessage("FeedBackWrite", "LNCV"));
502        // LACK reply will be received separately
503        // if (received) {
504        //      writeConfirmed = true;
505        // }
506    }
507
508    private JPanel ledPanel;
509
510    // a row of checkboxes to set LEDs in module on/off
511    private JPanel initDirectPanel() {
512        ledPanel = new JPanel();
513        for (int i = 0; i < 16; i++) {
514            JCheckBox ledBox = new JCheckBox(""+i);
515            ledPanel.add(ledBox);
516        }
517        JPanel options = new JPanel();
518        options.setLayout(new BoxLayout(options, BoxLayout.Y_AXIS));
519        JToggleButton buttonAll = new JToggleButton(Bundle.getMessage("AllOn"));
520        buttonAll.addActionListener(e -> toggleAll(buttonAll.isSelected()));
521        options.add(buttonAll);
522        JCheckBox serieTwo = new JCheckBox("LED2");
523        serieTwo.addActionListener(e -> renumber(serieTwo.isSelected()));
524        options.add(serieTwo); // place to the right of Set button
525        ledPanel.add(options);
526        JButton buttonSet = new JButton(Bundle.getMessage("ButtonSetDirect"));
527        ledPanel.add(buttonSet);
528        buttonSet.addActionListener(e -> setDirect(serieTwo.isSelected()));
529        ledPanel.setVisible(false); // initially hide ledPanel
530        return ledPanel;
531    }
532
533    private void toggleAll(boolean on) {
534        for (int j = 0; j < 16 ; j++) {
535            ((JCheckBox)ledPanel.getComponent(j)).setSelected(on);
536        }
537    }
538
539    protected void directActionPerformed() {
540        if (allProgRunning || moduleProgRunning > -1) {
541            directCheckBox.setSelected(false);
542            return;
543        }
544        if (directCheckBox.isSelected()) {
545            articleField.setEditable(false);
546            articleField.setText("6900"); // fixed article number as per documentation
547            articleField.setBackground(Color.WHITE); // reset
548            readButton.setEnabled (false);
549            ledPanel.setVisible(true);
550        } else {
551            articleField.setText("");
552            articleField.setEditable(true);
553            readButton.setEnabled (true);
554            ledPanel.setVisible(false);
555        }
556    }
557
558    /**
559     * Renumber the checkbox labels to match LED numbers.
560     * @param range2 false for LEDs 0-15, true for LEDs 16-31
561     */
562    protected void renumber(boolean range2) {
563        for (int j = 0; j < 16 ; j++) {
564            ((JCheckBox)ledPanel.getComponent(j)).setText(range2 ? ""+(j+16) : ""+j);
565        }
566    }
567
568    // SetDirect button
569    /**
570     * Handle SetDirect button, assemble LNCV Direct Set message. Requires presence of memo to send.
571     * @param range2 false for LEDs 0-15, true for LEDs 16-31
572     */
573    protected void setDirect(boolean range2) {
574        if (addressField.getText() != null) {
575            try {
576                adr = inDomain(addressField.getText(), 65535);
577                int cv = 0x00;
578                // fetch the bits as set on the ledPanel
579                for (int j = 0; j < 16 ; j++) {
580                    cv += (((JCheckBox)ledPanel.getComponent(j)).isSelected() ? (1 << j) : 0);
581                    //log.debug("j={} cv={}", j, cv);
582                }
583                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createDirectWriteRequest(adr, cv, range2));
584            } catch (NumberFormatException e) {
585                log.error("invalid entry, must be number");
586            }
587        } else {
588            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
589            addressField.setBackground(Color.RED);
590            return;
591        }
592        // stop and inform user
593        statusText1.setText(Bundle.getMessage("FeedBackSetDirect"));
594    }
595
596    private int inDomain(String entry, int max) {
597        int n = -1;
598        try {
599            n = Integer.parseInt(entry);
600        } catch (NumberFormatException e) {
601            log.error("invalid entry, must be number");
602        }
603        if ((0 <= n) && (n <= max)) {
604            return n;
605        } else {
606            statusText1.setText(Bundle.getMessage("FeedBackInputOutsideRange"));
607            return 0;
608        }
609    }
610
611    public void copyEntry(int art, int mod) {
612        if ((moduleProgRunning < 0) && !allProgRunning) { // protect locked fields while programming
613            articleField.setText(art + "");
614            addressField.setText(mod + "");
615        }
616    }
617
618    /**
619     * {@inheritDoc}
620     * Compare to {@link LnOpsModeProgrammer#message(jmri.jmrix.loconet.LocoNetMessage)}
621     *
622     * @param m a message received and analysed for LNCV characteristics
623     */
624    @Override
625    public synchronized void message(LocoNetMessage m) { // receive a LocoNet message and log it
626        // got a LocoNet message, see if it's an LNCV response
627        //log.debug("LncvProgPane heard message {}", m.toMonitorString());
628        if (LncvMessageContents.isSupportedLncvMessage(m)) {
629            // raw data, to display
630            String raw = (rawCheckBox.isSelected() ? ("[" + m + "] ") : "");
631            // format the message text, expect it to provide consistent \n after each line
632            String formatted = m.toMonitorString(memo.getSystemPrefix());
633            // copy the formatted data
634            reply += raw + formatted;
635        }
636        // or LACK write confirmation response from module?
637        if ((m.getOpCode() == LnConstants.OPC_LONG_ACK) &&
638                (m.getElement(1) == 0x6D)) { // elem 1 = OPC (matches 0xED), elem 2 = ack1
639            writeConfirmed = true;
640            if (m.getElement(2) == 0x7f) {
641                valueField.setBackground(Color.GREEN);
642                reply += Bundle.getMessage("LNCV_WRITE_CONFIRMED", moduleProgRunning) + "\n";
643            } else if (m.getElement(2) == 1) {
644                valueField.setBackground(Color.RED);
645                reply += Bundle.getMessage("LNCV_WRITE_CV_NOTSUPPORTED", moduleProgRunning, cv) + "\n";
646            } else if (m.getElement(2) == 2) {
647                valueField.setBackground(Color.RED);
648                reply += Bundle.getMessage("LNCV_WRITE_CV_READONLY", moduleProgRunning, cv) + "\n";
649            } else if (m.getElement(2) == 3) {
650                valueField.setBackground(Color.RED);
651                reply += Bundle.getMessage("LNCV_WRITE_CV_OUTOFBOUNDS", moduleProgRunning, val) + "\n";
652            }
653        }
654        if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_WRITE) {
655            reply += Bundle.getMessage("LNCV_WRITE_MOD_MONITOR", (moduleProgRunning == -1 ? "ALL" : moduleProgRunning)) + "\n";
656        }
657        if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ) {
658            reply += Bundle.getMessage("LNCV_READ_MOD_MONITOR", (moduleProgRunning == -1 ? "ALL" : moduleProgRunning)) + "\n";
659        }
660        if ((LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY) ||
661                (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY2)) {
662            // it's an LNCV ReadReply message, decode contents:
663            LncvMessageContents contents = new LncvMessageContents(m);
664            int msgArt = contents.getLncvArticleNum();
665            int msgAdr = moduleProgRunning;
666            int msgCv = contents.getCvNum();
667            int msgVal = contents.getCvValue();
668            if ((msgCv == 0) || (msgArt == art)) { // trust last used address. to be sure, check against Article (hardware class) number
669                msgAdr = msgVal; // if cvNum = 0, this is the LNCV module address
670            }
671            String foundMod = "(LNCV) " + Bundle.getMessage("LabelArticle") +  art + " "
672                    + Bundle.getMessage("LabelAddress") + msgAdr + " "
673                    + Bundle.getMessage("LabelCv") + msgCv + " "
674                    + Bundle.getMessage("LabelValue")+ msgVal + "\n";
675            reply += foundMod;
676            log.debug("ReadReply={}", reply);
677            // storing a Module in the list using the (first) write reply is handled by loconet.LncvDevicesManager
678
679            // enter returned CV in CVnum field
680            cvField.setText(msgCv + "");
681            cvField.setBackground(Color.WHITE);
682            // enter returned value in Value field
683            valueField.setText(msgVal + "");
684            valueField.setBackground(Color.WHITE);
685
686            LncvDevice dev = memo.getLncvDevicesManager().getDevice(art, adr);
687            if (dev != null) {
688                dev.setCvNum(msgCv);
689                dev.setCvValue(msgVal);
690            }
691            memo.getLncvDevicesManager().firePropertyChange("DeviceListChanged", true, false);
692        }
693
694        if (reply != null) { // we fool allProgFinished (copied from LNSV2 class)
695            allProgFinished(null);
696        }
697    }
698
699    /**
700     * AllProg Session callback.
701     *
702     * @param error feedback from Finish process
703     */
704    public void allProgFinished(String error) {
705        if (error != null) {
706             log.error("LNCV process finished with error: {}", error);
707             statusText2.setText(Bundle.getMessage("FeedBackDiscoverFail"));
708        } else {
709            synchronized (this) {
710                if (lncvdm.getDeviceCount() == 1) {
711                    statusText2.setText(Bundle.getMessage("FeedBackDiscoverSuccessOne"));
712                } else {
713                    statusText2.setText(Bundle.getMessage("FeedBackDiscoverSuccess", lncvdm.getDeviceCount()));
714                }
715                result.setText(reply);
716            }
717        }
718    }
719
720    /**
721     * Give user feedback on closing of any open programming sessions when tool window is closed.
722     * @see #dispose() for actual closing of sessions
723     */
724    public void handleCloseEvent() {
725        //log.debug("handleCloseEvent() called in LncvProgPane");
726        if (allProgRunning || moduleProgRunning > 0) {
727            // adds a Don't remember again checkbox and stores setting in pm
728            // show dialog
729            if (pm != null && !pm.getSimplePreferenceState(dontWarnOnClose)) {
730                final JDialog dialog = new JDialog();
731                dialog.setTitle(Bundle.getMessage("ReminderTitle"));
732                dialog.setLocationRelativeTo(null);
733                dialog.setDefaultCloseOperation(javax.swing.JFrame.DISPOSE_ON_CLOSE);
734                JPanel container = new JPanel();
735                container.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
736                container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
737
738                JLabel question = new JLabel(Bundle.getMessage("DialogRunningWarning"), JLabel.CENTER);
739                question.setAlignmentX(Component.CENTER_ALIGNMENT);
740                container.add(question);
741
742                JButton okButton = new JButton(Bundle.getMessage("ButtonOK"));
743                JPanel buttons = new JPanel();
744                buttons.setAlignmentX(Component.CENTER_ALIGNMENT);
745                buttons.add(okButton);
746                container.add(buttons);
747
748                final JCheckBox remember = new JCheckBox(Bundle.getMessage("DontRemind"));
749                remember.setAlignmentX(Component.CENTER_ALIGNMENT);
750                remember.setFont(remember.getFont().deriveFont(10f));
751                container.add(remember);
752
753                okButton.addActionListener(e -> {
754                    if ((remember.isSelected()) && (pm != null)) {
755                        pm.setSimplePreferenceState(dontWarnOnClose, remember.isSelected());
756                    }
757                    dialog.dispose();
758                });
759
760
761                dialog.getContentPane().add(container);
762                dialog.pack();
763                dialog.setModal(true);
764                dialog.setVisible(true);
765            }
766
767            // dispose will take care of actually stopping any open prog session
768        }
769    }
770
771    /**
772     * {@inheritDoc}
773     */
774    @Override
775    public void dispose() {
776        if (memo != null && memo.getLnTrafficController() != null) {
777            // disconnect from the LnTrafficController, normally attached/detached after Discovery completed
778            memo.getLnTrafficController().removeLocoNetListener(~0, this);
779        }
780        // and unwind swing
781        if (pm != null) {
782            pm.setSimplePreferenceState(rawDataCheck, rawCheckBox.isSelected());
783        }
784        // prevent closing LNCV tool with programming session left open on module(s).
785        if (moduleProgRunning >= 0) {
786            modProgButtonActionPerformed();
787        }
788        if (allProgRunning) {
789            allProgButtonActionPerformed();
790        }
791        super.setVisible(false);
792
793        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((tpm) -> {
794            synchronized (this) {
795                tpm.stopPersisting(moduleTable);
796            }
797        });
798
799        super.dispose();
800    }
801
802    /**
803     * Testing methods.
804     *
805     * @return text currently in Article field
806     */
807    protected String getArticleEntry() {
808        if (!articleField.isEditable()) {
809            return "locked";
810        } else {
811            return articleField.getText();
812        }
813    }
814
815    protected String getAddressEntry() {
816        if (!addressField.isEditable()) {
817            return "locked";
818        } else {
819            return addressField.getText();
820        }
821    }
822
823    protected synchronized String getMonitorContents(){
824            return reply;
825    }
826
827    protected void setCvFields(int cvNum, int cvVal) {
828        cvField.setText("" + cvNum);
829        if (cvVal > -1) {
830            valueField.setText("" + cvVal);
831        } else {
832            valueField.setText("");
833        }
834    }
835
836    protected synchronized LncvDevice getModule(int i) {
837        if (lncvdm == null) {
838            lncvdm = memo.getLncvDevicesManager();
839        }
840        log.debug("lncvdm.getDeviceCount()={}", lncvdm.getDeviceCount());
841        if (i > -1 && i < lncvdm.getDeviceCount()) {
842            return lncvdm.getDeviceList().getDevice(i);
843        } else {
844            log.debug("getModule({}) failed", i);
845            return null;
846        }
847    }
848
849    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvProgPane.class);
850
851}