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}