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 int consistRecLen = 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[consistRecLen];
053
054    JLabel textConsist = new JLabel();
055    JLabel consistNumber = new JLabel();
056
057    private NceTrafficController tc = null;
058    private int consistStartNum = -1;
059    private int consistEndNum = -1;
060
061    public NceConsistBackup(NceTrafficController t) {
062        tc = t;
063        consistStartNum = tc.csm.getConsistMin();
064        consistEndNum = tc.csm.getConsistMax();
065    }
066
067    @Override
068    public void run() {
069
070        // get file to write to
071        JFileChooser fc = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
072        fc.addChoosableFileFilter(new TextFilter());
073
074        File fs = new File("NCE consist backup.txt"); // NOI18N
075        fc.setSelectedFile(fs);
076
077        int retVal = fc.showSaveDialog(null);
078        if (retVal != JFileChooser.APPROVE_OPTION) {
079            return; // Canceled
080        }
081        if (fc.getSelectedFile() == null) {
082            return; // Canceled
083        }
084        File f = fc.getSelectedFile();
085        if (fc.getFileFilter() != fc.getAcceptAllFileFilter()) {
086            // append .txt to file name if needed
087            String fileName = f.getAbsolutePath();
088            String fileNameLC = fileName.toLowerCase();
089            if (!fileNameLC.endsWith(".txt")) {
090                fileName = fileName + ".txt";
091                f = new File(fileName);
092            }
093        }
094        if (f.exists()) {
095            if (JmriJOptionPane.showConfirmDialog(null,
096                    MessageFormat.format(Bundle.getMessage("FileExists"), f.getName()),
097                    Bundle.getMessage("OverwriteFile"),
098                    JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) {
099                return;
100            }
101        }
102
103        try (PrintWriter fileOut = new PrintWriter(new BufferedWriter(new FileWriter(f)),
104                true)) {
105
106            if (JmriJOptionPane.showConfirmDialog(null,
107                    Bundle.getMessage("BackupTakesAwhile"),
108                    Bundle.getMessage("NceConsistBackup"),
109                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
110                fileOut.close();
111                return;
112            }
113
114            // create a status frame
115            JPanel ps = new JPanel();
116            jmri.util.JmriJFrame fstatus = new jmri.util.JmriJFrame(Bundle.getMessage("NceConsistBackup"));
117            fstatus.setLocationRelativeTo(null);
118            fstatus.setSize(300, 100);
119            fstatus.getContentPane().add(ps);
120
121            ps.add(textConsist);
122            ps.add(consistNumber);
123
124            textConsist.setText(Bundle.getMessage("ConsistLineNumber"));
125            textConsist.setVisible(true);
126            consistNumber.setVisible(true);
127
128            // now read NCE CS consist memory and write to file
129            waiting = 0; // reset in case there was a previous error
130            fileValid = true; // assume we're going to succeed
131            // output string to file
132            // head 2 bytes, end 2 bytes, mid num * 2 bytes
133            int consistBytesEach = (tc.csm.getConsistMidEntries() * 2) + 2 + 2;
134            int memOffsetEnd = consistBytesEach * (1 +consistEndNum - consistStartNum);
135            int consistRecNum = 0;
136            int consistNum = consistStartNum;
137
138            for (int memOffset = 0; memOffset < memOffsetEnd; memOffset += consistRecLen) {
139                
140                consistNum = consistStartNum + (memOffset / consistBytesEach);
141                consistRecNum = memOffset / consistRecLen;
142                consistNumber.setText(Integer.toString(consistNum));
143                fstatus.setVisible(true);
144
145                getNceConsist(consistRecNum);
146
147                if (!fileValid) {
148                    memOffset = memOffsetEnd; // break out of for loop
149                }
150                if (fileValid) {
151                    StringBuilder buf = new StringBuilder();
152                    buf.append(":").append(Integer.toHexString(tc.csm.getConsistHeadAddr() + memOffset));
153
154                    for (int i = 0; i < consistRecLen; i++) {
155                        buf.append(" ").append(StringUtil.twoHexFromInt(nceConsistData[i++]));
156                        buf.append(StringUtil.twoHexFromInt(nceConsistData[i]));
157                    }
158
159                    log.debug("consist {}", buf);
160                    fileOut.println(buf.toString());
161                }
162            }
163
164            if (fileValid) {
165                // NCE file terminator
166                String line = ":0000";
167                fileOut.println(line);
168            }
169
170            // Write to disk and close file
171            fileOut.flush();
172            fileOut.close();
173
174            // kill status panel
175            fstatus.dispose();
176
177            if (fileValid) {
178                JmriJOptionPane.showMessageDialog(null,
179                        Bundle.getMessage("SuccessfulBackup"),
180                        Bundle.getMessage("NceConsistBackup"),
181                        JmriJOptionPane.INFORMATION_MESSAGE);
182            } else {
183                JmriJOptionPane.showMessageDialog(null,
184                        Bundle.getMessage("BackupFailed"),
185                        Bundle.getMessage("NceConsistBackup"),
186                        JmriJOptionPane.ERROR_MESSAGE);
187            }
188
189        } catch (IOException e) {
190            // this is the end of the try-with-resources that opens fileOut.
191        }
192
193    }
194
195    // Read 16 bytes of NCE CS memory
196    private void getNceConsist(int cN) {
197
198        NceMessage m = readConsistMemory(cN);
199        tc.sendNceMessage(m, this);
200        // wait for read to complete
201        readWait();
202    }
203
204    // wait up to 30 sec per read
205    private boolean readWait() {
206        int waitcount = 30;
207        while (waiting > 0) {
208            synchronized (this) {
209                try {
210                    wait(1000);
211                } catch (InterruptedException e) {
212                    Thread.currentThread().interrupt(); // retain if needed later
213                }
214            }
215            if (waitcount-- < 0) {
216                log.error("read timeout"); // NOI18N
217                fileValid = false; // need to quit
218                return false;
219            }
220        }
221        return true;
222    }
223
224    /*
225     * // USB set cab memory pointer private void setUsbCabMemoryPointer(int
226     * cab, int offset) { log.debug("Macro base address: {}, offset: {}",
227     * Integer.toHexString(cab), offset); replyLen = NceMessage.REPLY_1; //
228     * Expect 1 byte response waiting++; byte[] bl =
229     * NceBinaryCommand.usbMemoryPointer(cab, offset); NceMessage m =
230     * NceMessage.createBinaryMessage(tc, bl, NceMessage.REPLY_1);
231     * tc.sendNceMessage(m, this); }
232     * 
233     * // USB Read N bytes of NCE cab memory private void readUsbMemoryN(int
234     * num) { switch (num) { case 1: replyLen = NceMessage.REPLY_1; // Expect 1
235     * byte response break; case 2: replyLen = NceMessage.REPLY_2; // Expect 2
236     * byte response break; case 4: replyLen = NceMessage.REPLY_4; // Expect 4
237     * byte response break; default: log.error("Invalid usb read byte count");
238     * return; } waiting++; byte[] bl = NceBinaryCommand.usbMemoryRead((byte)
239     * num); NceMessage m = NceMessage.createBinaryMessage(tc, bl, replyLen);
240     * tc.sendNceMessage(m, this); }
241     * 
242     * // USB Write 1 byte of NCE memory private void writeUsbMemory1(byte
243     * value) { log.debug("Write byte: {}", String.format("%2X", value));
244     * replyLen = NceMessage.REPLY_1; // Expect 1 byte response waiting++;
245     * byte[] bl = NceBinaryCommand.usbMemoryWrite1(value); NceMessage m =
246     * NceMessage.createBinaryMessage(tc, bl, NceMessage.REPLY_1);
247     * tc.sendNceMessage(m, this); }
248     */
249    
250    // Reads 16 bytes of NCE consist memory
251    private NceMessage readConsistMemory(int consistNum) {
252
253        int nceConsistAddr = (consistNum * consistRecLen) + tc.csm.getConsistHeadAddr();
254        replyLen = NceMessage.REPLY_16; // Expect 16 byte response
255        waiting++;
256        byte[] bl = NceBinaryCommand.accMemoryRead(nceConsistAddr);
257        NceMessage m = NceMessage.createBinaryMessage(tc, bl, NceMessage.REPLY_16);
258        return m;
259    }
260
261    @Override
262    public void message(NceMessage m) {
263    } // ignore replies
264
265    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY")
266    // this reply always expects two consecutive reads
267    @Override
268    public void reply(NceReply r) {
269
270        if (waiting <= 0) {
271            log.error("unexpected response"); // NOI18N
272            return;
273        }
274        if (r.getNumDataElements() != replyLen) {
275            log.error("reply length incorrect"); // NOI18N
276            return;
277        }
278
279        // load data buffer
280        for (int i = 0; i < NceMessage.REPLY_16; i++) {
281            nceConsistData[i] = (byte) r.getElement(i);
282        }
283        waiting--;
284
285        // wake up backup thread
286        synchronized (this) {
287            notify();
288        }
289    }
290
291    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NceConsistBackup.class);
292
293}