001package jmri.jmrix.rfid;
002
003import jmri.jmrix.AbstractMRReply;
004import org.slf4j.Logger;
005import org.slf4j.LoggerFactory;
006
007/**
008 * Basic interface to be implemented for each tag protocol.
009 *
010 * @author Matthew Harris Copyright (C) 2014
011 * @since 3.9.2
012 */
013abstract public class RfidProtocol {
014
015    protected boolean isConcentrator;
016    protected char concentratorFirst;
017    protected char concentratorLast;
018    protected int portPosition;
019
020    /**
021     * Constructor for an RFID Protocol. Used when a single reader is connected
022     * directly to a port, not via a concentrator.
023     */
024    public RfidProtocol() {
025        this('\u0000', '\u0000', 0);
026    }
027
028    /**
029     * Constructor for an RFID Protocol. Supports the use of concentrators where
030     * a character range is used to determine the specific reader port.
031     *
032     * @param concentratorFirst  character representing first concentrator port
033     * @param concentratorLast   character representing last concentrator port
034     * @param portPosition       position of port character in reply string;
035     *                            1 for first character
036     */
037    public RfidProtocol(char concentratorFirst, char concentratorLast, int portPosition) {
038        isConcentrator = concentratorFirst != '\u0000' && concentratorLast != '\u0000' && portPosition != 0;
039        this.concentratorFirst = concentratorFirst;
040        this.concentratorLast = concentratorLast;
041        this.portPosition = portPosition - 1; // needs to be zero-based;
042    }
043
044    /**
045     * Retrieves RFID Tag information from message
046     *
047     * @param msg Message to decode
048     * @return String representation of tag
049     */
050    public String getTag(AbstractMRReply msg) {
051        log.error("getTag should not be called");
052        return String.valueOf(msg.getElement(0));
053    }
054
055    /**
056     * Determines if this protocol provides checksum values Default is false.
057     * Protocols that do provide them should override this method.
058     *
059     * @return true if provided
060     */
061    public boolean providesChecksum() {
062        return false;
063    }
064
065    /**
066     * When available, returns the checksum portion of an RFID reply
067     *
068     * @param msg RFID reply to process
069     * @return checksum value
070     */
071    abstract public String getCheckSum(AbstractMRReply msg);
072
073    /**
074     * Determines if this RFID reply is valid
075     *
076     * @param msg RFID reply to process
077     * @return true if valid
078     */
079    abstract public boolean isValid(AbstractMRReply msg);
080
081    /**
082     * Determines if at the end of this RFID reply
083     *
084     * @param msg RFID reply to process
085     * @return true if at end
086     */
087    abstract public boolean endOfMessage(AbstractMRReply msg);
088
089    /**
090     * Returns the initialisation string to be sent to an adapter implementing
091     * the protocol. For those protocols that do not require one, return a blank
092     * string
093     *
094     * @return initialisation string
095     */
096    abstract public String initString();
097
098    public char getReaderPort(AbstractMRReply msg) {
099        if (isConcentrator) {
100            Character p = (char) msg.getElement(portPosition);
101            if (p.toString().matches("[" + this.concentratorFirst + "-" + this.concentratorLast + "]")) {
102                return p;
103            }
104        }
105        return 0x00;
106    }
107
108    /**
109     * Provides a textual representation of this message for the monitor
110     *
111     * @param msg RFID reply to process
112     * @return textual representation
113     */
114    public String toMonitorString(AbstractMRReply msg) {
115        StringBuilder sb = new StringBuilder();
116
117        // don't know, just show
118        sb.append("Unknown reply of length ");
119        sb.append(msg.getNumDataElements());
120        sb.append(": ");
121        sb.append(msg.toString()).append("\n");
122        sb.append("\n");
123        return sb.toString();
124    }
125
126    /**
127     * Precomputed translation table for hex characters 0..f
128     */
129    private static final byte[] hexCodes = new byte['f' + 1];
130
131    /**
132     * Static method to initialise translation table
133     */
134    static {
135        // Only 0..9, A..F & a..f are valid hex characters
136        // all others are invalid
137
138        // Set everything to invalid initially
139        for (int i = 0; i <= 'f'; i++) {
140            hexCodes[i] = -1;
141        }
142
143        // Now set values for 0..9
144        for (int i = '0'; i <= '9'; i++) {
145            hexCodes[i] = (byte) (i - '0');
146        }
147
148        // Now set values for A..F
149        for (int i = 'A'; i <= 'F'; i++) {
150            hexCodes[i] = (byte) (i - 'A' + 10);
151        }
152
153        // Finally, set values for a..f
154        for (int i = 'a'; i <= 'f'; i++) {
155            hexCodes[i] = (byte) (i - 'a' + 10);
156        }
157    }
158
159    /**
160     * Convert a single hex character to its corresponding hex value using
161     * pre-calculated translation table.
162     *
163     * @param c character to convert (0..9, a..f or A..F)
164     * @return corresponding integer value (0..15)
165     * @throws IllegalArgumentException when c is not a hex character
166     */
167    private static int charToNibble(char c) {
168        if (c > 'f') {
169            throw new IllegalArgumentException("Invalid hex character: " + c);
170        }
171        int nibble = hexCodes[c];
172        if (nibble < 0) {
173            throw new IllegalArgumentException("Invalid hex character: " + c);
174        }
175        return nibble;
176    }
177
178    /**
179     * Converts a hex string to an unsigned byte array. Both upper and lower
180     * case hex codes are permitted.
181     *
182     * @param s String representation of a hex number. Must be a whole number of
183     *          bytes (i.e. an even number of characters) and be formed only of
184     *          digits 0..9, a..f or A..F
185     * @return corresponding unsigned byte array
186     * @throws IllegalArgumentException when s is not a valid hex string
187     */
188    protected static byte[] convertHexString(String s) {
189
190        // Check the length of the string to convert
191        // is a whole number of bytes
192        int stringLength = s.length();
193        if ((stringLength & 0x1) != 0) {
194            throw new IllegalArgumentException("convertHexString requires an even number of hex characters");
195        }
196
197        // Create byte array to store the converted string
198        byte[] bytes = new byte[stringLength / 2];
199
200        // Loop through the string converting individual bytes
201        for (int i = 0, j = 0; i < stringLength; i += 2, j++) {
202            // Convert the high and low nibbles
203            int high = charToNibble(s.charAt(i));
204            int low = charToNibble(s.charAt(i + 1));
205
206            // Combine both nibbles into a byte
207            bytes[j] = (byte) ((high << 4) | low);
208        }
209        return bytes;
210    }
211
212    private static final Logger log = LoggerFactory.getLogger(RfidProtocol.class);
213
214}