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}