001package jmri.jmrix.loconet.soundloader; 002 003import jmri.jmrix.loconet.LnTrafficController; 004import jmri.jmrix.loconet.LocoNetMessage; 005import jmri.jmrix.loconet.LocoNetSystemConnectionMemo; 006import jmri.jmrix.loconet.spjfile.SpjFile; 007import org.slf4j.Logger; 008import org.slf4j.LoggerFactory; 009 010/** 011 * Controls the actual LocoNet transfers to download sounds into a Digitrax SFX 012 * decoder. 013 * 014 * @author Bob Jacobsen Copyright (C) 2006 015 */ 016public class LoaderEngine { 017 018 static final int CMD_START = 0x04; 019 static final int CMD_ADD = 0x08; 020 021 static final int TYPE_SDF = 0x01; 022 static final int TYPE_WAV = 0x00; 023 024 static final int SENDPAGESIZE = 256; 025 static final int SENDDATASIZE = 128; 026 027 SpjFile spjFile; 028 029 public LoaderEngine(LocoNetSystemConnectionMemo memo) { 030 this.memo = memo; 031 } 032 033 /** 034 * Send the complete sequence to download to a decoder. 035 * <p> 036 * Intended to be run in a separate thread. 037 * 038 * Uses "notify" method for status updates; 039 * overload that to redirect the messages. 040 * 041 * @param file the spjfile to be used. 042 */ 043 public void runDownload(SpjFile file) { 044 this.spjFile = file; 045 046 initController(); 047 048 // use a try-catch to handle aborts from below 049 try { 050 // erase flash 051 notify(Bundle.getMessage("EngineEraseFlash")); 052 controller.sendLocoNetMessage(getEraseMessage()); 053 protectedWait(1000); 054 notify(Bundle.getMessage("EngineEraseWait")); 055 protectedWait(20000); 056 057 // start 058 notify(Bundle.getMessage("EngineSendInit")); 059 controller.sendLocoNetMessage(getInitMessage()); 060 protectedWait(250); 061 062 // send SDF info 063 sendSDF(); 064 065 // send all WAV subfiles 066 sendAllWAV(); 067 068 // end 069 controller.sendLocoNetMessage(getExitMessage()); 070 notify(Bundle.getMessage("EngineDone")); 071 } catch (DelayException e) { 072 notify(Bundle.getMessage("EngineAbortDelay")); 073 } 074 075 } 076 077 void sendSDF() throws DelayException { 078 notify(Bundle.getMessage("EngineSendSdf")); 079 080 // get control info, data 081 SpjFile.Header header = spjFile.findSdfHeader(); 082 int handle = header.getHandle(); 083 String name = header.getName(); 084 byte[] contents = header.getByteArray(); 085 086 // transfer 087 LocoNetMessage m; 088 089 m = initTransfer(TYPE_SDF, handle, name, contents); 090 controller.sendLocoNetMessage(m); 091 throttleOutbound(m); 092 093 while ((m = nextTransfer()) != null) { 094 controller.sendLocoNetMessage(m); 095 throttleOutbound(m); 096 } 097 } 098 099 void sendAllWAV() throws DelayException { 100 notify(Bundle.getMessage("EngineSendWav")); 101 for (int i = 1; i < spjFile.numHeaders(); i++) { 102 // see if WAV 103 if (spjFile.getHeader(i).isWAV()) { 104 sendOneWav(i); 105 } 106 } 107 } 108 109 public void sendOneWav(int index) throws DelayException { 110 notify(Bundle.getMessage("EngineSendWavBlock", index)); 111 // get control info, data 112 SpjFile.Header header = spjFile.getHeader(index); 113 int handle = header.getHandle(); 114 String name = header.getName(); 115 byte[] buffer = header.getByteArray(); 116 117 // that byte array is the "record", not "data"; 118 // recopy in offset 119 int offset = header.getDataStart() - header.getRecordStart(); 120 int len = header.getDataLength(); 121 byte[] contents = new byte[len]; 122 for (int i = 0; i < len; i++) { 123 contents[i] = buffer[i + offset]; 124 } 125 126 // transfer 127 LocoNetMessage m; 128 129 m = initTransfer(TYPE_WAV, handle, name, contents); 130 controller.sendLocoNetMessage(m); 131 throttleOutbound(m); 132 133 while ((m = nextTransfer()) != null) { 134 controller.sendLocoNetMessage(m); 135 throttleOutbound(m); 136 } 137 } 138 139 /** 140 * Notify of status of download. 141 * <p> 142 * This implementation doesn't do much, but this is provided as a separate 143 * method to allow easy overloading. 144 * @param message string form of message. 145 */ 146 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value = "SLF4J_FORMAT_SHOULD_BE_CONST", 147 justification = "passing debug message String unchanged") 148 public void notify(String message) { 149 log.debug(message); 150 } 151 152 /** 153 * Delay to prevent too much data being sent down. 154 * 155 * Works with the controller to ensure that too much data doesn't back up. 156 * @param m Throttle message to send 157 * @throws DelayException if too much time elapsed before send possible 158 */ 159 void throttleOutbound(LocoNetMessage m) throws DelayException { 160 protectedWait(50); // minimum wait to clear 161 162 // wait up to 1 sec in 10mSec chunks for isXmtBusy to clear 163 for (int i = 1; i < 100; i++) { 164 if (!controller.isXmtBusy()) { 165 return; // done, so return 166 } // wait a while, and then try again 167 protectedWait(10); 168 } 169 throw new DelayException("Ran out of time after sending " + m.toString()); // NOI18N 170 } 171 172 static class DelayException extends Exception { 173 DelayException(String s) { 174 super(s); 175 } 176 } 177 178 /** 179 * Provide a simple object wait. 180 * <p> 181 * This handles interrupts, synchronization, etc. 182 * @param millis milliseconds to wait. 183 */ 184 public void protectedWait(int millis) { 185 synchronized (this) { 186 try { 187 wait(millis); 188 } catch (InterruptedException e) { 189 Thread.currentThread().interrupt(); // retain if needed later 190 } 191 } 192 } 193 194 /** 195 * Start a sequence to download a specific type of data. 196 * 197 * This returns the message to start the process. You then loop calling 198 * nextWavTransfer() until it says it's complete. 199 * 200 * @param type Either TYPE_SDF or TYPE_WAV for the data type 201 * @param handle Handle number for the following data 202 * @param name Name of the transfer 203 * @param contents Data to download 204 * @return Prepared message 205 */ 206 LocoNetMessage initTransfer(int type, int handle, String name, byte[] contents) { 207 transferType = type; 208 transferStart = true; 209 transferHandle = handle; 210 transferName = name; 211 transferContents = contents; 212 213 return getStartDataMessage(transferType, handle, contents.length); 214 } 215 216 private boolean transferStart; 217 private int transferType; 218 private int transferHandle; 219 private String transferName; 220 private byte[] transferContents; 221 private int transferIndex; 222 223 /** 224 * Get the next message for an ongoing WAV download. 225 * <p> 226 * You loop calling nextWavTransfer() until it says it's complete by 227 * returning null. 228 * @return message to send. 229 */ 230 public LocoNetMessage nextTransfer() { 231 if (transferStart) { 232 233 transferStart = false; 234 transferIndex = 0; 235 236 // first transfer, send DataHeader info 237 byte[] header = new byte[40]; 238 header[0] = (byte) transferHandle; 239 header[1] = (byte) (transferContents.length & 0xFF); 240 header[2] = (byte) ((transferContents.length / 256) & 0xFF); 241 header[3] = (byte) ((transferContents.length / 256 / 256) & 0xFF); 242 header[4] = 0; // hdroffset 243 header[5] = 0; // wavemode1 244 header[6] = 0; // wavemode2 245 header[7] = 0; // spare1 246 247 for (int i = 8; i < 40; i++) { 248 header[i] = 0; 249 } 250 if (transferName.length() > 32) { 251 log.error("name {} is too long, truncated", transferName); 252 } 253 for (int i = 0; i < Math.min(32, transferName.length()); i++) { 254 header[i + 8] = (byte) transferName.charAt(i); 255 } 256 257 return getSendDataMessage(transferType, transferHandle, header); 258 259 } else { 260 // subsequent transfers, send what data you can. 261 // calculate remaining bytes 262 int remaining = transferContents.length - transferIndex; 263 if (remaining < 0) { 264 log.error("Did not expect to find length {} and index {}", transferContents.length, transferIndex); 265 } 266 if (remaining <= 0) { 267 return null; // transfer complete 268 } 269 // set up a buffer for this transfer 270 int sendSize = remaining; 271 if (remaining > SENDDATASIZE) { 272 sendSize = SENDDATASIZE; 273 } 274 byte[] buffer = new byte[sendSize]; 275 for (int i = 0; i < sendSize; i++) { 276 buffer[i] = transferContents[transferIndex + i]; 277 } 278 279 // update for next time 280 transferIndex = transferIndex + sendSize; 281 282 // and return the message 283 return getSendDataMessage(transferType, transferHandle, buffer); 284 } 285 } 286 287 /** 288 * Get a message to start the download of data 289 * 290 * @param type Either TYPE_SDF or TYPE_WAV for the data type 291 * @param handle Handle number for the following data 292 * @param length Total length of the WAV data to load 293 * @return Prepared message 294 */ 295 LocoNetMessage getStartDataMessage(int type, int handle, int length) { 296 int pagecount = length / SENDPAGESIZE; 297 int remainder = length - pagecount * SENDPAGESIZE; 298 if (remainder != 0) { 299 pagecount++; 300 } 301 302 if (log.isDebugEnabled()) { 303 log.debug("getStartDataMessage: {},{},{};{},{}", type, handle, length, pagecount, remainder); 304 } 305 306 LocoNetMessage m = new LocoNetMessage(new int[]{0xD3, (type | CMD_START), handle, pagecount & 0x7F, 307 (pagecount / 128), 0}); 308 m.setParity(); 309 return m; 310 } 311 312 /** 313 * Get a message to tell the PR2 to store length bytes of data (following) 314 * 315 * @param type Either TYPE_SDF or TYPE_WAV for the data type 316 * @param handle Handle number for the following data 317 * @param contents Data to download 318 * @return Prepared message 319 */ 320 LocoNetMessage getSendDataMessage(int type, int handle, byte[] contents) { 321 322 int length = contents.length; 323 324 LocoNetMessage m = new LocoNetMessage(length + 7); 325 m.setElement(0, 0xD3); 326 m.setElement(1, type | CMD_ADD); 327 m.setElement(2, handle); 328 m.setElement(3, length & 0x7F); 329 m.setElement(4, (length / 128)); 330 m.setElement(5, 0x00); // 1st checksum 331 332 for (int i = 0; i < length; i++) { 333 m.setElement(6 + i, contents[i]); 334 } 335 336 m.setParity(); 337 return m; 338 } 339 340 /** 341 * Get a message to erase the non-volatile sound memory 342 * @return Prepared message 343 */ 344 LocoNetMessage getEraseMessage() { 345 LocoNetMessage m = new LocoNetMessage(new int[]{0xD3, 0x02, 0x01, 0x7F, 0x00, 0x50}); 346 m.setParity(); 347 return m; 348 } 349 350 /** 351 * Get a message to initialize the load sequence 352 * @return Prepared message 353 */ 354 LocoNetMessage getInitMessage() { 355 LocoNetMessage m = new LocoNetMessage(new int[]{0xD3, 0x01, 0x00, 0x00, 0x00, 0x2D}); 356 m.setParity(); 357 return m; 358 } 359 360 /** 361 * Get a message to exit the download process 362 * @return Prepared message 363 */ 364 LocoNetMessage getExitMessage() { 365 LocoNetMessage m = new LocoNetMessage(new int[]{0xD3, 0x00, 0x00, 0x00, 0x00, 0x2C}); 366 m.setParity(); 367 return m; 368 } 369 370 LocoNetSystemConnectionMemo memo; 371 372 void initController() { 373 if (controller == null) { 374 controller = memo.getLnTrafficController(); 375 } 376 } 377 378 LnTrafficController controller = null; 379 380 public void dispose() { 381 } 382 383 private final static Logger log = LoggerFactory.getLogger(LoaderEngine.class); 384 385}