001package jmri.jmrix.nce.macro;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.BufferedReader;
006import java.io.File;
007import java.io.FileReader;
008import java.io.IOException;
009
010import javax.swing.JFileChooser;
011import javax.swing.JPanel;
012
013import jmri.jmrix.nce.NceBinaryCommand;
014import jmri.jmrix.nce.NceMessage;
015import jmri.jmrix.nce.NceReply;
016import jmri.jmrix.nce.NceTrafficController;
017import jmri.util.FileUtil;
018import jmri.util.StringUtil;
019import jmri.util.swing.JmriJOptionPane;
020import jmri.util.swing.TextFilter;
021
022/**
023 * Restores NCE Macros from a text file defined by NCE.
024 * <p>
025 * NCE "Backup macros" dumps the macros into a text file. Each line contains the
026 * contents of one macro. The first macro, 0 starts at address xC800 (PH5 0x6000). The last
027 * macro 255 is at address xDBEC.
028 * <p>
029 * NCE file format:
030 * <p>
031 * :C800 (macro 0: 20 hex chars representing 10 accessories) :C814 (macro 1: 20
032 * hex chars representing 10 accessories) :C828 (macro 2: 20 hex chars
033 * representing 10 accessories) . . :DBEC (macro 255: 20 hex chars representing
034 * 10 accessories) :0000
035 * <p>
036 * Macro data byte:
037 * <p>
038 * 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
039 * D D addr bit 7 6 5 4 3 2 10 9 8 1 0 turnout T
040 * <p>
041 * By convention, MSB address bits 10 - 8 are one's complement. NCE macros
042 * always set the C bit to 1. The LSB "D" (0) determines if the accessory is to
043 * be thrown (0) or closed (1). The next two bits "D D" are the LSBs of the
044 * accessory address. Note that NCE display addresses are 1 greater than NMRA
045 * DCC. Note that address bit 2 isn't supposed to be inverted, but it is the way
046 * NCE implemented their macros.
047 * <p>
048 * Examples:
049 * <p>
050 * 81F8 = accessory 1 thrown 9FFC = accessory 123 thrown B5FD = accessory 211
051 * close BF8F = accessory 2044 close
052 * <p>
053 * FF10 = link macro 16
054 * <p>
055 * The restore routine checks that each line of the file begins with the
056 * appropriate macro address.
057 *
058 * @author Dan Boudreau Copyright (C) 2007
059 * @author Ken Cameron Copyright (C) 2023
060 */
061public class NceMacroRestore extends Thread implements jmri.jmrix.nce.NceListener {
062
063    private int cs_macro_mem; // start of NCE CS Macro memory
064    private static final int MACRO_LNTH = 20;  // 20 bytes per macro
065    private static final int REPLY_1 = 1;   // reply length of 1 byte expected
066    private int replyLen = 0;    // expected byte length
067    private int waiting = 0;     // to catch responses not intended for this module
068    private boolean fileValid = false;  // used to flag status messages
069
070    javax.swing.JLabel textMacro = new javax.swing.JLabel();
071    javax.swing.JLabel macroNumber = new javax.swing.JLabel();
072
073    private final NceTrafficController tc;
074
075    public NceMacroRestore(NceTrafficController t) {
076        super();
077        this.tc = t;
078        cs_macro_mem = tc.csm.getMacroAddr();
079    }
080
081    @Override
082    public void run() {
083
084        // Get file to read from
085        JFileChooser fc = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
086        fc.addChoosableFileFilter(new TextFilter());
087        int retVal = fc.showOpenDialog(null);
088        if (retVal != JFileChooser.APPROVE_OPTION) {
089            return; // Canceled
090        }
091        if (fc.getSelectedFile() == null) {
092            return; // Canceled
093        }
094        File f = fc.getSelectedFile();
095        
096        try (BufferedReader in = new BufferedReader(new FileReader(f))) {
097
098            // create a status frame
099            JPanel ps = new JPanel();
100            jmri.util.JmriJFrame fstatus = new jmri.util.JmriJFrame(Bundle.getMessage("RestoreTitle"));
101            fstatus.setLocationRelativeTo(null);
102            fstatus.setSize(200, 100);
103            fstatus.getContentPane().add(ps);
104
105            ps.add(textMacro);
106            ps.add(macroNumber);
107
108            textMacro.setText(Bundle.getMessage("MacroNumberLabel"));
109            textMacro.setVisible(true);
110            macroNumber.setVisible(true);
111
112            // Now read the file and check the macro address
113            waiting = 0;
114            fileValid = false;     // in case we break out early
115            int macroNum = 0;     // for user status messages
116            int curMacro = cs_macro_mem;  // load the start address of the NCE macro memory
117            byte[] macroAccy = new byte[20];  // NCE Macro data
118            String line;
119            int macroMemMim = tc.csm.getMacroAddr();
120            int macroMemMax = macroMemMim + (tc.csm.getMacroSize() * tc.csm.getMacroLimit());
121            int macroMemAddr;
122
123            while (true) {
124                try {
125                    line = in.readLine();
126                } catch (IOException e) {
127                    break;
128                }
129
130                macroNumber.setText(Integer.toString(macroNum++));
131
132                if (line == null) {    // while loop does not break out quick enough
133                    log.error("NCE macro file terminator :0000 not found"); // NOI18N
134                    break;
135                }
136                log.debug("macro {}", line);
137                // check that each line contains the NCE memory address of the macro
138                String macroAddr = ":" + Integer.toHexString(curMacro);
139                String[] macroLine = line.split(" ");
140
141                // check for end of macro terminator
142                if (macroLine[0].equalsIgnoreCase(":0000")) {
143                    fileValid = true; // success!
144                    break;
145                }
146
147                if (!macroAddr.equalsIgnoreCase(macroLine[0])) {
148                    log.error("Restore file selected is not a vaild backup file"); // NOI18N
149                    log.error("Macro addr in restore file should be {} Macro addr read {}", macroAddr, macroLine[0]); // NOI18N
150                    break;
151                }
152                
153                // check for macroLine our of range
154                macroMemAddr = Integer.parseUnsignedInt(macroLine[0].replace(":", ""), 16);
155                if (macroMemAddr < macroMemMim) {
156                    log.warn("macro mem file out of range, ending restore, got: {} mimimum: {} ",
157                            Integer.toHexString(macroMemAddr), Integer.toHexString(macroMemMim));
158                    fileValid = false;
159                    break;
160                }
161                if (macroMemAddr >= macroMemMax ) {
162                    log.warn("macro mem file out of range, ending restore, got: {} maximum: {} ",
163                            Integer.toHexString(macroMemAddr), Integer.toHexString(macroMemMax));
164                    fileValid = false;
165                    break;
166                }
167
168                // macro file found, give the user the choice to continue
169                if (curMacro == cs_macro_mem) {
170                    if (JmriJOptionPane
171                            .showConfirmDialog(
172                                    null,
173                                    Bundle.getMessage("dialogRestoreTime"),
174                                    Bundle.getMessage("RestoreTitle"),
175                                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
176                        break;
177                    }
178                }
179
180                fstatus.setVisible(true);
181
182                // now read the entire line from the file and create NCE messages
183                for (int i = 0; i < 10; i++) {
184                    int j = i << 1;    // i = word index, j = byte index
185
186                    byte[] b = StringUtil.bytesFromHexString(macroLine[i + 1]);
187
188                    macroAccy[j] = b[0];
189                    macroAccy[j + 1] = b[1];
190                }
191
192                NceMessage m = writeNceMacroMemory(curMacro, macroAccy, false);
193                tc.sendNceMessage(m, this);
194                m = writeNceMacroMemory(curMacro, macroAccy, true);
195                tc.sendNceMessage(m, this);
196
197                curMacro += MACRO_LNTH;
198
199                // wait for writes to NCE CS to complete
200                if (waiting > 0) {
201                    synchronized (this) {
202                        try {
203                            wait(20000);
204                        } catch (InterruptedException e) {
205                            Thread.currentThread().interrupt(); // retain if needed later
206                        }
207                    }
208                }
209                // failed
210                if (waiting > 0) {
211                    log.error("timeout waiting for reply"); // NOI18N
212                    break;
213                }
214            }
215
216            in.close();
217            
218            // kill status panel
219            fstatus.dispose();
220
221            if (fileValid) {
222                JmriJOptionPane.showMessageDialog(null,
223                        Bundle.getMessage("dialogRestoreSuccess"),
224                        Bundle.getMessage("RestoreTitle"),
225                        JmriJOptionPane.INFORMATION_MESSAGE);
226            } else {
227                JmriJOptionPane.showMessageDialog(null,
228                        Bundle.getMessage("dialogRestoreFailed"),
229                        Bundle.getMessage("RestoreTitle"),
230                        JmriJOptionPane.ERROR_MESSAGE);
231            }
232
233        } catch (IOException ignore) {
234        }
235    }
236
237    // writes 20 bytes of NCE macro memory, and adjusts for second write
238    private NceMessage writeNceMacroMemory(int curMacro, byte[] b,
239            boolean second) {
240
241        replyLen = REPLY_1; // Expect 1 byte response
242        waiting++;
243        byte[] bl;
244
245        if (second) {
246            // write next 4 bytes
247            curMacro += 16; // adjust memory address for second memory write
248            byte[] data = new byte[4];
249            for (int i = 0; i < 4; i++) {
250                data[i] = b[i + 16];
251            }
252            bl = NceBinaryCommand.accMemoryWrite4(curMacro, data);
253
254        } else {
255            // write first 16 bytes
256            byte[] data = new byte[16];
257            for (int i = 0; i < 16; i++) {
258                data[i] = b[i];
259            }
260            bl = NceBinaryCommand.accMemoryWriteN(curMacro, data);
261        }
262        NceMessage m = NceMessage.createBinaryMessage(tc, bl, REPLY_1);
263        return m;
264    }
265
266    @Override
267    public void message(NceMessage m) {
268    } // ignore replies
269
270    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY")
271    @Override
272    public void reply(NceReply r) {
273        log.debug("waiting for {} responses ", waiting);
274        if (waiting <= 0) {
275            log.error("unexpected response"); // NOI18N
276            return;
277        }
278        waiting--;
279        if (r.getNumDataElements() != replyLen) {
280            log.error("reply length incorrect"); // NOI18N
281            return;
282        }
283        if (replyLen == REPLY_1) {
284            // Looking for proper response
285            if (r.getElement(0) != NceMessage.NCE_OKAY) {
286                log.error("reply incorrect"); // NOI18N
287            }
288        }
289
290        // wake up restore thread
291        if (waiting == 0) {
292            synchronized (this) {
293                notify();
294            }
295        }
296    }
297
298    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NceMacroRestore.class);
299
300}