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}