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 *
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 *
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        // this does not fill the full width, why?
176//        JSplitPane holder = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
177//                tablePanel, getMonitorPanel());
178//        holder.setMinimumSize(new Dimension(1000, 400));
179//        holder.setPreferredSize(new Dimension(1000, 400));
180//        holder.setDividerSize(8);
181//        holder.setOneTouchExpandable(true);
182//        add(holder, BorderLayout.LINE_START);
183        add(tablePanel);
184        add(getMonitorPanel());
185        rawCheckBox.setSelected(pm.getSimplePreferenceState(rawDataCheck));
186    }
187
188    /*
189     * Initialize the LNCV Monitor panel.
190     */
191    protected JPanel getMonitorPanel() {
192        JPanel panel3 = new JPanel();
193        panel3.setLayout(new BoxLayout(panel3, BoxLayout.Y_AXIS));
194
195        JPanel panel31 = new JPanel();
196        panel31.setLayout(new BoxLayout(panel31, BoxLayout.Y_AXIS));
197        JScrollPane resultScrollPane = new JScrollPane(result);
198        panel31.add(resultScrollPane);
199
200        panel31.add(rawCheckBox);
201        rawCheckBox.setVisible(true);
202        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw"));
203        panel3.add(panel31);
204        Border panel3Border = BorderFactory.createEtchedBorder();
205        Border panel3Titled = BorderFactory.createTitledBorder(panel3Border, Bundle.getMessage("LncvMonitorTitle"));
206        panel3.setBorder(panel3Titled);
207        return panel3;
208    }
209
210    /*
211     * Initialize the Button panel. Requires presence of memo to send and receive.
212     */
213    protected JPanel initButtonPanel() {
214        // Set up buttons and entry fields
215        JPanel panel4 = new JPanel();
216        panel4.setLayout(new FlowLayout());
217
218        JPanel panel41 = new JPanel();
219        panel41.setLayout(new BoxLayout(panel41, BoxLayout.PAGE_AXIS));
220        allProgButton.setText(allProgRunning ?
221                Bundle.getMessage("ButtonStopAllProg") : Bundle.getMessage("ButtonStartAllProg"));
222        allProgButton.setToolTipText(Bundle.getMessage("TipAllProgButton"));
223        allProgButton.addActionListener(e -> allProgButtonActionPerformed());
224        panel41.add(allProgButton);
225
226        modProgButton.setText((moduleProgRunning >= 0) ?
227                Bundle.getMessage("ButtonStopModProg") : Bundle.getMessage("ButtonStartModProg"));
228        modProgButton.setToolTipText(Bundle.getMessage("TipModuleProgButton"));
229        modProgButton.addActionListener(e -> modProgButtonActionPerformed());
230        panel41.add(modProgButton);
231        panel4.add(panel41);
232
233        JPanel panel42 = new JPanel();
234        panel42.setLayout(new BoxLayout(panel42, BoxLayout.PAGE_AXIS));
235        JPanel panel421 = new JPanel();
236        panel421.add(articleFieldLabel);
237        // entry field (decimal)
238        articleField.setToolTipText(Bundle.getMessage("TipModuleArticleField"));
239        panel421.add(articleField);
240        panel42.add(panel421);
241
242        JPanel panel422 = new JPanel();
243        panel422.add(addressFieldLabel);
244        // entry field (decimal) for Module Address
245        addressField.setText("1");
246        panel422.add(addressField);
247        panel42.add(panel422);
248        panel42.add(directCheckBox);
249        directCheckBox.addActionListener(e -> directActionPerformed());
250        directCheckBox.setToolTipText(Bundle.getMessage("TipDirectMode"));
251        panel4.add(panel42);
252
253        JPanel panel43 = new JPanel();
254        Border panel43Border = BorderFactory.createEtchedBorder();
255        panel43.setBorder(panel43Border);
256        panel43.setLayout(new BoxLayout(panel43, BoxLayout.LINE_AXIS));
257
258        JPanel panel431 = new JPanel();
259        panel431.setLayout(new BoxLayout(panel431, BoxLayout.PAGE_AXIS));
260        JPanel panel4311 = new JPanel();
261        panel4311.add(cvFieldLabel);
262        // entry field (decimal) for CV number to read/write
263        //cvField.setToolTipText(Bundle.getMessage("TipModuleCvField"));
264        cvField.setText("0");
265        panel4311.add(cvField);
266        panel431.add(panel4311);
267
268        JPanel panel4312 = new JPanel();
269        panel4312.add(valueFieldLabel);
270        // entry field (decimal) for CV value
271        //valueField.setToolTipText(Bundle.getMessage("TipModuleValueField"));
272        valueField.setText("1");
273        panel4312.add(valueField);
274        panel431.add(panel4312);
275        panel43.add(panel431);
276
277        JPanel panel432 = new JPanel();
278        panel432.setLayout(new BoxLayout(panel432, BoxLayout.PAGE_AXIS));
279        panel432.add(readButton);
280        readButton.setEnabled(false);
281        readButton.addActionListener(e -> readButtonActionPerformed());
282
283        panel432.add(writeButton);
284        writeButton.setEnabled(false);
285        writeButton.addActionListener(e -> writeButtonActionPerformed());
286        panel43.add(panel432);
287        panel4.add(panel43);
288
289        return panel4;
290    }
291
292    /*
293     * Initialize the Status panel.
294     */
295    protected JPanel initStatusPanel() {
296        JPanel panel2 = new JPanel();
297        panel2.setLayout(new BoxLayout(panel2, BoxLayout.PAGE_AXIS));
298        JPanel panel21 = new JPanel();
299        panel21.setLayout(new FlowLayout());
300
301        statusText1.setText("   ");
302        statusText1.setHorizontalAlignment(JLabel.CENTER);
303        panel21.add(statusText1);
304        panel2.add(panel21);
305
306        statusText2.setText("   ");
307        statusText2.setHorizontalAlignment(JLabel.CENTER);
308        panel2.add(statusText2);
309        return panel2;
310    }
311
312    /**
313     * GENERALPROG button.
314     */
315    public void allProgButtonActionPerformed() {
316        if (moduleProgRunning >= 0) {
317            statusText1.setText(Bundle.getMessage("FeedBackModProgRunning"));
318            return;
319        }
320        if (directCheckBox.isSelected()) {
321            statusText1.setText(Bundle.getMessage("FeedBackDirectRunning"));
322            return;
323        }
324        // provide user feedback
325        readButton.setEnabled(!allProgRunning);
326        writeButton.setEnabled(!allProgRunning);
327        log.debug("AllProg pressed, allProgRunning={}", allProgRunning);
328        if (allProgRunning) {
329            log.debug("Session was running, closing");
330            // send LncvAllProgEnd command on LocoNet
331            memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createAllProgEndRequest(art));
332            statusText1.setText(Bundle.getMessage("FeedBackStopAllProg"));
333            allProgButton.setText(Bundle.getMessage("ButtonStartAllProg"));
334            articleField.setEditable(true);
335            addressField.setEditable(true);
336            allProgRunning = false;
337            return;
338        }
339        articleField.setEditable(false);
340        addressField.setEditable(false);
341        art = -1;
342        if (!articleField.getText().equals("")) {
343            try {
344                art = inDomain(articleField.getText(), 9999);
345            } catch (NumberFormatException e) {
346                // fine, will do broadcast all
347            }
348        }
349        // show dialog to protect unwanted ALL messages
350        Object[] dialogBoxButtonOptions = {
351                Bundle.getMessage("ButtonProceed"),
352                Bundle.getMessage("ButtonCancel")};
353        int userReply = JmriJOptionPane.showOptionDialog(this.getParent(),
354                Bundle.getMessage("DialogAllWarning"),
355                Bundle.getMessage("WarningTitle"),
356                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE,
357                null, dialogBoxButtonOptions, dialogBoxButtonOptions[1]);
358        if (userReply != 0 ) { // not array position 0 ButtonProceed
359            return;
360        }
361        statusText1.setText(Bundle.getMessage("FeedBackStartAllProg"));
362        // send LncvProgSessionStart command on LocoNet
363        LocoNetMessage m = LncvMessageContents.createAllProgStartRequest(art);
364        memo.getLnTrafficController().sendLocoNetMessage(m);
365        // stop and inform user
366        statusText1.setText(Bundle.getMessage("FeedBackStartAllProg"));
367        allProgButton.setText(Bundle.getMessage("ButtonStopAllProg"));
368        allProgRunning = true;
369        log.debug("AllProgRunning=TRUE, allProgButtonActionPerformed ready");
370    }
371
372    // MODULEPROG button
373    /**
374     * Handle Start/End Module Prog button.
375     */
376    public void modProgButtonActionPerformed() {
377        if (allProgRunning) {
378            statusText1.setText(Bundle.getMessage("FeedBackAllProgRunning"));
379            return;
380        }
381        if (directCheckBox.isSelected()) {
382            statusText1.setText(Bundle.getMessage("FeedBackDirectRunning"));
383            return;
384        }
385        if (articleField.getText().equals("")) {
386            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
387            articleField.setBackground(Color.RED);
388            modProgButton.setSelected(false);
389            return;
390        }
391        if (addressField.getText().equals("")) {
392            statusText1.setText(Bundle.getMessage("FeedBackEnterAddress"));
393            addressField.setBackground(Color.RED);
394            modProgButton.setSelected(false);
395            return;
396        }
397        // provide user feedback
398        articleField.setBackground(Color.WHITE); // reset
399        readButton.setEnabled(moduleProgRunning < 0);
400        writeButton.setEnabled(moduleProgRunning < 0);
401        if (moduleProgRunning >= 0) { // stop prog
402            try {
403                art = inDomain(articleField.getText(), 9999);
404                adr = moduleProgRunning; // use module address that was used to start Modprog
405                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createModProgEndRequest(art, adr));
406                statusText1.setText(Bundle.getMessage("FeedBackModProgClosed", adr));
407                modProgButton.setText(Bundle.getMessage("ButtonStartModProg"));
408                moduleProgRunning = -1;
409                articleField.setEditable(true);
410                addressField.setEditable(true);
411            } catch (NumberFormatException e) {
412                statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
413                modProgButton.setSelected(true);
414            }
415            return;
416        }
417        if ((!articleField.getText().equals("")) && (!addressField.getText().equals(""))) {
418            try {
419                art = inDomain(articleField.getText(), 9999);
420                adr = inDomain(addressField.getText(), 65535); // goes in d5-d6 as module address
421                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createModProgStartRequest(art, adr));
422                statusText1.setText(Bundle.getMessage("FeedBackModProgOpen", adr));
423                modProgButton.setText(Bundle.getMessage("ButtonStopModProg"));
424                moduleProgRunning = adr; // store address during modProg, so next line is mostly as UI indication:
425                articleField.setEditable(false);
426                addressField.setEditable(false); // lock address field to prevent accidentally changing it
427
428            } catch (NumberFormatException e) {
429                log.error("invalid entry, must be number");
430            }
431        }
432        // stop and inform user
433    }
434
435    // READCV button
436    /**
437     * Handle Read CV button, assemble LNCV read message. Requires presence of memo.
438     */
439    public void readButtonActionPerformed() {
440        String sArt = "65535"; // LncvMessageContents.LNCV_ALL = broadcast
441        if (moduleProgRunning >= 0) {
442            sArt = articleField.getText();
443            articleField.setBackground(Color.WHITE); // reset
444        }
445        if ((sArt != null) && (addressField.getText() != null) && (cvField.getText() != null)) {
446            try {
447                art = inDomain(sArt, 9999); // limited according to Uhlenbrock info
448                adr = inDomain(addressField.getText(), 65535); // used as address for reply
449                cv = inDomain(cvField.getText(), 9999); // decimal entry
450                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createCvReadRequest(art, adr, cv));
451            } catch (NumberFormatException e) {
452                log.error("invalid entry, must be number");
453            }
454        } else {
455            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
456            articleField.setBackground(Color.RED);
457            return;
458        }
459        // stop and inform user
460        statusText1.setText(Bundle.getMessage("FeedBackRead"));
461    }
462
463    // WriteCV button
464    /**
465     * Handle Write button click, assemble LNCV write message. Requires presence of memo.
466     */
467    public void writeButtonActionPerformed() {
468        String sArt = "65535"; // LncvMessageContents.LNCV_ALL;
469        if (moduleProgRunning >= 0) {
470            sArt = articleField.getText();
471        }
472        if ((sArt != null) && (cvField.getText() != null) && (valueField.getText() != null)) {
473            articleField.setBackground(Color.WHITE);
474            try {
475                art = inDomain(sArt, 9999);
476                cv = inDomain(cvField.getText(), 9999); // decimal entry
477                val = inDomain(valueField.getText(), 65535); // decimal entry
478                if (cv == 0 && (val > 65534 || val < 1)) {
479                    // reserved general module address, warn in status and abort
480                    statusText1.setText(Bundle.getMessage("FeedBackValidAddressRange"));
481                    valueField.setBackground(Color.RED);
482                    return;
483                }
484                writeConfirmed = false;
485                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createCvWriteRequest(art, cv, val));
486                valueField.setBackground(Color.ORANGE);
487            } catch (NumberFormatException e) {
488                log.error("invalid entry, must be number");
489            }
490        } else {
491            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
492            articleField.setBackground(Color.RED);
493            return;
494        }
495        // stop and inform user
496        statusText1.setText(Bundle.getMessage("FeedBackWrite"));
497        // LACK reply will be received separately
498        // if (received) {
499        //      writeConfirmed = true;
500        // }
501    }
502
503    private JPanel ledPanel;
504
505    // a row of checkboxes to set LEDs in module on/off
506    private JPanel initDirectPanel() {
507        ledPanel = new JPanel();
508        for (int i = 0; i < 16; i++) {
509            JCheckBox ledBox = new JCheckBox(""+i);
510            ledPanel.add(ledBox);
511        }
512        JPanel options = new JPanel();
513        options.setLayout(new BoxLayout(options, BoxLayout.Y_AXIS));
514        JToggleButton buttonAll = new JToggleButton(Bundle.getMessage("AllOn"));
515        buttonAll.addActionListener(e -> toggleAll(buttonAll.isSelected()));
516        options.add(buttonAll);
517        JCheckBox serieTwo = new JCheckBox("LED2");
518        serieTwo.addActionListener(e -> renumber(serieTwo.isSelected()));
519        options.add(serieTwo); // place to the right of Set button
520        ledPanel.add(options);
521        JButton buttonSet = new JButton(Bundle.getMessage("ButtonSetDirect"));
522        ledPanel.add(buttonSet);
523        buttonSet.addActionListener(e -> setDirect(serieTwo.isSelected()));
524        ledPanel.setVisible(false); // initially hide ledPanel
525        return ledPanel;
526    }
527
528    private void toggleAll(boolean on) {
529        for (int j = 0; j < 16 ; j++) {
530            ((JCheckBox)ledPanel.getComponent(j)).setSelected(on);
531        }
532    }
533
534    protected void directActionPerformed() {
535        if (allProgRunning || moduleProgRunning > -1) {
536            directCheckBox.setSelected(false);
537            return;
538        }
539        if (directCheckBox.isSelected()) {
540            articleField.setEditable(false);
541            articleField.setText("6900"); // fixed article number as per documentation
542            articleField.setBackground(Color.WHITE); // reset
543            readButton.setEnabled (false);
544            ledPanel.setVisible(true);
545        } else {
546            articleField.setText("");
547            articleField.setEditable(true);
548            readButton.setEnabled (true);
549            ledPanel.setVisible(false);
550        }
551    }
552
553    /**
554     * Renumber the checkbox labels to match LED numbers.
555     * @param range2 false for LEDs 0-15, true for LEDs 16-31
556     */
557    protected void renumber(boolean range2) {
558        for (int j = 0; j < 16 ; j++) {
559            ((JCheckBox)ledPanel.getComponent(j)).setText(range2 ? ""+(j+16) : ""+j);
560        }
561    }
562
563    // SetDirect button
564    /**
565     * Handle SetDirect button, assemble LNCV Direct Set message. Requires presence of memo to send.
566     * @param range2 false for LEDs 0-15, true for LEDs 16-31
567     */
568    protected void setDirect(boolean range2) {
569        if (addressField.getText() != null) {
570            try {
571                adr = inDomain(addressField.getText(), 65535);
572                int cv = 0x00;
573                // fetch the bits as set on the ledPanel
574                for (int j = 0; j < 16 ; j++) {
575                    cv += (((JCheckBox)ledPanel.getComponent(j)).isSelected() ? (1 << j) : 0);
576                    //log.debug("j={} cv={}", j, cv);
577                }
578                memo.getLnTrafficController().sendLocoNetMessage(LncvMessageContents.createDirectWriteRequest(adr, cv, range2));
579            } catch (NumberFormatException e) {
580                log.error("invalid entry, must be number");
581            }
582        } else {
583            statusText1.setText(Bundle.getMessage("FeedBackEnterArticle"));
584            addressField.setBackground(Color.RED);
585            return;
586        }
587        // stop and inform user
588        statusText1.setText(Bundle.getMessage("FeedBackSetDirect"));
589    }
590
591    private int inDomain(String entry, int max) {
592        int n = -1;
593        try {
594            n = Integer.parseInt(entry);
595        } catch (NumberFormatException e) {
596            log.error("invalid entry, must be number");
597        }
598        if ((0 <= n) && (n <= max)) {
599            return n;
600        } else {
601            statusText1.setText(Bundle.getMessage("FeedBackInputOutsideRange"));
602            return 0;
603        }
604    }
605
606    public void copyEntry(int art, int mod) {
607        if ((moduleProgRunning < 0) && !allProgRunning) { // protect locked fields while programming
608            articleField.setText(art + "");
609            addressField.setText(mod + "");
610        }
611    }
612
613    /**
614     * {@inheritDoc}
615     * Compare to {@link LnOpsModeProgrammer#message(jmri.jmrix.loconet.LocoNetMessage)}
616     *
617     * @param m a message received and analysed for LNCV characteristics
618     */
619    @Override
620    public synchronized void message(LocoNetMessage m) { // receive a LocoNet message and log it
621        // got a LocoNet message, see if it's an LNCV response
622        //log.debug("LncvProgPane heard message {}", m.toMonitorString());
623        if (LncvMessageContents.isSupportedLncvMessage(m)) {
624            // raw data, to display
625            String raw = (rawCheckBox.isSelected() ? ("[" + m + "] ") : "");
626            // format the message text, expect it to provide consistent \n after each line
627            String formatted = m.toMonitorString(memo.getSystemPrefix());
628            // copy the formatted data
629            reply += raw + formatted;
630        }
631        // or LACK write confirmation response from module?
632        if ((m.getOpCode() == LnConstants.OPC_LONG_ACK) &&
633                (m.getElement(1) == 0x6D)) { // elem 1 = OPC (matches 0xED), elem 2 = ack1
634            writeConfirmed = true;
635            if (m.getElement(2) == 0x7f) {
636                valueField.setBackground(Color.GREEN);
637                reply += Bundle.getMessage("LNCV_WRITE_CONFIRMED", moduleProgRunning) + "\n";
638            } else if (m.getElement(2) == 1) {
639                valueField.setBackground(Color.RED);
640                reply += Bundle.getMessage("LNCV_WRITE_CV_NOTSUPPORTED", moduleProgRunning, cv) + "\n";
641            } else if (m.getElement(2) == 2) {
642                valueField.setBackground(Color.RED);
643                reply += Bundle.getMessage("LNCV_WRITE_CV_READONLY", moduleProgRunning, cv) + "\n";
644            } else if (m.getElement(2) == 3) {
645                valueField.setBackground(Color.RED);
646                reply += Bundle.getMessage("LNCV_WRITE_CV_OUTOFBOUNDS", moduleProgRunning, val) + "\n";
647            }
648        }
649        if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_WRITE) {
650            reply += Bundle.getMessage("LNCV_WRITE_MOD_MONITOR", (moduleProgRunning == -1 ? "ALL" : moduleProgRunning)) + "\n";
651        }
652        if (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ) {
653            reply += Bundle.getMessage("LNCV_READ_MOD_MONITOR", (moduleProgRunning == -1 ? "ALL" : moduleProgRunning)) + "\n";
654        }
655        if ((LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY) ||
656                (LncvMessageContents.extractMessageType(m) == LncvMessageContents.LncvCommand.LNCV_READ_REPLY2)) {
657            // it's an LNCV ReadReply message, decode contents:
658            LncvMessageContents contents = new LncvMessageContents(m);
659            int msgArt = contents.getLncvArticleNum();
660            int msgAdr = moduleProgRunning;
661            int msgCv = contents.getCvNum();
662            int msgVal = contents.getCvValue();
663            if ((msgCv == 0) || (msgArt == art)) { // trust last used address. to be sure, check against Article (hardware class) number
664                msgAdr = msgVal; // if cvNum = 0, this is the LNCV module address
665            }
666            String foundMod = "(LNCV) " + Bundle.getMessage("LabelArticle") +  art + " "
667                    + Bundle.getMessage("LabelAddress") + msgAdr + " "
668                    + Bundle.getMessage("LabelCv") + msgCv + " "
669                    + Bundle.getMessage("LabelValue")+ msgVal + "\n";
670            reply += foundMod;
671            log.debug("ReadReply={}", reply);
672            // storing a Module in the list using the (first) write reply is handled by loconet.LncvDevicesManager
673
674            // enter returned CV in CVnum field
675            cvField.setText(msgCv + "");
676            cvField.setBackground(Color.WHITE);
677            // enter returned value in Value field
678            valueField.setText(msgVal + "");
679            valueField.setBackground(Color.WHITE);
680
681            LncvDevice dev = memo.getLncvDevicesManager().getDevice(art, adr);
682            if (dev != null) {
683                dev.setCvNum(msgCv);
684                dev.setCvValue(msgVal);
685            }
686            memo.getLncvDevicesManager().firePropertyChange("DeviceListChanged", true, false);
687        }
688
689        if (reply != null) { // we fool allProgFinished (copied from LNSV2 class)
690            allProgFinished(null);
691        }
692    }
693
694    /**
695     * AllProg Session callback.
696     *
697     * @param error feedback from Finish process
698     */
699    public void allProgFinished(String error) {
700        if (error != null) {
701             log.error("LNCV process finished with error: {}", error);
702             statusText2.setText(Bundle.getMessage("FeedBackDiscoverFail"));
703        } else {
704            synchronized (this) {
705                statusText2.setText(Bundle.getMessage("FeedBackDiscoverSuccess", lncvdm.getDeviceCount()));
706                result.setText(reply);
707            }
708        }
709    }
710
711    /**
712     * Give user feedback on closing of any open programming sessions when tool window is closed.
713     * @see #dispose() for actual closing of sessions
714     */
715    public void handleCloseEvent() {
716        //log.debug("handleCloseEvent() called in LncvProgPane");
717        if (allProgRunning || moduleProgRunning > 0) {
718            // adds a Don't remember again checkbox and stores setting in pm
719            // show dialog
720            if (pm != null && !pm.getSimplePreferenceState(dontWarnOnClose)) {
721                final JDialog dialog = new JDialog();
722                dialog.setTitle(Bundle.getMessage("ReminderTitle"));
723                dialog.setLocationRelativeTo(null);
724                dialog.setDefaultCloseOperation(javax.swing.JFrame.DISPOSE_ON_CLOSE);
725                JPanel container = new JPanel();
726                container.setBorder(BorderFactory.createEmptyBorder(10,10,10,10));
727                container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS));
728
729                JLabel question = new JLabel(Bundle.getMessage("DialogRunningWarning"), JLabel.CENTER);
730                question.setAlignmentX(Component.CENTER_ALIGNMENT);
731                container.add(question);
732
733                JButton okButton = new JButton(Bundle.getMessage("ButtonOK"));
734                JPanel buttons = new JPanel();
735                buttons.setAlignmentX(Component.CENTER_ALIGNMENT);
736                buttons.add(okButton);
737                container.add(buttons);
738
739                final JCheckBox remember = new JCheckBox(Bundle.getMessage("DontRemind"));
740                remember.setAlignmentX(Component.CENTER_ALIGNMENT);
741                remember.setFont(remember.getFont().deriveFont(10f));
742                container.add(remember);
743
744                okButton.addActionListener(e -> {
745                    if ((remember.isSelected()) && (pm != null)) {
746                        pm.setSimplePreferenceState(dontWarnOnClose, remember.isSelected());
747                    }
748                    dialog.dispose();
749                });
750
751
752                dialog.getContentPane().add(container);
753                dialog.pack();
754                dialog.setModal(true);
755                dialog.setVisible(true);
756            }
757
758            // dispose will take care of actually stopping any open prog session
759        }
760    }
761
762    /**
763     * {@inheritDoc}
764     */
765    @Override
766    public void dispose() {
767        if (memo != null && memo.getLnTrafficController() != null) {
768            // disconnect from the LnTrafficController, normally attached/detached after Discovery completed
769            memo.getLnTrafficController().removeLocoNetListener(~0, this);
770        }
771        // and unwind swing
772        if (pm != null) {
773            pm.setSimplePreferenceState(rawDataCheck, rawCheckBox.isSelected());
774        }
775        // prevent closing LNCV tool with programming session left open on module(s).
776        if (moduleProgRunning >= 0) {
777            modProgButtonActionPerformed();
778        }
779        if (allProgRunning) {
780            allProgButtonActionPerformed();
781        }
782        super.setVisible(false);
783
784        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((tpm) -> {
785            synchronized (this) {
786                tpm.stopPersisting(moduleTable);
787            }
788        });
789
790        super.dispose();
791    }
792
793    /**
794     * Testing methods.
795     *
796     * @return text currently in Article field
797     */
798    protected String getArticleEntry() {
799        if (!articleField.isEditable()) {
800            return "locked";
801        } else {
802            return articleField.getText();
803        }
804    }
805
806    protected String getAddressEntry() {
807        if (!addressField.isEditable()) {
808            return "locked";
809        } else {
810            return addressField.getText();
811        }
812    }
813
814    protected synchronized String getMonitorContents(){
815            return reply;
816    }
817
818    protected void setCvFields(int cvNum, int cvVal) {
819        cvField.setText(""+cvNum);
820        if (cvVal > -1) {
821            valueField.setText("" + cvVal);
822        } else {
823            valueField.setText("");
824        }
825    }
826
827    protected synchronized LncvDevice getModule(int i) {
828        if (lncvdm == null) {
829            lncvdm = memo.getLncvDevicesManager();
830        }
831        log.debug("lncvdm.getDeviceCount()={}", lncvdm.getDeviceCount());
832        if (i > -1 && i < lncvdm.getDeviceCount()) {
833            return lncvdm.getDeviceList().getDevice(i);
834        } else {
835            log.debug("getModule({}) failed", i);
836            return null;
837        }
838    }
839
840    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LncvProgPane.class);
841
842}