/* FirmataParser.cpp Copyright (c) 2006-2008 Hans-Christoph Steiner. All rights reserved. Copyright (C) 2009-2016 Jeff Hoefs. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See file LICENSE.txt for further informations on licensing terms. */ //****************************************************************************** //* Includes //****************************************************************************** #include "FirmataParser.h" #include "FirmataConstants.h" using namespace firmata; //****************************************************************************** //* Constructors //****************************************************************************** /** * The FirmataParser class. * @param dataBuffer A pointer to an external buffer used to store parsed data * @param dataBufferSize The size of the external buffer */ FirmataParser::FirmataParser(uint8_t * const dataBuffer, size_t dataBufferSize) : dataBuffer(dataBuffer), dataBufferSize(dataBufferSize), executeMultiByteCommand(0), multiByteChannel(0), waitForData(0), parsingSysex(false), sysexBytesRead(0), currentAnalogCallbackContext((void *)NULL), currentDigitalCallbackContext((void *)NULL), currentReportAnalogCallbackContext((void *)NULL), currentReportDigitalCallbackContext((void *)NULL), currentPinModeCallbackContext((void *)NULL), currentPinValueCallbackContext((void *)NULL), currentReportFirmwareCallbackContext((void *)NULL), currentReportVersionCallbackContext((void *)NULL), currentDataBufferOverflowCallbackContext((void *)NULL), currentStringCallbackContext((void *)NULL), currentSysexCallbackContext((void *)NULL), currentSystemResetCallbackContext((void *)NULL), currentAnalogCallback((callbackFunction)NULL), currentDigitalCallback((callbackFunction)NULL), currentReportAnalogCallback((callbackFunction)NULL), currentReportDigitalCallback((callbackFunction)NULL), currentPinModeCallback((callbackFunction)NULL), currentPinValueCallback((callbackFunction)NULL), currentDataBufferOverflowCallback((dataBufferOverflowCallbackFunction)NULL), currentStringCallback((stringCallbackFunction)NULL), currentSysexCallback((sysexCallbackFunction)NULL), currentReportFirmwareCallback((versionCallbackFunction)NULL), currentReportVersionCallback((systemCallbackFunction)NULL), currentSystemResetCallback((systemCallbackFunction)NULL) { allowBufferUpdate = ((uint8_t *)NULL == dataBuffer); } //****************************************************************************** //* Public Methods //****************************************************************************** //------------------------------------------------------------------------------ // Serial Receive Handling /** * Parse data from the input stream. * @param inputData A single byte to be added to the parser. */ void FirmataParser::parse(uint8_t inputData) { uint8_t command; if (parsingSysex) { if (inputData == END_SYSEX) { //stop sysex byte parsingSysex = false; //fire off handler function processSysexMessage(); } else { //normal data byte - add to buffer bufferDataAtPosition(inputData, sysexBytesRead); ++sysexBytesRead; } } else if ( (waitForData > 0) && (inputData < 128) ) { --waitForData; bufferDataAtPosition(inputData, waitForData); if ( (waitForData == 0) && executeMultiByteCommand ) { // got the whole message switch (executeMultiByteCommand) { case ANALOG_MESSAGE: if (currentAnalogCallback) { (*currentAnalogCallback)(currentAnalogCallbackContext, multiByteChannel, (dataBuffer[0] << 7) + dataBuffer[1]); } break; case DIGITAL_MESSAGE: if (currentDigitalCallback) { (*currentDigitalCallback)(currentDigitalCallbackContext, multiByteChannel, (dataBuffer[0] << 7) + dataBuffer[1]); } break; case SET_PIN_MODE: if (currentPinModeCallback) (*currentPinModeCallback)(currentPinModeCallbackContext, dataBuffer[1], dataBuffer[0]); break; case SET_DIGITAL_PIN_VALUE: if (currentPinValueCallback) (*currentPinValueCallback)(currentPinValueCallbackContext, dataBuffer[1], dataBuffer[0]); break; case REPORT_ANALOG: if (currentReportAnalogCallback) (*currentReportAnalogCallback)(currentReportAnalogCallbackContext, multiByteChannel, dataBuffer[0]); break; case REPORT_DIGITAL: if (currentReportDigitalCallback) (*currentReportDigitalCallback)(currentReportDigitalCallbackContext, multiByteChannel, dataBuffer[0]); break; } executeMultiByteCommand = 0; } } else { // remove channel info from command byte if less than 0xF0 if (inputData < 0xF0) { command = inputData & 0xF0; multiByteChannel = inputData & 0x0F; } else { command = inputData; // commands in the 0xF* range don't use channel data } switch (command) { case ANALOG_MESSAGE: case DIGITAL_MESSAGE: case SET_PIN_MODE: case SET_DIGITAL_PIN_VALUE: waitForData = 2; // two data bytes needed executeMultiByteCommand = command; break; case REPORT_ANALOG: case REPORT_DIGITAL: waitForData = 1; // one data byte needed executeMultiByteCommand = command; break; case START_SYSEX: parsingSysex = true; sysexBytesRead = 0; break; case SYSTEM_RESET: systemReset(); break; case REPORT_VERSION: if (currentReportVersionCallback) (*currentReportVersionCallback)(currentReportVersionCallbackContext); break; } } } /** * @return Returns true if the parser is actively parsing data. */ bool FirmataParser::isParsingMessage(void) const { return (waitForData > 0 || parsingSysex); } /** * Provides a mechanism to either set or update the working buffer of the parser. * The method will be enabled when no buffer has been provided, or an overflow * condition exists. * @param dataBuffer A pointer to an external buffer used to store parsed data * @param dataBufferSize The size of the external buffer */ int FirmataParser::setDataBufferOfSize(uint8_t * dataBuffer, size_t dataBufferSize) { int result; if ( !allowBufferUpdate ) { result = __LINE__; } else if ((uint8_t *)NULL == dataBuffer) { result = __LINE__; } else { this->dataBuffer = dataBuffer; this->dataBufferSize = dataBufferSize; allowBufferUpdate = false; result = 0; } return result; } /** * Attach a generic sysex callback function to a command (options are: ANALOG_MESSAGE, * DIGITAL_MESSAGE, REPORT_ANALOG, REPORT DIGITAL, SET_PIN_MODE and SET_DIGITAL_PIN_VALUE). * @param command The ID of the command to attach a callback function to. * @param newFunction A reference to the callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(uint8_t command, callbackFunction newFunction, void * context) { switch (command) { case ANALOG_MESSAGE: currentAnalogCallback = newFunction; currentAnalogCallbackContext = context; break; case DIGITAL_MESSAGE: currentDigitalCallback = newFunction; currentDigitalCallbackContext = context; break; case REPORT_ANALOG: currentReportAnalogCallback = newFunction; currentReportAnalogCallbackContext = context; break; case REPORT_DIGITAL: currentReportDigitalCallback = newFunction; currentReportDigitalCallbackContext = context; break; case SET_PIN_MODE: currentPinModeCallback = newFunction; currentPinModeCallbackContext = context; break; case SET_DIGITAL_PIN_VALUE: currentPinValueCallback = newFunction; currentPinValueCallbackContext = context; break; } } /** * Attach a version callback function (supported option: REPORT_FIRMWARE). * @param command The ID of the command to attach a callback function to. * @param newFunction A reference to the callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(uint8_t command, versionCallbackFunction newFunction, void * context) { switch (command) { case REPORT_FIRMWARE: currentReportFirmwareCallback = newFunction; currentReportFirmwareCallbackContext = context; break; } } /** * Attach a system callback function (supported options are: SYSTEM_RESET, REPORT_VERSION). * @param command The ID of the command to attach a callback function to. * @param newFunction A reference to the callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(uint8_t command, systemCallbackFunction newFunction, void * context) { switch (command) { case REPORT_VERSION: currentReportVersionCallback = newFunction; currentReportVersionCallbackContext = context; break; case SYSTEM_RESET: currentSystemResetCallback = newFunction; currentSystemResetCallbackContext = context; break; } } /** * Attach a callback function for the STRING_DATA command. * @param command Must be set to STRING_DATA or it will be ignored. * @param newFunction A reference to the string callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(uint8_t command, stringCallbackFunction newFunction, void * context) { switch (command) { case STRING_DATA: currentStringCallback = newFunction; currentStringCallbackContext = context; break; } } /** * Attach a generic sysex callback function to sysex command. * @param command The ID of the command to attach a callback function to. * @param newFunction A reference to the sysex callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(uint8_t command, sysexCallbackFunction newFunction, void * context) { (void)command; currentSysexCallback = newFunction; currentSysexCallbackContext = context; } /** * Attach a buffer overflow callback * @param newFunction A reference to the buffer overflow callback function to attach. * @param context An optional context to be provided to the callback function (NULL by default). * @note The context parameter is provided so you can pass a parameter, by reference, to * your callback function. */ void FirmataParser::attach(dataBufferOverflowCallbackFunction newFunction, void * context) { currentDataBufferOverflowCallback = newFunction; currentDataBufferOverflowCallbackContext = context; } /** * Detach a callback function for a specified command (such as SYSTEM_RESET, STRING_DATA, * ANALOG_MESSAGE, DIGITAL_MESSAGE, etc). * @param command The ID of the command to detatch the callback function from. */ void FirmataParser::detach(uint8_t command) { switch (command) { case REPORT_FIRMWARE: attach(command, (versionCallbackFunction)NULL, NULL); break; case REPORT_VERSION: case SYSTEM_RESET: attach(command, (systemCallbackFunction)NULL, NULL); break; case STRING_DATA: attach(command, (stringCallbackFunction)NULL, NULL); break; case START_SYSEX: attach(command, (sysexCallbackFunction)NULL, NULL); break; default: attach(command, (callbackFunction)NULL, NULL); break; } } /** * Detach the buffer overflow callback * @param Any pointer of type dataBufferOverflowCallbackFunction. */ void FirmataParser::detach(dataBufferOverflowCallbackFunction) { currentDataBufferOverflowCallback = (dataBufferOverflowCallbackFunction)NULL; currentDataBufferOverflowCallbackContext = (void *)NULL; } //****************************************************************************** //* Private Methods //****************************************************************************** /** * Buffer abstraction to prevent memory corruption * @param data The byte to put into the buffer * @param pos The position to insert the byte into the buffer * @return writeError A boolean to indicate if an error occured * @private */ bool FirmataParser::bufferDataAtPosition(const uint8_t data, const size_t pos) { bool bufferOverflow = (pos >= dataBufferSize); // Notify of overflow condition if ( bufferOverflow && ((dataBufferOverflowCallbackFunction)NULL != currentDataBufferOverflowCallback) ) { allowBufferUpdate = true; currentDataBufferOverflowCallback(currentDataBufferOverflowCallbackContext); // Check if overflow was resolved during callback bufferOverflow = (pos >= dataBufferSize); } // Write data to buffer if no overflow condition persist if ( !bufferOverflow ) { dataBuffer[pos] = data; } return bufferOverflow; } /** * Transform 7-bit firmata message into 8-bit stream * @param bytec The encoded data byte length of the message (max: 16383). * @param bytev A pointer to the encoded array of data bytes. * @return The length of the decoded data. * @note The conversion will be done in place on the provided buffer. * @private */ size_t FirmataParser::decodeByteStream(size_t bytec, uint8_t * bytev) { size_t decoded_bytes, i; for ( i = 0, decoded_bytes = 0 ; i < bytec ; ++decoded_bytes, ++i ) { bytev[decoded_bytes] = bytev[i]; bytev[decoded_bytes] |= (uint8_t)(bytev[++i] << 7); } return decoded_bytes; } /** * Process incoming sysex messages. Handles REPORT_FIRMWARE and STRING_DATA internally. * Calls callback function for STRING_DATA and all other sysex messages. * @private */ void FirmataParser::processSysexMessage(void) { switch (dataBuffer[0]) { //first byte in buffer is command case REPORT_FIRMWARE: if (currentReportFirmwareCallback) { const size_t major_version_offset = 1; const size_t minor_version_offset = 2; const size_t string_offset = 3; // Test for malformed REPORT_FIRMWARE message (used to query firmware prior to Firmata v3.0.0) if ( 3 > sysexBytesRead ) { (*currentReportFirmwareCallback)(currentReportFirmwareCallbackContext, 0, 0, (const char *)NULL); } else { const size_t end_of_string = (string_offset + decodeByteStream((sysexBytesRead - string_offset), &dataBuffer[string_offset])); bufferDataAtPosition('\0', end_of_string); // NULL terminate the string (*currentReportFirmwareCallback)(currentReportFirmwareCallbackContext, (size_t)dataBuffer[major_version_offset], (size_t)dataBuffer[minor_version_offset], (const char *)&dataBuffer[string_offset]); } } break; case STRING_DATA: if (currentStringCallback) { const size_t string_offset = 1; const size_t end_of_string = (string_offset + decodeByteStream((sysexBytesRead - string_offset), &dataBuffer[string_offset])); bufferDataAtPosition('\0', end_of_string); // NULL terminate the string (*currentStringCallback)(currentStringCallbackContext, (const char *)&dataBuffer[string_offset]); } break; default: if (currentSysexCallback) (*currentSysexCallback)(currentSysexCallbackContext, dataBuffer[0], sysexBytesRead - 1, dataBuffer + 1); } } /** * Resets the system state upon a SYSTEM_RESET message from the host software. * @private */ void FirmataParser::systemReset(void) { size_t i; waitForData = 0; // this flag says the next serial input will be data executeMultiByteCommand = 0; // execute this after getting multi-byte data multiByteChannel = 0; // channel data for multiByteCommands for (i = 0; i < dataBufferSize; ++i) { dataBuffer[i] = 0; } parsingSysex = false; sysexBytesRead = 0; if (currentSystemResetCallback) (*currentSystemResetCallback)(currentSystemResetCallbackContext); }