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}