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
120            while (true) {
121                try {
122                    line = in.readLine();
123                } catch (IOException e) {
124                    break;
125                }
126
127                macroNumber.setText(Integer.toString(macroNum++));
128
129                if (line == null) {    // while loop does not break out quick enough
130                    log.error("NCE macro file terminator :0000 not found"); // NOI18N
131                    break;
132                }
133                log.debug("macro {}", line);
134                // check that each line contains the NCE memory address of the macro
135                String macroAddr = ":" + Integer.toHexString(curMacro);
136                String[] macroLine = line.split(" ");
137
138                // check for end of macro terminator
139                if (macroLine[0].equalsIgnoreCase(":0000")) {
140                    fileValid = true; // success!
141                    break;
142                }
143
144                if (!macroAddr.equalsIgnoreCase(macroLine[0])) {
145                    log.error("Restore file selected is not a vaild backup file"); // NOI18N
146                    log.error("Macro addr in restore file should be {} Macro addr read {}", macroAddr, macroLine[0]); // NOI18N
147                    break;
148                }
149
150                // macro file found, give the user the choice to continue
151                if (curMacro == cs_macro_mem) {
152                    if (JmriJOptionPane
153                            .showConfirmDialog(
154                                    null,
155                                    Bundle.getMessage("dialogRestoreTime"),
156                                    Bundle.getMessage("RestoreTitle"),
157                                    JmriJOptionPane.YES_NO_OPTION) != JmriJOptionPane.YES_OPTION) {
158                        break;
159                    }
160                }
161
162                fstatus.setVisible(true);
163
164                // now read the entire line from the file and create NCE messages
165                for (int i = 0; i < 10; i++) {
166                    int j = i << 1;    // i = word index, j = byte index
167
168                    byte[] b = StringUtil.bytesFromHexString(macroLine[i + 1]);
169
170                    macroAccy[j] = b[0];
171                    macroAccy[j + 1] = b[1];
172                }
173
174                NceMessage m = writeNceMacroMemory(curMacro, macroAccy, false);
175                tc.sendNceMessage(m, this);
176                m = writeNceMacroMemory(curMacro, macroAccy, true);
177                tc.sendNceMessage(m, this);
178
179                curMacro += MACRO_LNTH;
180
181                // wait for writes to NCE CS to complete
182                if (waiting > 0) {
183                    synchronized (this) {
184                        try {
185                            wait(20000);
186                        } catch (InterruptedException e) {
187                            Thread.currentThread().interrupt(); // retain if needed later
188                        }
189                    }
190                }
191                // failed
192                if (waiting > 0) {
193                    log.error("timeout waiting for reply"); // NOI18N
194                    break;
195                }
196            }
197
198            in.close();
199            
200            // kill status panel
201            fstatus.dispose();
202
203            if (fileValid) {
204                JmriJOptionPane.showMessageDialog(null,
205                        Bundle.getMessage("dialogRestoreSuccess"),
206                        Bundle.getMessage("RestoreTitle"),
207                        JmriJOptionPane.INFORMATION_MESSAGE);
208            } else {
209                JmriJOptionPane.showMessageDialog(null,
210                        Bundle.getMessage("dialogRestoreFailed"),
211                        Bundle.getMessage("RestoreTitle"),
212                        JmriJOptionPane.ERROR_MESSAGE);
213            }
214
215        } catch (IOException ignore) {
216        }
217    }
218
219    // writes 20 bytes of NCE macro memory, and adjusts for second write
220    private NceMessage writeNceMacroMemory(int curMacro, byte[] b,
221            boolean second) {
222
223        replyLen = REPLY_1; // Expect 1 byte response
224        waiting++;
225        byte[] bl;
226
227        if (second) {
228            // write next 4 bytes
229            curMacro += 16; // adjust memory address for second memory write
230            byte[] data = new byte[4];
231            for (int i = 0; i < 4; i++) {
232                data[i] = b[i + 16];
233            }
234            bl = NceBinaryCommand.accMemoryWrite4(curMacro, data);
235
236        } else {
237            // write first 16 bytes
238            byte[] data = new byte[16];
239            for (int i = 0; i < 16; i++) {
240                data[i] = b[i];
241            }
242            bl = NceBinaryCommand.accMemoryWriteN(curMacro, data);
243        }
244        NceMessage m = NceMessage.createBinaryMessage(tc, bl, REPLY_1);
245        return m;
246    }
247
248    @Override
249    public void message(NceMessage m) {
250    } // ignore replies
251
252    @SuppressFBWarnings(value = "NN_NAKED_NOTIFY")
253    @Override
254    public void reply(NceReply r) {
255        log.debug("waiting for {} responses ", waiting);
256        if (waiting <= 0) {
257            log.error("unexpected response"); // NOI18N
258            return;
259        }
260        waiting--;
261        if (r.getNumDataElements() != replyLen) {
262            log.error("reply length incorrect"); // NOI18N
263            return;
264        }
265        if (replyLen == REPLY_1) {
266            // Looking for proper response
267            if (r.getElement(0) != NceMessage.NCE_OKAY) {
268                log.error("reply incorrect"); // NOI18N
269            }
270        }
271
272        // wake up restore thread
273        if (waiting == 0) {
274            synchronized (this) {
275                notify();
276            }
277        }
278    }
279
280    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(NceMacroRestore.class);
281
282}