/* * XMLReader_v5.cpp * Author: Alex St. Clair * Created: August 2019 * * This file implements a class to handle reading and parsing Strateole 2 XML * messages from the Zephyr gondola's On-Board Computer. * * The reader uses the serial buffers internal to the Arduino software core, * and this reader is designed for use on the Teensy 3.6, for which it is easy * and necessary to modify the core to increase the size of these buffers. * * Version 5 is a complete re-design of the XMLReader */ #include "XMLReader_v5.h" char inst_ids[4][8] = {"FLOATS", "RACHUTS", "LPC", "RATS"}; // global structs for received parameters DIB_Param_t dibParam = {0}; PIB_Param_t pibParam = {0}; RATS_Param_t ratsParam = {0}; LPC_Param_t lpcParam = {0}; MCB_Param_t mcbParam = {0}; PU_Param_t puParam = {0}; XMLReader::XMLReader(Stream * rxstream, Instrument_t inst) { rx_stream = rxstream; instrument = inst; } // get the next character from the stream and update the CRC inline bool XMLReader::ReadNextChar(char * new_char) { uint16_t c; uint8_t ret_char, msb, lsb; // make sure the new character is good, if not, return failure int read_ret = rx_stream->read(); //Serial.write(read_ret); if (read_ret == -1) return false; ret_char = (uint8_t) read_ret; // update CRC msb = working_crc >> 8; lsb = working_crc & 0xFF; c = ret_char ^ msb; c ^= (c >> 4); msb = (lsb ^ (c >> 3) ^ (c << 4)) & 255; lsb = (c ^ (c << 5)) & 255; working_crc = (msb << 8) + lsb; // assign the char to the receive location, return success *new_char = (char) ret_char; return true; } void XMLReader::ResetReader() { working_crc = crc_poly; crc_result = 0; num_fields = 0; // null-terminate all buffer first characters message_buff[0] = '\0'; for (int i = 0; i < MAX_MSG_FIELDS; i++) { fields[i][0] = '\0'; field_values[i][0] = '\0'; } } bool XMLReader::GetNewMessage() { // set a 0.1 second timeout uint32_t timeout = millis() + 200; char read_char = '\0'; // read the message type opening tag through the newline, verify the type if (!MessageTypeOpen(timeout)) { ResetReader(); return false; } // as long as there is a tab next, read a full field through the newline while (millis() < timeout) { if (rx_stream->available()) { if ('\t' == rx_stream->peek()) { // clear the \t and read the field if (!ReadNextChar(&read_char) || !ReadField(timeout)) { ResetReader(); rx_stream->flush(); return false; } } else { break; // no tab means no more fields } } } // read the message type closing tag through the newline if (!MessageTypeClose(timeout)) { ResetReader(); rx_stream->flush(); return false; } // save the crc result (working_crc will still be updating unnecessarily) crc_result = working_crc; // read and verify the full CRC through the newline if (!ReadVerifyCRC(timeout)) { ResetReader(); rx_stream->flush(); return false; } // parse the message if (!ParseMessage()) return false; // Add an extra 0.1 seconds to the timeout for the binary section timeout += 100; // read the binary section if it's a telecommand if (TC == zephyr_message && !ReadBinarySection(timeout)) { ResetReader(); rx_stream->flush(); return false; } ResetReader(); return true; } // -------------------------------------------------------- // Message Parsing // -------------------------------------------------------- bool XMLReader::ParseMessage() { unsigned int utemp = 0; // first field is always message id if (0 != strcmp(fields[0], "Msg")) return false; if (1 != sscanf(field_values[0], "%u", &utemp)) return false; if (utemp > 65535) return false; message_id = (uint16_t) utemp; // GPS message differs entirely from here on, parse it separately if (GPS == zephyr_message) return ParseGPSMessage(); // verify instrument id if (0 != strcmp(fields[1], "Inst")) return false; if (0 != strcmp(field_values[1], inst_ids[instrument])) return false; switch (zephyr_message) { case IM: // verify mode field if (0 != strcmp(fields[2], "Mode")) return false; // get mode if (0 == strcmp(field_values[2], "SB")) { zephyr_mode = MODE_STANDBY; } else if (0 == strcmp(field_values[2], "FL")) { zephyr_mode = MODE_FLIGHT; } else if (0 == strcmp(field_values[2], "LP")) { zephyr_mode = MODE_LOWPOWER; } else if (0 == strcmp(field_values[2], "SA")) { zephyr_mode = MODE_SAFETY; } else if (0 == strcmp(field_values[2], "EF")) { zephyr_mode = MODE_EOF; } else { return false; } break; case SAck: case RAAck: case TMAck: // verify ack field if (0 != strcmp(fields[2], "Ack")) return false; // get ack value if (0 == strcmp(field_values[2], "ACK")) { zephyr_ack = true; } else if (0 == strcmp(field_values[2], "NAK")) { zephyr_ack = false; } else { return false; } break; case SW: // shutdown warning has no more fields break; case TC: // verify binary length field if (0 != strcmp(fields[2], "Length")) return false; // get the binary length if (1 != sscanf(field_values[2], "%u", &utemp)) return false; if (utemp > MAX_TC_SIZE) return false; tc_length = (uint16_t) utemp; break; default: return false; } return true; } // Parse the GPS message, ensure that the GPS struct only ever contains valid data bool XMLReader::ParseGPSMessage() { float longtemp, lattemp, alttemp, szatemp, vbattemp, difftemp; unsigned int yeartemp, monthtemp, daytemp, hourtemp, minutetemp, secondtemp, qualitytemp; // verify the fields if (0 != strcmp(fields[1], "Date")) return false; if (0 != strcmp(fields[2], "Time")) return false; if (0 != strcmp(fields[3], "Lon")) return false; if (0 != strcmp(fields[4], "Lat")) return false; if (0 != strcmp(fields[5], "Alt")) return false; if (0 != strcmp(fields[6], "SZA")) return false; if (0 != strcmp(fields[7], "VBAT")) return false; if (0 != strcmp(fields[8], "Diff")) return false; if (0 != strcmp(fields[9], "Quality")) return false; // parse the date (YYYY/MM/DD) if (3 != sscanf(field_values[1], "%u/%u/%u", &yeartemp, &monthtemp, &daytemp)) return false; if (yeartemp > 2050) return false; if (monthtemp > 12) return false; if (daytemp > 31) return false; // parse the time (HH:MM:SS) if (3 != sscanf(field_values[2], "%u:%u:%u", &hourtemp, &minutetemp, &secondtemp)) return false; if (hourtemp > 23) return false; if (minutetemp > 59) return false; if (secondtemp > 59) return false; // don't handle leap seconds // parse the longitude if (1 != sscanf(field_values[3], "%f", &longtemp)) return false; // parse the latitude if (1 != sscanf(field_values[4], "%f", &lattemp)) return false; // parse the altitude if (1 != sscanf(field_values[5], "%f", &alttemp)) return false; // parse the solar zenith angle if (1 != sscanf(field_values[6], "%f", &szatemp)) return false; // parse the vbat if (1 != sscanf(field_values[7], "%f", &vbattemp)) return false; // parse the diff if (1 != sscanf(field_values[8], "%f", &difftemp)) return false; // parse the GPS fix quality if (1 != sscanf(field_values[9], "%u", &qualitytemp)) return false; if (0 == qualitytemp) return false; // ignore these messages // only assign values once the message has been parsed successfully zephyr_gps.year = (uint16_t) yeartemp; zephyr_gps.month = (uint8_t) monthtemp; zephyr_gps.day = (uint8_t) daytemp; zephyr_gps.hour = (uint8_t) hourtemp; zephyr_gps.minute = (uint8_t) minutetemp; zephyr_gps.second = (uint8_t) secondtemp; zephyr_gps.quality = qualitytemp; if (qualitytemp >= 1) { zephyr_gps.longitude = longtemp; zephyr_gps.latitude = lattemp; zephyr_gps.altitude = alttemp; zephyr_gps.solar_zenith_angle = szatemp; zephyr_gps.diff = difftemp; zephyr_gps.vbat = vbattemp; } return true; } // -------------------------------------------------------- // Read specific message parts into buffers // -------------------------------------------------------- bool XMLReader::MessageTypeOpen(uint32_t timeout) { int stream_peek; // wait while the buffer contains characters until the opening '<' is next stream_peek = rx_stream->peek(); while (millis() < timeout && -1 != stream_peek && '<' != stream_peek) { rx_stream->read(); // clear the char stream_peek = rx_stream->peek(); // look at the next } // ensure we have the opening character if ('<' != stream_peek) return false; // read in the message type opening tag and newline if (!ReadOpeningTag(timeout, message_buff, 8)) return false; if (!ReadSpecificChar(timeout, '\n')) return false; // determine the message type if (0 == strcmp(MSG_IM, message_buff)) { zephyr_message = IM; } else if (0 == strcmp(MSG_SAck, message_buff)) { zephyr_message = SAck; } else if (0 == strcmp(MSG_SW, message_buff)) { zephyr_message = SW; } else if (0 == strcmp(MSG_RAAck, message_buff)) { zephyr_message = RAAck; } else if (0 == strcmp(MSG_TMAck, message_buff)) { zephyr_message = TMAck; } else if (0 == strcmp(MSG_TC, message_buff)) { zephyr_message = TC; } else if (0 == strcmp(MSG_GPS, message_buff)) { zephyr_message = GPS; } else { // error zephyr_message = UNKNOWN; return false; } return true; } bool XMLReader::MessageTypeClose(uint32_t timeout) { char close_type[8] = {0}; // read the tag and the newline if (!ReadSpecificChar(timeout, '<')) return false; if (!ReadClosingTag(timeout, close_type, 8)) return false; if (!ReadSpecificChar(timeout, '\n')) return false; // verify that the closing message type matches the opening type if (0 != strcmp(close_type, message_buff)) return false; return true; } bool XMLReader::ReadField(uint32_t timeout) { int itr = 0; char close_field[8] = {0}; char new_char = '\0'; // read opening field tag if (!ReadOpeningTag(timeout, fields[num_fields], MAX_MSG_FIELDS)) return false; // read the field value until start of close tag or error while (millis() < timeout && itr < 15) { // if there's a character available, parse it if (ReadNextChar(&new_char)) { if ('<' == new_char) { break; } else { field_values[num_fields][itr++] = new_char; } } } // always null-terminate the buffer field_values[num_fields][itr] = '\0'; // verify we've started the closing tag if ('<' != new_char && !ReadSpecificChar(timeout, '<')) return false; // read closing field tag if (!ReadClosingTag(timeout, close_field, 8)) return false; // ensure the opening and closing field tags match if (0 != strcmp(close_field, fields[num_fields])) return false; // get the newline if (!ReadSpecificChar(timeout, '\n')) return false; num_fields++; return true; } bool XMLReader::ReadVerifyCRC(uint32_t timeout) { int itr = 0; unsigned int read_crc = 0; char crc_tag[4] = {0}; char crc_value[6] = {0}; char new_char = '\0'; // read and verify opening CRC tag if (!ReadOpeningTag(timeout, crc_tag, 4)) return false; if (0 != strcmp(crc_tag, "CRC")) return false; // read the CRC value until start of close tag or error while (millis() < timeout && itr < 5) { // if there's a character available, parse it if (ReadNextChar(&new_char)) { if ('<' == new_char) { break; } else { crc_value[itr++] = new_char; } } } // always null-terminate the buffer crc_value[itr] = '\0'; // verify we've started the closing tag if ('<' != new_char && !ReadSpecificChar(timeout, '<')) return false; // read and verify closing crc tag if (!ReadClosingTag(timeout, crc_tag, 4)) return false; if (0 != strcmp(crc_tag, "CRC")) return false; // convert the crc from the message to uint16_t if (1 != sscanf(crc_value, "%u", &read_crc)) return false; if (read_crc > 65535) return false; // get the trailing newline if it's there if ('\n' == rx_stream->peek()) { rx_stream->read(); } // return the CRC result return true; //((uint16_t) read_crc == crc_result); } bool XMLReader::ReadBinarySection(uint32_t timeout) { uint16_t itr = 0; uint16_t read_crc = 0; char rx_char = '\0'; // read "START" from the stream if (!ReadSpecificChar(timeout, 'S')) return false; if (!ReadSpecificChar(timeout, 'T')) return false; if (!ReadSpecificChar(timeout, 'A')) return false; if (!ReadSpecificChar(timeout, 'R')) return false; if (!ReadSpecificChar(timeout, 'T')) return false; // reset CRC for the binary section working_crc = crc_poly; // read the binary section into the telecommand buffer num_tcs = 0; tc_index = 0; curr_tc = 0; while (millis() < timeout && itr < tc_length) { if (ReadNextChar(&rx_char)) { tc_buffer[itr++] = rx_char; if (';' == rx_char) num_tcs++; } } // verify that we read all of the expected characters if (itr != tc_length) return false; // TC buffer is parsed as a char array string, so null-terminate it tc_buffer[itr] = '\0'; // store the CRC result for comparison with the transmitted value crc_result = working_crc; // read the first CRC byte (LSB) from the stream while (millis() < timeout && !ReadNextChar(&rx_char)); read_crc = (uint16_t) rx_char; // read the second CRC byte (MSB) from the stream while (millis() < timeout && !ReadNextChar(&rx_char)); read_crc |= (uint16_t) ((uint8_t) rx_char << 8); // verify that the stream ends with "END" if (!ReadSpecificChar(timeout, 'E')) return false; if (!ReadSpecificChar(timeout, 'N')) return false; if (!ReadSpecificChar(timeout, 'D')) return false; return true; //read_crc == crc_result; } // -------------------------------------------------------- // Generic Helper Functions // -------------------------------------------------------- // read the desired character, fail if timeout or wrong character bool XMLReader::ReadSpecificChar(uint32_t timeout, char specific_char) { char new_char; // wait until there's a character available while (millis() < timeout && !rx_stream->available()); // verify that we get the expected char if (!ReadNextChar(&new_char) || specific_char != new_char) return false; return true; } bool XMLReader::ReadOpeningTag(uint32_t timeout, char * buffer, uint8_t buff_size) { int itr = 0; char new_char = '\0'; if (!ReadSpecificChar(timeout, '<')) return false; // read the tag until close or error while (millis() < timeout && itr < (buff_size - 1)) { // if there's a character available, parse it if (ReadNextChar(&new_char)) { if ('>' == new_char) { break; } else { buffer[itr++] = new_char; } } } // always null-terminate the buffer buffer[itr] = '\0'; if (new_char == '>') { return true; } else { return ReadSpecificChar(timeout, '>'); } } // note: the leading '<' should already have been read before calling // this way, fields and CRC can read the '<' and know to stop bool XMLReader::ReadClosingTag(uint32_t timeout, char * buffer, uint8_t buff_size) { int itr = 0; char new_char = '\0'; if (!ReadSpecificChar(timeout, '/')) return false; // read the tag until close or error while (millis() < timeout && itr < (buff_size - 1)) { // if there's a character available, parse it if (ReadNextChar(&new_char)) { if ('>' == new_char) { break; } else { buffer[itr++] = new_char; } } } // always null-terminate the buffer buffer[itr] = '\0'; if (new_char == '>') { return true; } else { return ReadSpecificChar(timeout, '>'); } }