001package jmri.jmrix.loconet.swing.lnsv1prog;
002
003import jmri.InstanceManager;
004import jmri.UserPreferencesManager;
005import jmri.jmrit.beantable.EnablingCheckboxRenderer;
006import jmri.jmrix.loconet.*;
007import jmri.jmrix.loconet.lnsvf1.Lnsv1Device;
008import jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents;
009import jmri.swing.JTablePersistenceManager;
010import jmri.util.swing.JmriJOptionPane;
011import jmri.util.table.ButtonEditor;
012import jmri.util.table.ButtonRenderer;
013
014import javax.swing.*;
015import javax.swing.border.Border;
016import javax.swing.table.TableRowSorter;
017import java.awt.*;
018
019/**
020 * Frame for discovery and display of LocoNet LNSVf1 boards, e.g. LocoIO.
021 * Derived from lncvprog. Verified with HDL and GCA hardware.
022 * <p>
023 * Some of the message formats used in this class are Copyright Digitrax
024 * and used with permission as part of the JMRI project. That permission does
025 * not extend to uses in other software products. If you wish to use this code,
026 * algorithm or these message formats outside of JMRI, please contact Digitrax.
027 * <p>
028 * Buttons in table row allow to add roster entry for device, and switch to the
029 * DecoderPro ops mode programmer.
030 *
031 * @author Egbert Broerse Copyright (C) 2021, 2022, 2025
032 */
033public class Lnsv1ProgPane extends jmri.jmrix.loconet.swing.LnPanel implements LocoNetListener {
034
035    private LocoNetSystemConnectionMemo memo;
036    protected JButton probeAllButton = new JButton();
037    protected JButton setAllAddressButton = new JButton();
038    protected JButton readButton = new JButton(Bundle.getMessage("ButtonRead"));
039    protected JButton writeButton = new JButton(Bundle.getMessage("ButtonWrite"));
040    protected JTextField addressField = new JTextField(4);
041    protected JTextField subAddressField = new JTextField(4);
042    protected JTextField svField = new JTextField(4);
043    protected JTextField valueField = new JTextField(4);
044    protected JCheckBox rawCheckBox = new JCheckBox(Bundle.getMessage("ButtonShowRaw"));
045    protected JCheckBox decimalCheckBox = new JCheckBox(Bundle.getMessage("ButtonShowDecimal"));
046    protected JTable moduleTable = null;
047    protected Lnsv1ProgTableModel moduleTableModel = null;
048    public static final int ROW_HEIGHT = (new JButton("X").getPreferredSize().height)*9/10;
049
050    protected JPanel tablePanel = null;
051    protected JLabel statusText1 = new JLabel();
052    protected JLabel statusText2 = new JLabel();
053    protected JLabel sepFieldLabel = new JLabel("/", JLabel.RIGHT);
054    protected JLabel addressFieldLabel = new JLabel(Bundle.getMessage("LabelModuleAddress", JLabel.RIGHT));
055    protected JLabel svFieldLabel = new JLabel(Bundle.getMessage("LabelSv"), JLabel.RIGHT);
056    protected JLabel valueFieldLabel = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("HeadingValue")), JLabel.RIGHT);
057    protected JTextArea result = new JTextArea(6,50);
058    protected String reply = "";
059    protected int addr;
060    protected int subAddr;
061    protected int sv = 0;
062    protected int val;
063    boolean writeConfirmed = false;
064    private final String rawDataCheck = this.getClass().getName() + ".RawData"; // NOI18N
065    private final String decimalDataCheck = this.getClass().getName() + ".DecimalData"; // NOI18N
066    private UserPreferencesManager pm;
067    private transient TableRowSorter<Lnsv1ProgTableModel> sorter;
068    private Lnsv1DevicesManager lnsv1dm;
069
070    /**
071     * Constructor method
072     */
073    public Lnsv1ProgPane() {
074        super();
075    }
076
077    /**
078     * {@inheritDoc}
079     */
080    @Override
081    public String getHelpTarget() {
082        return "package.jmri.jmrix.loconet.swing.lnsv1prog.Lnsv1ProgPane"; // NOI18N
083    }
084
085    @Override
086    public String getTitle() {
087        return Bundle.getMessage("MenuItemLnsv1Prog");
088    }
089
090    /**
091     * Initialize the config window
092     */
093    @Override
094    public void initComponents() {
095        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
096        // buttons at top, like SE8c pane
097        add(initButtonPanel()); // requires presence of memo.
098        add(initStatusPanel()); // positioned after ButtonPanel so to keep it simple also delayed
099        // creation of table must wait for memo + tc to be available, see initComponents(memo) next
100    }
101
102    @Override
103    public synchronized void initComponents(LocoNetSystemConnectionMemo memo) {
104        super.initComponents(memo);
105        this.memo = memo;
106        lnsv1dm = memo.getLnsv1DevicesManager();
107        pm = InstanceManager.getDefault(UserPreferencesManager.class);
108        // connect to the LnTrafficController
109        if (memo.getLnTrafficController() == null) {
110            log.error("No traffic controller is available");
111        } else {
112            // add listener
113            memo.getLnTrafficController().addLocoNetListener(~0, this);
114        }
115
116        // create the data model and its table
117        moduleTableModel = new Lnsv1ProgTableModel(this, memo);
118        moduleTable = new JTable(moduleTableModel);
119        moduleTable.setRowSelectionAllowed(false);
120        moduleTable.setPreferredScrollableViewportSize(new Dimension(300, 200));
121        moduleTable.setRowHeight(ROW_HEIGHT);
122        moduleTable.setDefaultEditor(JButton.class, new ButtonEditor(new JButton()));
123        moduleTable.setDefaultRenderer(JButton.class, new ButtonRenderer());
124        moduleTable.setDefaultRenderer(Boolean.class, new EnablingCheckboxRenderer());
125        moduleTable.setRowSelectionAllowed(true);
126        moduleTable.getSelectionModel().addListSelectionListener(event -> {
127            synchronized (this) {
128                if (moduleTable.getSelectedRow() > -1 && moduleTable.getSelectedRow() < moduleTable.getRowCount()) {
129                    // copy composite board address, svNuma and value from selected row
130                    copyEntrytoFields((int) moduleTable.getValueAt(moduleTable.getSelectedRow(), Lnsv1ProgTableModel.MODADDR_COLUMN));
131                    setCvFields((int) moduleTable.getValueAt(moduleTable.getSelectedRow(), Lnsv1ProgTableModel.CV_COLUMN),
132                            (int) moduleTable.getValueAt(moduleTable.getSelectedRow(), Lnsv1ProgTableModel.VALUE_COLUMN));
133                }
134            }
135        });
136        // establish row sorting for the table
137        sorter = new TableRowSorter<>(moduleTableModel);
138        moduleTable.setRowSorter(sorter);
139         // establish table physical characteristics persistence
140        moduleTable.setName("LNSV1 Device Management"); // NOI18N
141        // Reset and then persist the table's ui state
142        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((tpm) -> {
143            synchronized (this) {
144                tpm.resetState(moduleTable);
145                tpm.persist(moduleTable, true);
146            }
147        });
148
149        JScrollPane tableScrollPane = new JScrollPane(moduleTable);
150        tablePanel = new JPanel();
151        Border resultBorder = BorderFactory.createEtchedBorder();
152        Border resultTitled = BorderFactory.createTitledBorder(resultBorder, Bundle.getMessage("Lnsv1TableTitle"));
153        tablePanel.setBorder(resultTitled);
154        tablePanel.setLayout(new BoxLayout(tablePanel, BoxLayout.Y_AXIS));
155        tablePanel.add(tableScrollPane);
156
157        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tablePanel, getMonitorPanel());
158        splitPane.setOneTouchExpandable(true);
159        splitPane.setAlignmentX(Component.CENTER_ALIGNMENT);
160        splitPane.setBorder(BorderFactory.createEmptyBorder());
161        add(splitPane);
162
163        rawCheckBox.setSelected(pm.getSimplePreferenceState(rawDataCheck));
164        decimalCheckBox.setSelected(pm.getSimplePreferenceState(decimalDataCheck));
165
166        // Probe when ready
167        probeAllButtonActionPerformed();
168    }
169
170    /*
171     * Initialize the LNSV1 Monitor panel.
172     */
173    protected JPanel getMonitorPanel() {
174        JPanel panel3 = new JPanel();
175        panel3.setLayout(new BoxLayout(panel3, BoxLayout.Y_AXIS));
176
177        JPanel panel31 = new JPanel();
178        panel31.setLayout(new BoxLayout(panel31, BoxLayout.Y_AXIS));
179        JScrollPane resultScrollPane = new JScrollPane(result);
180        panel31.add(resultScrollPane);
181
182        JPanel panel31b = new JPanel();
183        panel31b.setLayout(new BoxLayout(panel31b, BoxLayout.X_AXIS));
184        panel31b.add(rawCheckBox);
185        rawCheckBox.setVisible(true);
186        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw"));
187        panel31b.add(decimalCheckBox);
188        decimalCheckBox.setVisible(true);
189        decimalCheckBox.setToolTipText(Bundle.getMessage("TooltipShowDecimal"));
190        panel31.add(panel31b);
191
192        panel3.add(panel31);
193        Border panel3Border = BorderFactory.createEtchedBorder();
194        Border panel3Titled = BorderFactory.createTitledBorder(panel3Border, Bundle.getMessage("Lnsv1MonitorTitle"));
195        panel3.setBorder(panel3Titled);
196        return panel3;
197    }
198
199    /*
200     * Initialize the Button panel. Requires presence of memo to send and receive.
201     */
202    protected JPanel initButtonPanel() {
203        // Set up buttons and entry fields
204        JPanel panel4 = new JPanel();
205        panel4.setLayout(new BoxLayout(panel4, BoxLayout.X_AXIS));
206        panel4.add(Box.createHorizontalGlue()); // this will expand/contract
207
208        JPanel panel41 = new JPanel();
209        panel41.setLayout(new BoxLayout(panel41, BoxLayout.PAGE_AXIS));
210        probeAllButton.setText(Bundle.getMessage("ButtonProbe"));
211        probeAllButton.setToolTipText(Bundle.getMessage("TipProbeAllButton"));
212        probeAllButton.addActionListener(e -> probeAllButtonActionPerformed());
213        panel41.add(probeAllButton);
214
215        setAllAddressButton.setText(Bundle.getMessage("ButtonSetModuleAddress"));
216        setAllAddressButton.setToolTipText(Bundle.getMessage("TipSetModuleAddrButton"));
217        setAllAddressButton.addActionListener(e -> setAllAddressButtonActionPerformed());
218        panel41.add(setAllAddressButton);
219        panel4.add(panel41);
220
221        JPanel panel42 = new JPanel();
222        panel42.setLayout(new BoxLayout(panel42, BoxLayout.PAGE_AXIS));
223
224        JPanel panel421 = new JPanel(); // default FlowLayout
225        panel421.add(addressFieldLabel);
226        // entry field (decimal)
227        JPanel panel4211 = new JPanel(); // keep entry fields together
228        addressField.setToolTipText(Bundle.getMessage("TipModuleAddrEntry"));
229        panel4211.add(addressField); // entry field (decimal) for Module Low Address
230        panel4211.add(sepFieldLabel); // holds the slash between base and subaddress
231        subAddressField.setToolTipText(Bundle.getMessage("TipModuleSubaddrEntry"));
232        panel4211.add(subAddressField); // entry field (decimal) for Module Subaddress
233        panel421.add(panel4211);
234        panel42.add(panel421);
235        panel4.add(panel42);
236
237        JPanel panel43 = new JPanel(); // CV num + value
238        Border panel43Border = BorderFactory.createEtchedBorder();
239        panel43.setBorder(panel43Border);
240        panel43.setLayout(new BoxLayout(panel43, BoxLayout.LINE_AXIS));
241
242        JPanel panel431 = new JPanel(); // labels
243        panel431.setLayout(new BoxLayout(panel431, BoxLayout.PAGE_AXIS));
244        svFieldLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
245        svFieldLabel.setMinimumSize(new Dimension(60, new JTextField("X").getHeight() + 5));
246        panel431.add(svFieldLabel);
247
248        valueFieldLabel.setAlignmentX(Component.RIGHT_ALIGNMENT);
249        valueFieldLabel.setMinimumSize(new Dimension(60, new JTextField("X").getHeight() + 5));
250        panel431.add(valueFieldLabel);
251        panel43.add(panel431);
252
253        JPanel panel432 = new JPanel(); // entry fields
254        panel432.setMaximumSize(new Dimension(50, 50));
255        panel432.setPreferredSize(new Dimension(50, 50));
256        panel432.setMinimumSize(new Dimension(50, 50));
257        panel432.setLayout(new BoxLayout(panel432, BoxLayout.PAGE_AXIS));
258        panel432.add(svField); // entry field (decimal) for SV number to read/write
259        panel432.add(valueField); // entry field (decimal) for CV value
260        panel43.add(panel432);
261
262        JPanel panel433 = new JPanel(); // read/write buttons
263        panel433.setLayout(new BoxLayout(panel433, BoxLayout.PAGE_AXIS));
264        panel433.add(readButton);
265        readButton.setEnabled(true);
266        readButton.addActionListener(e -> readButtonActionPerformed());
267
268        panel433.add(writeButton);
269        writeButton.setEnabled(false); // disabled button, to write we point to Roster in button tooltip
270        writeButton.addActionListener(e -> writeButtonActionPerformed());
271        writeButton.setToolTipText(Bundle.getMessage("ButtonWriteInactiveTip"));
272        panel43.add(panel433);
273        panel4.add(panel43);
274
275        panel4.add(Box.createHorizontalGlue()); // this will expand/contract
276        panel4.setAlignmentX(Component.CENTER_ALIGNMENT);
277
278        return panel4;
279    }
280
281    /*
282     * Initialize the Status panel.
283     */
284    protected JPanel initStatusPanel() {
285        JPanel panel2 = new JPanel();
286        panel2.setLayout(new BoxLayout(panel2, BoxLayout.PAGE_AXIS));
287
288        statusText1.setText("   ");
289        statusText1.setHorizontalAlignment(JLabel.CENTER);
290        panel2.add(statusText1);
291
292        statusText2.setText("   ");
293        statusText2.setHorizontalAlignment(JLabel.CENTER);
294        panel2.add(statusText2);
295
296        panel2.setAlignmentX(Component.CENTER_ALIGNMENT);
297        return panel2;
298    }
299
300    /**
301     * PROBE button.
302     */
303    public void probeAllButtonActionPerformed() {
304        // send probeAll command onto LocoNet
305        statusText1.setText(Bundle.getMessage("FeedBackProbing"));
306        probeAllButton.setText(Bundle.getMessage("ButtonProbing"));
307        LocoNetMessage m = Lnsv1MessageContents.createBroadcastProbeAll();
308        memo.getLnTrafficController().sendLocoNetMessage(m);
309        // wait for replies
310        try {
311            Thread.sleep(200);
312        } catch (InterruptedException e) {
313            Thread.currentThread().interrupt(); // retain if needed later
314            return; // interrupt kills the thread
315        }
316        statusText1.setText(Bundle.getMessage("FeedBackProbingStop"));
317        probeAllButton.setText(Bundle.getMessage("ButtonProbe"));
318    }
319
320    // MODULE_SET_ADDRESS button
321    /**
322     * Write SV1 and (optionally) SV2.
323     */
324    public void setAllAddressButtonActionPerformed() {
325        addressField.setBackground(Color.WHITE);
326        subAddressField.setBackground(Color.WHITE);
327        if (addressField.getText().isEmpty() || subAddressField.getText().isEmpty()) {
328            statusText1.setText(Bundle.getMessage("FeedBackEnterHiLoAddress"));
329            if (addressField.getText().isEmpty()) {
330                addressField.setBackground(Color.RED);
331            } else {
332                subAddressField.setBackground(Color.RED);
333            }
334            setAllAddressButton.setSelected(false);
335            return;
336        }
337        // show dialog to protect unwanted ALL messages
338        Object[] dialogBoxButtonOptions = {
339                Bundle.getMessage("ButtonProceed"),
340                Bundle.getMessage("ButtonCancel")};
341        int userReply = JmriJOptionPane.showOptionDialog(this.getParent(),
342                Bundle.getMessage("DialogAllLnsv1Warning"),
343                Bundle.getMessage("WarningTitle"),
344                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE,
345                null, dialogBoxButtonOptions, dialogBoxButtonOptions[1]);
346        if (userReply != 0 ) { // not array position 0 ButtonProceed
347            return;
348        }
349        if ((!addressField.getText().isEmpty()) && (!subAddressField.getText().isEmpty())) {
350            try {
351                addr = inDomain(addressField.getText(), 1, 127); // goes in DST_L as module low address
352                subAddr = inDomain(subAddressField.getText(), 1,127); // goes in d5 as module high address
353                // check & warn for reserved LocoBuffer 0x50/0d80 address
354                if (addr == 0x50) {
355                    locoBufferReservedAddress();
356                    return;
357                }
358                setAllAddressButton.setEnabled(false);
359                statusText1.setText(Bundle.getMessage("FeedBackModAddrStart", addr, subAddr));
360                addressField.setEditable(false); // lock addressL & H fields to prevent accidentally changing it
361                subAddressField.setEditable(false);
362                LocoNetMessage[] messageArray = Lnsv1MessageContents.createBroadcastSetAddress(addr, subAddr);
363                // send Lnsv1 broadcast write address command(s) onto LocoNet
364                for (LocoNetMessage m : messageArray) {
365                    if (m != null) {
366                        memo.getLnTrafficController().sendLocoNetMessage(m);
367                    }
368                }
369                // wait a second for replies
370                try {
371                    Thread.sleep(500);
372                } catch (InterruptedException e) {
373                    Thread.currentThread().interrupt(); // retain if needed later
374                    return; // interrupt kills the thread
375                }
376                setAllAddressButton.setEnabled(true);
377                addressField.setEditable(true); // unlock addressL fields
378                subAddressField.setEditable(true);
379            } catch (NumberFormatException e) {
380                statusText1.setText(Bundle.getMessage("FeedBackEnterHiLoAddress"));
381                log.error("invalid entry, both must be numbers");
382            }
383        }
384    }
385
386    // READ_SV button
387    /**
388     * Handle Read CV button, assemble LNSVf1 read message. Requires presence of memo.
389     */
390    public void readButtonActionPerformed() {
391        addressField.setBackground(Color.WHITE);
392        subAddressField.setBackground(Color.WHITE);
393        svField.setBackground(Color.WHITE);
394        if (addressField.getText().isEmpty() || subAddressField.getText().isEmpty()) {
395            statusText1.setText(Bundle.getMessage("FeedBackEnterHiLoAddress"));
396            if (addressField.getText().isEmpty()) {
397                addressField.setBackground(Color.RED);
398            } else {
399                subAddressField.setBackground(Color.RED);
400            }
401            setAllAddressButton.setSelected(false);
402            return;
403        }
404        if (svField.getText().isEmpty()) {
405            svField.setBackground(Color.RED);
406            statusText1.setText(Bundle.getMessage("FeedBackEnterSv"));
407            return;
408        }
409        try {
410            addr = inDomain(addressField.getText(), 1,127); // goes in DST_L, used as module base address
411            subAddr = inDomain(subAddressField.getText(), 1,127); // goes in D5, used as module subaddress
412            // check & warn for reserved LocoBuffer 0x50/0d80 address
413            if (addr == 0x50) {
414                locoBufferReservedAddress();
415                return;
416            }
417            sv = inDomain(svField.getText(), 0,127); // decimal entry
418            log.debug("ReadButtonPressed adrL={}, sub={}, sv={}", addr, subAddr, sv);
419            LocoNetMessage m = Lnsv1MessageContents.createSv1ReadRequest(addr, subAddr, sv);
420            memo.getLnTrafficController().sendLocoNetMessage(m);
421        } catch (NumberFormatException e) {
422            statusText1.setText(Bundle.getMessage("FeedBackEnterNumbers"));
423            log.error("invalid entry, must be numbers");
424        }
425        // stop and inform user
426        statusText1.setText(Bundle.getMessage("FeedBackRead", "LNSV1"));
427    }
428
429    // WriteCV button
430    /**
431     * Handle Write button click, assemble LNSVf1 write message. Requires presence of memo.
432     */
433    public void writeButtonActionPerformed() {
434        if (addressField.getText() != null && subAddressField.getText() != null
435                && (svField.getText() != null) && (valueField.getText() != null)) {
436            try {
437                addr = inDomain(addressField.getText(), 1,0x7F); // goes in DST_L as module low address
438                subAddr = inDomain(subAddressField.getText(), 1,0x7F); // goes in d5 as module high address
439                // check & warn for reserved LocoBuffer 0x50/0d80 address
440                if (addr == 0x50) {
441                    locoBufferReservedAddress();
442                    return;
443                }
444                sv = inDomain(svField.getText(), 1,0x7F); // decimal entry
445                val = inDomain(valueField.getText(), 0,0x7F); // decimal entry
446                if (sv == 100 || sv == 80) {
447                    // reserved general module address, warn in status and abort
448                    statusText1.setText(Bundle.getMessage("FeedBackValidAddressRange"));
449                    valueField.setBackground(Color.RED);
450                    return;
451                }
452                writeConfirmed = false;
453                LocoNetMessage m = Lnsv1MessageContents.createSv1WriteRequest(addr, subAddr, sv, val);
454                memo.getLnTrafficController().sendLocoNetMessage(m);
455                valueField.setBackground(Color.ORANGE);
456            } catch (NumberFormatException e) {
457                statusText1.setText(Bundle.getMessage("FeedBackEnterNumbers"));
458                log.error("invalid entry, must be numbers");
459            }
460        } else {
461            statusText1.setText(Bundle.getMessage("FeedBackEnterHiLoAddress"));
462            return;
463        }
464        // stop and inform user
465        statusText1.setText(Bundle.getMessage("FeedBackWrite", "LNSV1"));
466    }
467
468    private int inDomain(String entry, int min, int max) {
469        int n = -1;
470        try {
471            n = Integer.parseInt(entry);
472        } catch (NumberFormatException e) {
473            log.error("invalid entry, must be number");
474        }
475        if ((min <= n) && (n <= max)) {
476            return n;
477        } else {
478            statusText1.setText(Bundle.getMessage("FeedBackInputOutsideRange"));
479            return 0;
480        }
481    }
482
483    public void copyEntrytoFields(int adr) {
484        addressField.setText((adr & 0x7F) + "");
485        subAddressField.setText((((adr >> 8) & 0x7F) + 1) + "");
486    }
487
488    /**
489     * Show dialog to warn that address 0x50 is reserved and invalid entry in LNSV1 pane
490     */
491    private void locoBufferReservedAddress() {
492        Object[] dialogBoxButtonOptions = {
493                Bundle.getMessage("ButtonOK")};
494        JmriJOptionPane.showOptionDialog(this.getParent(),
495                Bundle.getMessage("DialogWarnLbReserved"),
496                Bundle.getMessage("WarningTitle"),
497                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.WARNING_MESSAGE,
498                null, dialogBoxButtonOptions, dialogBoxButtonOptions[0]);
499    }
500
501    /**
502     * {@inheritDoc}
503     * See jmri.jmrix.loconet.lnsvf1.Lnsv1MessageContents.toString().
504     * Compare to {@link LnOpsModeProgrammer#message(LocoNetMessage)}.
505     * We pick up the SV + value add some details to status line (LnMonitor shows on our pane too).
506     * @param m a message received and analysed for LNSVf1 characteristics
507     */
508    @Override
509    public synchronized void message(LocoNetMessage m) { // receive a LocoNet message and log it to the monitor
510        // got a LocoNet message, see if it's an LNSV1 response
511        if (Lnsv1MessageContents.isSupportedSv1Message(m)) {
512            // raw data, to display
513            String raw = (rawCheckBox.isSelected() ? ("[" + m + "] ") : "");
514            // format the message text, expect it to provide consistent \n after each line
515            String formatted = m.toMonitorString(memo.getSystemPrefix());
516            // copy the formatted data
517            reply += raw + formatted;
518        } else {
519            log.debug("Rejected by isSupportedSv1Message");
520            return;
521        }
522        // use dec checkbox state to choose either LnMonitor by loconet.messageinterp
523        //  using Integer.toHexString(i) and/or String.format("0x%02X", i))
524        boolean addDec = decimalCheckBox.isSelected();
525        Lnsv1MessageContents contents = new Lnsv1MessageContents(m);
526        // Use Programmer to (read and) write individual SV's - best done via Ports tab sheet
527
528        if (Lnsv1MessageContents.extractMessageType(m) == Lnsv1MessageContents.Sv1Command.SV1_WRITE) {
529            // it's an LNSV1 WriteReply message, decode contents:
530            log.debug("SV1_WRITE decode contents");
531            if (contents.getSrcL() == 0x50) {
532                if (contents.getDstL() == 0x00) {
533                    log.debug("Write all from LocoBuffer/PC");
534                    if (addDec) {
535                        reply += Bundle.getMessage("SV1_WRITE_ALL_INTERPRETED_DEC",
536                                contents.getSvNum(),
537                                contents.getSv1D4()); // NOI18N
538                    }
539                } else { // write request from LocoBuffer
540                    log.debug("Write request from LocoBuffer/PC");
541                    if (addDec) {
542                        reply += Bundle.getMessage("SV1_WRITE_INTERPRETED_DEC",
543                                contents.getSrcL(),
544                                contents.getSubAddress(),
545                                contents.getSvNum(),
546                                contents.getSv1D4()); // NOI18N
547                    }
548                }
549            } else {
550                // Write Reply from LocoIO
551                log.debug("Write Reply from LocoIO");
552                if (addDec) {
553                    reply += Bundle.getMessage("SV1_WRITE_REPLY_INTERPRETED_DEC",
554                            contents.getSrcL(),
555                            contents.getSubAddress(),
556                            contents.getSvNum(),
557                            contents.getSv1D8()); // NOI18N
558                }
559            }
560        }
561        if (Lnsv1MessageContents.extractMessageType(m) == Lnsv1MessageContents.Sv1Command.SV1_READ) {
562            // it's an LNSV1 ReadReply message, decode contents:
563            log.debug("SV1_READ decode contents");
564            if (contents.getSrcL() == 0x50) {
565                log.debug("Read request from LocoBuffer/PC"); // nothing else to do
566                if (addDec) {
567                    reply += Bundle.getMessage("SV1_READ_INTERPRETED_DEC",
568                            contents.getDstL(),
569                            contents.getSubAddress(),
570                            contents.getSvNum()); // NOI18N
571                }
572            } else {
573                // Read Reply from LocoIO
574                log.debug("Read Reply from LocoIO");
575                if (addDec) {
576                    reply += Bundle.getMessage("SV1_READ_REPLY_INTERPRETED_DEC",
577                            contents.getSrcL(),
578                            contents.getSubAddress(),
579                            contents.getSvNum(),
580                            contents.getSv1D6(),
581                            contents.getSv1D7(),
582                            contents.getSv1D8()); // NOI18N
583                }
584                // storing a Module in the list using the (first) write reply is handled by loconet.Lnsv1DevicesManager
585                // store in Value field if module address matches
586                if (("" + contents.getSrcL()).equals(addressField.getText()) &&
587                        ("" + contents.getSubAddress()).equals(subAddressField.getText()) &&
588                        ("" + contents.getSvNum()).equals(svField.getText())) {
589                    valueField.setText("" + contents.getSvValue());
590                }
591                // store the (last read) cvNum and cvValue to the Lnsv1Device
592                Lnsv1Device dev = memo.getLnsv1DevicesManager().getDevice(addr, subAddr);
593                if (dev != null) {
594                    dev.setCvNum(contents.getSvNum());
595                    dev.setCvValue(contents.getSv1D4());
596                }
597                memo.getLnsv1DevicesManager().firePropertyChange("DeviceListChanged", true, false);
598            }
599        }
600
601        if (reply != null) { // we fool allProgFinished (copied from LNSV2 class)
602            allProgFinished(null);
603        }
604    }
605
606    /**
607     * AllProg Session callback.
608     *
609     * @param error feedback from Finish process
610     */
611    public void allProgFinished(String error) {
612        if (error != null) {
613             log.error("LNSV1 process finished with error: {}", error);
614             statusText2.setText(Bundle.getMessage("FeedBackDiscoverFail"));
615        } else {
616            synchronized (this) {
617                if (lnsv1dm.getDeviceCount() == 1) {
618                    statusText2.setText(Bundle.getMessage("FeedBackDiscoverSuccessOne"));
619                } else {
620                    statusText2.setText(Bundle.getMessage("FeedBackDiscoverSuccess", lnsv1dm.getDeviceCount()));
621                }
622                result.setText(reply);
623            }
624        }
625    }
626
627    /**
628     * {@inheritDoc}
629     */
630    @Override
631    public void dispose() {
632        if (memo != null && memo.getLnTrafficController() != null) {
633            // disconnect from the LnTrafficController, normally attached/detached after Discovery completed
634            memo.getLnTrafficController().removeLocoNetListener(~0, this);
635        }
636        // and unwind swing
637        if (pm != null) {
638            pm.setSimplePreferenceState(rawDataCheck, rawCheckBox.isSelected());
639            pm.setSimplePreferenceState(decimalDataCheck, decimalCheckBox.isSelected());
640        }
641        super.setVisible(false);
642
643        InstanceManager.getOptionalDefault(JTablePersistenceManager.class).ifPresent((tpm) -> {
644            synchronized (this) {
645                tpm.stopPersisting(moduleTable);
646            }
647        });
648
649        super.dispose();
650    }
651
652    // Testing methods
653
654    protected synchronized String getMonitorContents(){
655            return reply;
656    }
657
658    protected void setCvFields(int cvNum, int cvVal) {
659        svField.setText(""+cvNum);
660        if (cvVal > -1) {
661            valueField.setText("" + cvVal);
662        } else {
663            valueField.setText("");
664        }
665    }
666
667    protected synchronized Lnsv1Device getModule(int i) {
668        if (lnsv1dm == null) {
669            lnsv1dm = memo.getLnsv1DevicesManager();
670        }
671        //log.debug("lncvdm.getDeviceCount()={}", lnsv1dm.getDeviceCount());
672        if (i > -1 && i < lnsv1dm.getDeviceCount()) {
673            return lnsv1dm.getDeviceList().getDevice(i);
674        } else {
675            log.debug("getModule({}) failed", i);
676            return null;
677        }
678    }
679
680    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Lnsv1ProgPane.class);
681
682}