package com.dreikraft.axbo.data; import com.dreikraft.events.ApplicationEventDispatcher; import com.dreikraft.axbo.events.InfoEvent; import com.dreikraft.axbo.events.MovementEvent; import com.dreikraft.axbo.util.ByteUtil; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * The AxboDataParser class processes data chunks received from the serial * interface. The data parser is implemented as an enum state machine (see * http://http://vanillajava.blogspot.co.at/2011/06/java-secret-using-enum-as-state-machine.html). * The aXbo protocol definition can be found here: * https://docs.google.com/document/d/1rQkp8XcMFh0PPenZfhWqaGXDfi2gdxutdeXtu-T6OTo/pub * * The class is implemented as an enum singleton (thread safe singleton). * * @author jan.illetschko@3kraft.com */ public enum AxboDataParser implements ProtocolHandler { /** * The singleton instance. */ INSTANCE; /** * The logger. */ public static final Log log = LogFactory.getLog(AxboDataParser.class); /** * Status command byte. */ public static final int STATUS_COMMAND_BYTE = 0x20; /** * Movement command byte */ public static final int MOVEMENT_COMMAND_BYTE = 0x26; /** * Dummy command byte. */ public static final int DUMMY_COMMAND_BYTE = 0xA4; /** * Dummy command byte for aXbo's with 2mb memory (since hardware release * HW12). */ public static final int DUMMY_COMMAND_BYTE_HW12 = 0xAC; /** * Flash buffer read command byte */ public static final int FLASH_BUFFERREAD = 0x00; /** * Toggle byte input off. */ public static final int TOGGLE_BYTE_INPUT_OFF = 0x00; /** * Toggle byte input on. */ public static final int TOGGLE_BYTE_INPUT_ON = 0x80; /** * Toggle byte output off. */ public static final int TOGGLE_BYTE_OUTPUT_OFF = 0x01; /** * Toggle byte output on. */ public static final int TOGGLE_BYTE_OUTPUT_ON = 0x81; /** * Data link escape byte. */ public static final int DLE = 0x10; /** * Start transaction byte. */ public static final int STX = 0x02; /** * End transaction byte. */ public static final int ETX = 0x03; /** * Acknowledged byte. */ public static final int ACK = 0x06; /** * Not acknowledged byte. */ public static final int NACK = 0x15; /** * Initial data buffer size. */ public static final int BUFFER_SIZE = 0x20; /** * State machine context for data parsing. */ private final AxboDataContext ctx; private int memSize = 1; private AxboDataParser() { ctx = new AxboDataContext(); } /** * {@inheritDoc} * * @param data */ @Override public void parse(final byte[] data) { if (log.isDebugEnabled()) { log.debug("Protocol: " + ByteUtil.dumpByteArray(data)); } // for all bytes in this data chunk for (final byte dataItem : data) { // sets the current data byte into the state machine context. ctx.setDataItem(ByteUtil.byteToInt(dataItem)); // processes the current data byte in the current state of the state // machine. ctx.getState().process(ctx); } } @Override public void reset() { ctx.setState(AxboDataStates.BEGIN); } public void setMemSize(int memSize) { this.memSize = memSize; } public int getMemSize() { return memSize; } } /** * The AxboDataState interface defines the processing interface of each state in * the state machine. * * @author jan.illetschko@3kraft.com */ interface AxboDataState { /** * Process data in the current state. Needs to overridden in every instance of * the state machine enum. * * @param ctx */ void process(AxboDataContext ctx); } /** * Data context passed between states of the state machine. * * @author jan.illetschko@3kraft.com */ class AxboDataContext { private AxboDataState state; private int dataItem; private int[] buffer; private int bufferPos; private int command; private boolean ignore; /** * Initializes the context in state BEGIN. */ public AxboDataContext() { state = AxboDataStates.BEGIN; } /** * Retrieves the current state. * * @return the current state. */ public AxboDataState getState() { return state; } /** * Sets the current state. * * @param state the state. */ public void setState(AxboDataState state) { this.state = state; } /** * Gets the current data byte. * * @return the current data byte */ public int getDataItem() { return dataItem; } /** * Sets the current data byte. * * @param dataItem the byte. */ public void setDataItem(int dataItem) { this.dataItem = dataItem; } /** * Gets thh current data buffer. * * @return the bytes in the buffer. */ public int[] getBuffer() { return buffer; } /** * Sets the data buffer. * * @param buffer a buffer */ public void setBuffer(int[] buffer) { this.buffer = buffer; } /** * Get the current position in the buffer. * * @return the current position */ public int getBufferPos() { return bufferPos; } /** * Set the position into processed data buffer. * * @param bufferPos the position. */ public void setBufferPos(int bufferPos) { this.bufferPos = bufferPos; } public int getCommand() { return command; } public void setCommand(int command) { this.command = command; } public boolean isIgnore() { return ignore; } public void setIgnore(boolean ignore) { this.ignore = ignore; } } /** * The state machine for parsing the data chunks read from the serial interface. * * @author jan.illetschko@3kraft.com */ enum AxboDataStates implements AxboDataState { /** * State begin. */ BEGIN() { @Override public void process(AxboDataContext ctx) { switch (ctx.getDataItem()) { case AxboDataParser.STX: ctx.setState(AxboDataStates.STX); break; case AxboDataParser.ACK: ctx.setState(AxboDataStates.ACK); break; case AxboDataParser.NACK: ctx.setState(AxboDataStates.NACK); break; } } }, /** * State ACK. Acknowledge received. */ ACK() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { // switch to state begin ctx.setState(BEGIN); // notify serial interface implementation about data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); } }, /** * State NACK. Not Acknowledge received. */ NACK() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { // switch to state begin ctx.setState(BEGIN); // notify serial interface implementation about data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); } }, /** * State STX. Start transaction received. */ STX() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { // switch to data processing ctx.setIgnore(ctx.getDataItem() == AxboDataParser.TOGGLE_BYTE_OUTPUT_OFF || ctx.getDataItem() == AxboDataParser.TOGGLE_BYTE_OUTPUT_ON); ctx.setState(COMMAND); } }, COMMAND() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { // reset data buffer ctx.setCommand(ctx.getDataItem()); ctx.setBufferPos(0); ctx.setBuffer(new int[AxboDataParser.BUFFER_SIZE]); ctx.setState(DATA); } }, /** * State DATA. Data processing enabled. */ DATA() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { if (ctx.getDataItem() == AxboDataParser.DLE) { // escape character // process escape character. ctx.setState(AxboDataStates.DATA_ESCAPE); } else { // write data byte to data buffer writeToBuffer(ctx); } } }, /** * State DATA_ESCAPE. Handle escape characters in data chunk. */ DATA_ESCAPE() { /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { if (ctx.getDataItem() == AxboDataParser.ETX) { // ETX // end transaction reached ctx.setState(AxboDataStates.CHECKSUM); } else { // skip DATA_ESCAPE character and write current byte to buffer writeToBuffer(ctx); ctx.setState(AxboDataStates.DATA); } } }, /** * State CHECKSUM. Process data checksum. */ CHECKSUM() { private int pos = 0; /** * {@inheritDoc} */ @Override public void process(AxboDataContext ctx) { // process 2 checksum bytes if (pos > 0) { pos = 0; ctx.setState(AxboDataStates.BEGIN); // process data buffer if (!ctx.isIgnore()) { handleData(ctx.getCommand(), ctx.getBuffer()); } } else { pos++; } } }; /** * Writes a data byte to the buffer. Resizes the buffer if required. * * @param ctx the data context of the state machine. */ void writeToBuffer(final AxboDataContext ctx) { if (ctx.getBufferPos() >= ctx.getBuffer().length) { // end of buffer reached, resize buffer ctx.setBuffer(Arrays.copyOf(ctx.getBuffer(), ctx.getBuffer().length + AxboDataParser.BUFFER_SIZE)); } // write data byte to buffer ctx.getBuffer()[ctx.getBufferPos()] = ctx.getDataItem(); // increment buffer position pointer ctx.setBufferPos(ctx.getBufferPos() + 1); } /** * Process data buffer. Triggers various event based on data type. * * @param data the data buffer */ void handleData(final int command, final int[] data) { try { if (AxboDataParser.log.isDebugEnabled()) { AxboDataParser.log.debug("Data: " + ByteUtil.dumpByteArray(data)); } switch (command) { case AxboDataParser.STATUS_COMMAND_BYTE: // data type is info data // extract version info char[] version = { (char) data[0], (char) data[1], (char) data[2], (char) data[3], (char) data[4], (char) data[5], (char) data[6], (char) data[7] }; // extract hardware info char[] hw = { (char) data[9], (char) data[10] }; // extract rtc int[] rtc = { data[12], data[13], data[14] }; // read serial number if available StringBuffer serialNumber = new StringBuffer(""); for (int i = 16; i < 16 + 8; i++) { if ((data[i] >= 0 && data[i] <= 9) || (data[i] >= 48 && data[i] <= 57)) { serialNumber .append((char) (data[i] < 48 ? data[i] + 48 : data[i])); } } if (serialNumber.toString().equals("00000000")) { serialNumber = new StringBuffer(""); } // put an info event into the event queue final AxboInfo axboData = new AxboInfo(serialNumber.toString().trim(), new String(hw).trim(), new String(version).trim(), String.valueOf(ByteUtil.hexToDec(rtc))); final InfoEvent info = new InfoEvent(this, axboData); ApplicationEventDispatcher.getInstance().dispatchEvent(info); // notify data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); break; case AxboDataParser.MOVEMENT_COMMAND_BYTE: // data type is a movment record if (AxboDataParser.log.isDebugEnabled()) { AxboDataParser.log.debug("protocol type: " + (char) data[16]); } // reduce sender ids from 8 two 2 final SensorID sensorId = (data[0]) % 2 != 0 ? SensorID.P1 : SensorID.P2; // create movement event from record final MovementData movementData = new MovementData(); final Calendar cal = GregorianCalendar.getInstance(); cal.clear(); cal.set(2000 + Integer.valueOf("" + (char) data[1] + (char) data[2]), Integer.valueOf(("" + (char) data[3] + (char) data[4])) - 1, Integer.valueOf("" + (char) data[5] + (char) data[6]), Integer.valueOf("" + (char) data[7] + (char) data[8]), Integer.valueOf("" + (char) data[9] + (char) data[10]), Integer.valueOf("" + (char) data[11] + (char) data[12])); movementData.setTimestamp(cal.getTime()); movementData.setMovementsX(ByteUtil.upperNibble(data[14]) + ByteUtil. lowerNibble(data[14])); movementData.setMovementsY(ByteUtil.upperNibble(data[15]) + ByteUtil .lowerNibble(data[15])); final MovementEvent movementEvent = new MovementEvent(this, movementData, sensorId.toString(), "" + (char) data[16], data); if (AxboDataParser.log.isDebugEnabled()) { AxboDataParser.log.debug("movement event: " + movementEvent); } // put the event into the event queue ApplicationEventDispatcher.getInstance().dispatchEvent( movementEvent); // notify data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); break; case AxboDataParser.DUMMY_COMMAND_BYTE: // set the aXbo memory size to 1 MB AxboDataParser.INSTANCE.setMemSize(1); // notify data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); break; case AxboDataParser.DUMMY_COMMAND_BYTE_HW12: // set the aXbo memory size to 2 MB for new Hardware AxboDataParser.INSTANCE.setMemSize(2); // notify data processed successfully DeviceContext.getDeviceType().getDataInterface().dataReceived(); break; default: if (AxboDataParser.log.isDebugEnabled()) AxboDataParser.log.debug("Unhandled command: " + ByteUtil.dumpByte( command)); } } catch (RuntimeException ex) { AxboDataParser.log.error("failed to process data:" + ByteUtil. dumpByteArray(data), ex); } } }