001package jmri.jmrix.sprog.update;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.io.BufferedInputStream;
006import java.io.BufferedOutputStream;
007import java.io.File;
008import java.io.FileInputStream;
009import java.io.FileOutputStream;
010import java.io.IOException;
011import java.util.Arrays;
012
013import jmri.util.swing.JmriJOptionPane;
014
015/**
016 * Class to encapsulate an intel format hex file and methods to manipulate it.
017 * Intended use is as an input format for new program code to be sent to a
018 * hardware device via some bootloading process.
019 *
020 * @author Andrew Crosland Copyright (C) 2010
021 */
022public class SprogHexFile extends jmri.util.JmriJFrame {
023
024    private File file;
025    private FileInputStream in;
026    private BufferedInputStream buffIn;
027    private FileOutputStream out;
028    private BufferedOutputStream buffOut;
029    private int address = 0;
030    private int type;
031    private int len;
032    private int data[];
033    private boolean read;
034    private int lineNo = 0;
035    private int charIn;
036    private String name;
037
038    // Hex file record types
039    static final byte EXT_ADDR = 4;
040    static final byte DATA = 0;
041    static final byte END = 1;
042    // Maximum record length
043    private static final int MAX_LEN = (255 + 1 + 2 + 1 + 1) * 2;
044    // Offsets of fields within the record
045    private static final int LEN = 0;
046    private static final int ADDRH = 1;
047    private static final int ADDRL = 2;
048    private static final int TYPE = 3;
049
050    public SprogHexFile(String fileName) {
051        name = fileName;
052        file = new File(fileName);
053    }
054
055    /**
056     * @return name of the open file
057     */
058    @Override
059    public String getName() {
060        return name;
061    }
062
063    /**
064     * Open hex file for reading.
065     *
066     * @return boolean true if successful
067     */
068    public boolean openRd() {
069        read = true;
070        try {
071            // Create an input reader based on the file, so we can read its data.
072            in = new FileInputStream(file);
073            buffIn = new BufferedInputStream(in);
074            // Assume addresses start at 0 for hex files that do not have an initial type 4 record
075            address = 0;
076            //line = new StringBuffer("");
077            return true;
078        } catch (IOException e) {
079            return false;
080        }
081    }
082
083    /**
084     * Open file for writing.
085     *
086     * @return boolean true if successful
087     */
088    public boolean openWr() {
089        read = false;
090        try {
091            // Create an output writer based on the file, so we can write.
092            out = new FileOutputStream(file);
093            buffOut = new BufferedOutputStream(out);
094            return true;
095        } catch (IOException e) {
096            return false;
097        }
098    }
099
100    /**
101     * Close the currently open file.
102     */
103    public void close() {
104        try {
105            if (read) {
106                buffIn.close();
107                in.close();
108            } else {
109                buffOut.flush();
110                buffOut.close();
111                out.close();
112            }
113            name = null;
114        } catch (IOException e) {
115
116        }
117    }
118
119    /**
120     * Read a record (line) from the hex file.
121     * <p>
122     * If it's an extended address record then update the address
123     * and read the next line. Returns the data length.
124     *
125     * @return int the data length of the record, or 0 if no data
126     */
127    @SuppressFBWarnings(value = "DLS_DEAD_LOCAL_STORE")
128    // False positive
129    public int read() {
130        // Make space for the maximum size record to be read
131        int record[] = new int[MAX_LEN];
132        do {
133            record = readLine();
134            if (type == EXT_ADDR) {
135                // Get new extended address and read next line
136                address = address & 0xffff
137                        + record[4] * 256 * 65536 + record[5] * 65536;
138                record = readLine();
139            }
140        } while ((type != DATA) && (type != END));
141        if (type == END) {
142            return 0;
143        }
144        data = new int[len];
145        for (int i = 0; i < len; i++) {
146            data[i] = record[TYPE + 1 + i];
147        }
148        return len;
149    }
150
151    /**
152     * Read a line from the hex file and verify the checksum.
153     *
154     * @return int[] the array of bytes read from the file
155     */
156    public int[] readLine() {
157        // Make space for the maximum size record to be read
158        int record[] = new int[MAX_LEN];
159        int checksum = 0;
160        // Read ":"
161        try {
162            while (((charIn = buffIn.read()) == 0xd)
163                    || (charIn == 0xa)) {
164                // skip
165            }
166            if (charIn != ':') {
167                if (log.isDebugEnabled()) {
168                    log.debug("HexFile.readLine no colon at start of line {}", lineNo);
169                }
170                return new int[]{-1};
171            }
172        } catch (IOException e) {
173            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("IoErrorReadingHexFile"),
174                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
175            if (log.isDebugEnabled()) {
176                log.debug("I/O Error reading hex file!{}", e.toString());
177            }
178        }
179        // length of data
180        record[LEN] = rdHexByte();
181        checksum += record[LEN];
182        // High address
183        record[ADDRH] = rdHexByte();
184        checksum += record[ADDRH];
185        // Low address
186        record[ADDRL] = rdHexByte();
187        checksum += record[ADDRL];
188        // record type
189        record[TYPE] = rdHexByte();
190        checksum += record[TYPE];
191        // update address
192        address = (address & 0xffff0000) + record[ADDRH] * 256 + record[ADDRL];
193        type = record[TYPE];
194        if (type != END) {
195            len = record[LEN];
196            for (int i = 1; i <= len; i++) {
197                record[TYPE + i] = rdHexByte();
198                checksum += record[TYPE + i];
199            }
200        }
201        int fileCheck = rdHexByte();
202        if (((checksum + fileCheck) & 0xff) != 0) {
203            log.error("HexFile.readLine bad checksum at line {}", lineNo);
204        }
205        lineNo++;
206        return record;
207    }
208
209    /**
210     * Read a hex byte.
211     *
212     * @return byte the byte that was read
213     */
214    private int rdHexByte() {
215        int hi = rdHexDigit();
216        int lo = rdHexDigit();
217        if ((hi < 16) && (lo < 16)) {
218            return (hi * 16 + lo);
219        } else {
220            return 0;
221        }
222    }
223
224    /**
225     * Read a single hex digit.
226     *
227     * @return 16 if digit is invalid. byte low nibble contains the hex digit read.
228     * high nibble set if error.
229     */
230    private int rdHexDigit() {
231        int b = 0;
232        try {
233            b = buffIn.read();
234            if ((b >= '0') && (b <= '9')) {
235                b = b - '0';
236            } else if ((b >= 'A') && (b <= 'F')) {
237                b = b - 'A' + 10;
238            } else if ((b >= 'a') && (b <= 'f')) {
239                b = b - 'a' + 10;
240            } else {
241                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("InvalidHexDigitAtLine", lineNo),
242                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
243                log.error("Format Error! Invalid hex digit at line {}", lineNo);
244                b = 16;
245            }
246        } catch (IOException e) {
247            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("IoErrorReadingHexFile"),
248                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
249            log.error("I/O Error reading hex file!{}", e.toString());
250        }
251        return (byte) b;
252    }
253
254    /**
255     * Write a line to the hex file.
256     *
257     * @param addr int the starting address of the data
258     * @param type byte the type of data record being written
259     * @param data byte[] the array of bytes to be written
260     */
261    public void write(int addr, byte type, byte[] data) {
262        // Make space for the record to be written
263        byte record[] = new byte[data.length + 1 + 2 + 1];
264        if (addr / 0x10000 != address / 0x10000) {
265            // write an extended address record
266            byte[] extAddr = {
267                2, 0, 0, EXT_ADDR, 0, (byte) (addr / 0x10000)};
268            writeLine(extAddr);
269        }
270        // update current address
271        address = addr;
272        // save length, address and record type
273        record[LEN] = (byte) (data.length);
274        record[ADDRH] = (byte) (address / 0x100);
275        record[ADDRL] = (byte) (address & 0xff);
276        record[TYPE] = type;
277        // copy the data
278        for (int i = 0; i < data.length; i++) {
279            record[TYPE + 1 + i] = data[i];
280        }
281        // write the record
282        writeLine(record);
283    }
284
285    /**
286     * Write an extended address record.
287     *
288     * @param addr the extended address
289     */
290    public void wrExtAddr(int addr) {
291        write(0, EXT_ADDR, new byte[]{(byte) (addr / 256), (byte) (addr & 0xff)});
292    }
293
294    /**
295     * Write an end of file record.
296     *
297     */
298    public void wrEof() {
299        writeLine(new byte[]{0, 0, 0, END});
300    }
301
302    /**
303     * Get the type of the last record read from the hex file.
304     *
305     * @return byte the record type
306     */
307    public int getRecordType() {
308        return type;
309    }
310
311    /**
312     * Get the length of the last record read from the hex file.
313     *
314     * @return byte the length
315     */
316    public int getLen() {
317        return len;
318    }
319
320    /**
321     * Get current address.
322     *
323     * @return int the current address
324     */
325    public int getAddress() {
326        return address;
327    }
328
329    /**
330     * Get lower byte of current address.
331     *
332     * @return byte the lower byte of the address
333     */
334    public byte getAddressL() {
335        return (byte) (address & 0xff);
336    }
337
338    /**
339     * Get high (middle) byte of current address.
340     *
341     * @return byte the high (middle) byte of the address
342     */
343    public byte getAddressH() {
344        return (byte) ((address / 0x100) & 0xff);
345    }
346
347    /**
348     * Get upper byte of current address.
349     *
350     * @return byte the upper byte of the address
351     */
352    public byte getAddressU() {
353        return (byte) (address / 0x10000);
354    }
355
356    /**
357     * Get data from last record read.
358     *
359     * @return byte[] array of data bytes
360     */
361    public int[] getData() {
362        return Arrays.copyOf(data, data.length);
363    }
364
365    /**
366     * Write a byte array to the hex file, prepending ":" and appending checksum
367     * and carriage return.
368     *
369     * @param data byte[] array of data bytes top be written
370     */
371    private void writeLine(byte[] data) {
372        int checksum = 0;
373        try {
374            buffOut.write(':');
375            for (int i = 0; i < data.length; i++) {
376                writeHexByte(data[i]);
377                checksum += data[i];
378            }
379            checksum = checksum & 0xff;
380            if (checksum > 0) {
381                checksum = 256 - checksum;
382            }
383            writeHexByte((byte) checksum);
384            buffOut.write('\n');
385        } catch (IOException e) {
386
387        }
388    }
389
390    /**
391     * Write a byte as two hex characters.
392     *
393     * @param b byte the byte to be written
394     */
395    private void writeHexByte(byte b) {
396        int i = b;
397        // correct for byte being -128 to +127
398        if (b < 0) {
399            i = 256 + b;
400        }
401        writeHexDigit((byte) (i / 16));
402        writeHexDigit((byte) (i & 0xf));
403    }
404
405    /**
406     * Write a single hex digit.
407     *
408     * @param b byte low nibble contains the hex digit to be written
409     */
410    private void writeHexDigit(byte b) {
411        try {
412            if (b > 9) {
413                buffOut.write(b - 9 + 0x40);
414            } else {
415                buffOut.write(b + 0x30);
416            }
417        } catch (IOException e) {
418        }
419    }
420
421    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SprogHexFile.class);
422
423}