001package jmri.jmrix.nce.macro;
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 javax.swing.JFileChooser;
012import javax.swing.JPanel;
013
014import jmri.jmrix.nce.NceBinaryCommand;
015import jmri.jmrix.nce.NceMessage;
016import jmri.jmrix.nce.NceReply;
017import jmri.jmrix.nce.NceTrafficController;
018import jmri.util.FileUtil;
019import jmri.util.JmriJFrame;
020import jmri.util.StringUtil;
021import jmri.util.swing.JmriJOptionPane;
022import jmri.util.swing.TextFilter;
023
024/**
025 * Backups NCE Macros to a text file format defined by NCE.
026 * <p>
027 * NCE "Backup macros" dumps the macros into a text file. Each line contains the
028 * contents of one macro. The first macro, 0 starts at address xC800 (PH5 0x6000). The last
029 * macro 255 is at address xDBEC.
030 * <p>
031 * NCE file format:
032 * <p>
033 * :C800 (macro 0: 20 hex chars representing 10 accessories) :C814 (macro 1: 20
034 * hex chars representing 10 accessories) :C828 (macro 2: 20 hex chars
035 * representing 10 accessories) . . :DBEC (macro 255: 20 hex chars representing
036 * 10 accessories) :0000
037 * <p>
038 * Macro data byte:
039 * <p>
040 * bit 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 _ _ _ _ 1 0 A A A A A A 1 A A A C D
041 * D D addr bit 7 6 5 4 3 2 10 9 8 1 0 turnout T
042 * <p>
043 * By convention, MSB address bits 10 - 8 are one's complement. NCE macros
044 * always set the C bit to 1. The LSB "D" (0) determines if the accessory is to
045 * be thrown (0) or closed (1). The next two bits "D D" are the LSBs of the
046 * accessory address. Note that NCE display addresses are 1 greater than NMRA
047 * DCC. Note that address bit 2 isn't supposed to be inverted, but it is the way
048 * NCE implemented their macros.
049 * <p>
050 * Examples:
051 * <p>
052 * 81F8 = accessory 1 thrown 9FFC = accessory 123 thrown B5FD = accessory 211
053 * close BF8F = accessory 2044 close
054 * <p>
055 * FF10 = link macro 16
056 * <p>
057 * This backup routine uses the same macro data format as NCE.
058 *
059 * @author Dan Boudreau Copyright (C) 2007
060 * @author Ken Cameron Copyright (C) 2023
061 */
062public class NceMacroBackup extends Thread implements jmri.jmrix.nce.NceListener {
063
064    private static final int NUM_MACRO = 256;  // there are 256 possible macros
065    private static final int MACRO_LNTH = 20;  // 20 bytes per macro
066    private static final int REPLY_16 = 16;   // reply length of 16 byte expected
067    private int replyLen = 0;    // expected byte length
068    private int waiting = 0;      // to catch responses not intended for this module
069    private boolean secondRead = false;    // when true, another 16 byte read expected
070    private boolean fileValid = false;    // used to flag backup status messages
071
072    private static final byte[] NCE_MACRO_DATA = new byte[MACRO_LNTH];
073
074    javax.swing.JLabel textMacro = new javax.swing.JLabel();
075    javax.swing.JLabel macroNumber = new javax.swing.JLabel();
076
077    private NceTrafficController tc = null;
078
079    public NceMacroBackup(NceTrafficController t) {
080        super();
081        this.tc = t;
082    }
083
084    @Override
085    public void run() {
086
087        // get file to write to
088        JFileChooser fc = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
089        fc.addChoosableFileFilter(new TextFilter());
090
091        File fs = new File("NCE macro backup.txt"); // NOI18N
092        fc.setSelectedFile(fs);
093
094        int retVal = fc.showSaveDialog(null);
095        if (retVal != JFileChooser.APPROVE_OPTION) {
096            return; // Canceled
097        }
098        if (fc.getSelectedFile() == null) {
099            return; // Canceled
100        }
101        File f = fc.getSelectedFile();
102        if (fc.getFileFilter() != fc.getAcceptAllFileFilter()) {
103            // append .txt to file name if needed
104            String fileName = f.getAbsolutePath();
105            String fileNameLC = fileName.toLowerCase();
106            if (!fileNameLC.endsWith(".txt")) {
107                fileName = fileName + ".txt";
108                f = new File(fileName);
109            }
110        }
111        if (f.exists()) {
112            if (JmriJOptionPane.showConfirmDialog(null,
113                    Bundle.getMessage("dialogConfirmOverwrite", f.getName()),
114                    Bundle.getMessage("dialogConfirmTitle"),
115                    JmriJOptionPane.OK_CANCEL_OPTION) != JmriJOptionPane.OK_OPTION) {
116                return;
117            }
118        }
119
120        try (PrintWriter fileOut = new PrintWriter(new BufferedWriter(new FileWriter(f)), true)) {
121            if (JmriJOptionPane.showConfirmDialog(null,
122                    Bundle.getMessage("dialogBackupTime"),
123                    Bundle.getMessage("BackupTitle"),
124                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
125                fileOut.close();
126                return;
127            }     
128
129            // create a status frame
130            JPanel ps = new JPanel();
131            JmriJFrame fstatus = new JmriJFrame(Bundle.getMessage("BackupTitle"));
132            fstatus.setLocationRelativeTo(null);
133            fstatus.setSize(200, 100);
134            fstatus.getContentPane().add(ps);
135
136            ps.add(textMacro);
137            ps.add(macroNumber);
138
139            textMacro.setText("Macro number:");
140            textMacro.setVisible(true);
141            macroNumber.setVisible(true);
142
143            // now read NCE CS macro memory and write to file
144            waiting = 0;   // reset in case there was a previous error
145            fileValid = true;  // assume we're going to succeed
146
147            for (int macroNum = 0; macroNum < NUM_MACRO; macroNum++) {
148
149                macroNumber.setText(Integer.toString(macroNum));
150                fstatus.setVisible(true);
151
152                getNceMacro(macroNum);
153
154                if (!fileValid) {
155                    macroNum = NUM_MACRO;  // break out of for loop
156                }
157                if (fileValid) {
158                    StringBuilder buf = new StringBuilder();
159                    buf.append(":").append(Integer.toHexString(tc.csm.getMacroAddr() + (macroNum * MACRO_LNTH)));
160
161
162                    for (int i = 0; i < MACRO_LNTH; i++) {
163                        buf.append(" ").append(StringUtil.twoHexFromInt(NCE_MACRO_DATA[i++]));
164                        buf.append(StringUtil.twoHexFromInt(NCE_MACRO_DATA[i]));
165                    }
166
167                    log.debug("macro {}", buf);
168
169                    fileOut.println(buf);
170                }
171            }
172
173            if (fileValid) {
174                // NCE file terminator
175                String line = ":0000";
176                fileOut.println(line);
177            }
178
179            // Write to disk and close file
180            fileOut.flush();
181            fileOut.close();
182
183            // kill status panel
184            fstatus.dispose();
185
186            if (fileValid) {
187                JmriJOptionPane.showMessageDialog(null,
188                        Bundle.getMessage("dialogBackupSuccess"),
189                        Bundle.getMessage("BackupTitle"),
190                        JmriJOptionPane.INFORMATION_MESSAGE);
191            } else {
192                JmriJOptionPane.showMessageDialog(null,
193                        Bundle.getMessage("dialogBackupFailed"),
194                        Bundle.getMessage("BackupTitle"),
195                        JmriJOptionPane.ERROR_MESSAGE);
196            }
197
198        } catch (IOException ignore) {
199        }
200
201    }
202
203    // Read 20 bytes of NCE CS memory
204    private void getNceMacro(int mN) {
205
206        NceMessage m = readMacroMemory(mN, false);
207        tc.sendNceMessage(m, this);
208        // wait for read to complete, flag determines if 1st or 2nd read
209        if (!readWait()) {
210            return;
211        }
212
213        NceMessage m2 = readMacroMemory(mN, true);
214        tc.sendNceMessage(m2, this);
215        readWait();
216    }
217
218    // wait up to 30 sec per read
219    private boolean readWait() {
220        int waitcount = 30;
221        while (waiting > 0) {
222            synchronized (this) {
223                try {
224                    wait(1000);
225                } catch (InterruptedException e) {
226                    Thread.currentThread().interrupt(); // retain if needed later
227                }
228            }
229            if (waitcount-- < 0) {
230                log.error("read timeout"); // NOI18N
231                fileValid = false; // need to quit
232                return false;
233            }
234        }
235        return true;
236    }
237
238    // Reads 16 bytes of NCE macro memory, and adjusts for second read
239    private NceMessage readMacroMemory(int macroNum, boolean second) {
240        secondRead = second;   // set flag for receive
241        int nceMacroAddr = (macroNum * MACRO_LNTH) + tc.csm.getMacroAddr();
242        if (second) {
243            nceMacroAddr += REPLY_16;  // adjust for second memory read
244        }
245        replyLen = REPLY_16;    // Expect 16 byte response
246        waiting++;
247        byte[] bl = NceBinaryCommand.accMemoryRead(nceMacroAddr);
248        NceMessage m = NceMessage.createBinaryMessage(tc, bl, REPLY_16);
249        return m;
250    }
251
252    @Override
253    public void message(NceMessage m) {
254    } // ignore replies
255
256    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY")
257    // this reply always expects two consecutive reads
258    @Override
259    public void reply(NceReply r) {
260
261        if (waiting <= 0) {
262            log.error("unexpected response"); // NOI18N
263            return;
264        }
265        if (r.getNumDataElements() != replyLen) {
266            log.error("reply length incorrect"); // NOI18N
267            return;
268        }
269
270        // first read 16 bytes, second read only 4 bytes needed
271        int offset = 0;
272        int numBytes = REPLY_16;
273        if (secondRead) {
274            offset = REPLY_16;
275            numBytes = 4;
276        }
277
278        for (int i = 0; i < numBytes; i++) {
279            NCE_MACRO_DATA[i + offset] = (byte) r.getElement(i);
280        }
281        waiting--;
282
283        // wake up backup thread
284        synchronized (this) {
285            notify();
286        }
287    }
288
289    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NceMacroBackup.class);
290
291}