001package jmri.jmrix.nce.consist;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.BufferedWriter;
006import java.io.File;
007import java.io.FileWriter;
008import java.io.IOException;
009import java.io.PrintWriter;
010
011import java.text.MessageFormat;
012import javax.swing.JFileChooser;
013import javax.swing.JLabel;
014import javax.swing.JPanel;
015
016import jmri.jmrix.nce.NceBinaryCommand;
017import jmri.jmrix.nce.NceMessage;
018import jmri.jmrix.nce.NceReply;
019import jmri.jmrix.nce.NceTrafficController;
020import jmri.util.FileUtil;
021import jmri.util.StringUtil;
022import jmri.util.swing.JmriJOptionPane;
023import jmri.util.swing.TextFilter;
024
025/**
026 * Backups NCE Consists to a text file format defined by NCE.
027 * <p>
028 * NCE "Backup consists" dumps the consists into a text file. The consists data
029 * are stored in the NCE CS starting at xF500 and ending at xFAFF.
030 * <p>
031 * NCE file format:
032 * <p>
033 * :F500 (16 bytes per line, grouped as 8 words with space delimiters) :F510 . .
034 * :FAF0 :0000
035 * <p>
036 * Consist data byte:
037 * <p>
038 * bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
039 * <p>
040 * This backup routine uses the same consist data format as NCE.
041 *
042 * @author Dan Boudreau Copyright (C) 2007
043 * @author Ken Cameron Copyright (C) 2023
044 */
045public class NceConsistBackup extends Thread implements jmri.jmrix.nce.NceListener {
046
047    private static final int CONSIST_LNTH = 16; // 16 bytes per line
048    private int replyLen = 0; // expected byte length
049    private int waiting = 0; // to catch responses not intended for this module
050    private boolean fileValid = false; // used to flag backup status messages
051
052    private final byte[] nceConsistData = new byte[CONSIST_LNTH];
053
054    JLabel textConsist = new JLabel();
055    JLabel consistNumber = new JLabel();
056
057    private NceTrafficController tc = null;
058    private int workingNumConsists = -1;
059
060    public NceConsistBackup(NceTrafficController t) {
061        tc = t;
062        workingNumConsists = tc.csm.getConsistMax();
063    }
064
065    @Override
066    public void run() {
067
068        // get file to write to
069        JFileChooser fc = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
070        fc.addChoosableFileFilter(new TextFilter());
071
072        File fs = new File("NCE consist backup.txt"); // NOI18N
073        fc.setSelectedFile(fs);
074
075        int retVal = fc.showSaveDialog(null);
076        if (retVal != JFileChooser.APPROVE_OPTION) {
077            return; // Canceled
078        }
079        if (fc.getSelectedFile() == null) {
080            return; // Canceled
081        }
082        File f = fc.getSelectedFile();
083        if (fc.getFileFilter() != fc.getAcceptAllFileFilter()) {
084            // append .txt to file name if needed
085            String fileName = f.getAbsolutePath();
086            String fileNameLC = fileName.toLowerCase();
087            if (!fileNameLC.endsWith(".txt")) {
088                fileName = fileName + ".txt";
089                f = new File(fileName);
090            }
091        }
092        if (f.exists()) {
093            if (JmriJOptionPane.showConfirmDialog(null,
094                    MessageFormat.format(Bundle.getMessage("FileExists"), f.getName()),
095                    Bundle.getMessage("OverwriteFile"),
096                    JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) {
097                return;
098            }
099        }
100
101        try (PrintWriter fileOut = new PrintWriter(new BufferedWriter(new FileWriter(f)),
102                true)) {
103
104            if (JmriJOptionPane.showConfirmDialog(null,
105                    Bundle.getMessage("BackupTakesAwhile"),
106                    Bundle.getMessage("NceConsistBackup"),
107                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
108                fileOut.close();
109                return;
110            }
111
112            // create a status frame
113            JPanel ps = new JPanel();
114            jmri.util.JmriJFrame fstatus = new jmri.util.JmriJFrame(Bundle.getMessage("NceConsistBackup"));
115            fstatus.setLocationRelativeTo(null);
116            fstatus.setSize(300, 100);
117            fstatus.getContentPane().add(ps);
118
119            ps.add(textConsist);
120            ps.add(consistNumber);
121
122            textConsist.setText(Bundle.getMessage("ConsistLineNumber"));
123            textConsist.setVisible(true);
124            consistNumber.setVisible(true);
125
126            // now read NCE CS consist memory and write to file
127            waiting = 0; // reset in case there was a previous error
128            fileValid = true; // assume we're going to succeed
129            // output string to file
130
131            for (int consistNum = 0; consistNum < workingNumConsists; consistNum++) {
132
133                consistNumber.setText(Integer.toString(consistNum));
134                fstatus.setVisible(true);
135
136                getNceConsist(consistNum);
137
138                if (!fileValid) {
139                    consistNum = workingNumConsists; // break out of for loop
140                }
141                if (fileValid) {
142                    StringBuilder buf = new StringBuilder();
143                    buf.append(":").append(Integer.toHexString(
144                            tc.csm.getConsistHeadAddr() + (consistNum * CONSIST_LNTH)));
145
146                    for (int i = 0; i < CONSIST_LNTH; i++) {
147                        buf.append(" ").append(StringUtil.twoHexFromInt(nceConsistData[i++]));
148                        buf.append(StringUtil.twoHexFromInt(nceConsistData[i]));
149                    }
150
151                    log.debug("consist {}", buf);
152                    fileOut.println(buf.toString());
153                }
154            }
155
156            if (fileValid) {
157                // NCE file terminator
158                String line = ":0000";
159                fileOut.println(line);
160            }
161
162            // Write to disk and close file
163            fileOut.flush();
164            fileOut.close();
165
166            // kill status panel
167            fstatus.dispose();
168
169            if (fileValid) {
170                JmriJOptionPane.showMessageDialog(null,
171                        Bundle.getMessage("SuccessfulBackup"),
172                        Bundle.getMessage("NceConsistBackup"),
173                        JmriJOptionPane.INFORMATION_MESSAGE);
174            } else {
175                JmriJOptionPane.showMessageDialog(null,
176                        Bundle.getMessage("BackupFailed"),
177                        Bundle.getMessage("NceConsistBackup"),
178                        JmriJOptionPane.ERROR_MESSAGE);
179            }
180
181        } catch (IOException e) {
182            // this is the end of the try-with-resources that opens fileOut.
183        }
184
185    }
186
187    // Read 16 bytes of NCE CS memory
188    private void getNceConsist(int cN) {
189
190        NceMessage m = readConsistMemory(cN);
191        tc.sendNceMessage(m, this);
192        // wait for read to complete
193        readWait();
194    }
195
196    // wait up to 30 sec per read
197    private boolean readWait() {
198        int waitcount = 30;
199        while (waiting > 0) {
200            synchronized (this) {
201                try {
202                    wait(1000);
203                } catch (InterruptedException e) {
204                    Thread.currentThread().interrupt(); // retain if needed later
205                }
206            }
207            if (waitcount-- < 0) {
208                log.error("read timeout"); // NOI18N
209                fileValid = false; // need to quit
210                return false;
211            }
212        }
213        return true;
214    }
215
216    // Reads 16 bytes of NCE consist memory
217    private NceMessage readConsistMemory(int consistNum) {
218
219        int nceConsistAddr = (consistNum * CONSIST_LNTH) + tc.csm.getConsistHeadAddr();
220        replyLen = NceMessage.REPLY_16; // Expect 16 byte response
221        waiting++;
222        byte[] bl = NceBinaryCommand.accMemoryRead(nceConsistAddr);
223        NceMessage m = NceMessage.createBinaryMessage(tc, bl, NceMessage.REPLY_16);
224        return m;
225    }
226
227    @Override
228    public void message(NceMessage m) {
229    } // ignore replies
230
231    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY")
232    // this reply always expects two consecutive reads
233    @Override
234    public void reply(NceReply r) {
235
236        if (waiting <= 0) {
237            log.error("unexpected response"); // NOI18N
238            return;
239        }
240        if (r.getNumDataElements() != replyLen) {
241            log.error("reply length incorrect"); // NOI18N
242            return;
243        }
244
245        // load data buffer
246        for (int i = 0; i < NceMessage.REPLY_16; i++) {
247            nceConsistData[i] = (byte) r.getElement(i);
248        }
249        waiting--;
250
251        // wake up backup thread
252        synchronized (this) {
253            notify();
254        }
255    }
256
257    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NceConsistBackup.class);
258
259}