ducky's telemetry library

Files at this revision

API Documentation at this revision

Comitter:
ikrase
Date:
Tue Mar 31 19:56:00 2015 +0000
Commit message:
imported telemetry, added include guard to telemetry-mbed.h

Changed in this revision

telemetry-data.cpp Show annotated file Show diff for this revision Revisions of this file
telemetry-mbed-hal.cpp Show annotated file Show diff for this revision Revisions of this file
telemetry-mbed.h Show annotated file Show diff for this revision Revisions of this file
telemetry.cpp Show annotated file Show diff for this revision Revisions of this file
telemetry.h Show annotated file Show diff for this revision Revisions of this file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/telemetry-data.cpp	Tue Mar 31 19:56:00 2015 +0000
@@ -0,0 +1,90 @@
+/*
+ * telemetry-data.cpp
+ *
+ *  Created on: Mar 2, 2015
+ *      Author: Ducky
+ *
+ * Implementation for Telemetry Data classes.
+ */
+#include <telemetry.h>
+#include <string.h>
+
+namespace telemetry {
+void packet_write_string(TransmitPacketInterface& packet, const char* str) {
+  // TODO: move into HAL for higher performance?
+  while (*str != '\0') {
+    packet.write_uint8(*str);
+    str++;
+  }
+  packet.write_uint8('\0');
+}
+
+size_t Data::get_header_kvrs_length() {
+  return 1 + strlen(internal_name) + 1
+      + 1 + strlen(display_name) + 1
+      + 1 + strlen(units) + 1;
+}
+
+void Data::write_header_kvrs(TransmitPacketInterface& packet) {
+  packet.write_uint8(RECORDID_INTERNAL_NAME);
+  packet_write_string(packet, internal_name);
+  packet.write_uint8(RECORDID_DISPLAY_NAME);
+  packet_write_string(packet, display_name);
+  packet.write_uint8(RECORDID_UNITS);
+  packet_write_string(packet, units);
+}
+
+template<>
+uint8_t Numeric<uint8_t>::get_subtype() {
+  return NUMERIC_SUBTYPE_UINT;
+}
+template<>
+void Numeric<uint8_t>::serialize_data(uint8_t value, TransmitPacketInterface& packet) {
+  packet.write_uint8(value);
+}
+template<>
+uint8_t Numeric<uint8_t>::deserialize_data(ReceivePacketBuffer& packet) {
+  return packet.read_uint8();
+}
+
+template<>
+uint8_t Numeric<uint16_t>::get_subtype() {
+  return NUMERIC_SUBTYPE_UINT;
+}
+template<>
+void Numeric<uint16_t>::serialize_data(uint16_t value, TransmitPacketInterface& packet) {
+  packet.write_uint16(value);
+}
+template<>
+uint16_t Numeric<uint16_t>::deserialize_data(ReceivePacketBuffer& packet) {
+  return packet.read_uint16();
+}
+
+template<>
+uint8_t Numeric<uint32_t>::get_subtype() {
+  return NUMERIC_SUBTYPE_UINT;
+}
+template<>
+void Numeric<uint32_t>::serialize_data(uint32_t value, TransmitPacketInterface& packet) {
+  packet.write_uint32(value);
+}
+template<>
+uint32_t Numeric<uint32_t>::deserialize_data(ReceivePacketBuffer& packet) {
+  return packet.read_uint32();
+}
+
+// TODO: move into HAL
+template<>
+uint8_t Numeric<float>::get_subtype() {
+  return NUMERIC_SUBTYPE_FLOAT;
+}
+template<>
+void Numeric<float>::serialize_data(float value, TransmitPacketInterface& packet) {
+  packet.write_float(value);
+}
+template<>
+float Numeric<float>::deserialize_data(ReceivePacketBuffer& packet) {
+  return packet.read_float();
+}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/telemetry-mbed-hal.cpp	Tue Mar 31 19:56:00 2015 +0000
@@ -0,0 +1,39 @@
+/*
+ * telemetry-mbedo-hal.cpp
+ *
+ *  Created on: Mar 4, 2015
+ *      Author: Ducky
+ *
+ * Telemetry HAL for Serial on mBed.
+ */
+
+#ifdef __ARMCC_VERSION
+
+#include "telemetry-mbed.h"
+
+namespace telemetry {
+
+void MbedHal::transmit_byte(uint8_t data) {
+  // TODO: optimize with DMA
+  serial.putc(data);
+}
+
+size_t MbedHal::rx_available() {
+  return serial.rxBufferGetCount();
+}
+
+uint8_t MbedHal::receive_byte() {
+  return serial.getc();
+}
+
+void MbedHal::do_error(const char* msg) {
+  serial.printf("%s\r\n", msg);
+}
+
+uint32_t MbedHal::get_time_ms() {
+  return timer.read_ms();
+}
+
+}
+
+#endif // ifdef MBED
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/telemetry-mbed.h	Tue Mar 31 19:56:00 2015 +0000
@@ -0,0 +1,36 @@
+// Make this less hacky and detect properly
+#ifdef __ARMCC_VERSION
+
+#ifndef TELEMETRY_MBED_H
+#define TELEMETRY_MBED_H
+
+#include "telemetry.h"
+#include "mbed.h"
+#include "MODSERIAL.h"
+
+namespace telemetry {
+
+class MbedHal : public HalInterface {
+public:
+  MbedHal(MODSERIAL& serial) :
+    serial(serial) {
+	  timer.start();
+  }
+
+  virtual void transmit_byte(uint8_t data);
+  virtual size_t rx_available();
+  virtual uint8_t receive_byte();
+
+  virtual void do_error(const char* message);
+
+  virtual uint32_t get_time_ms();
+
+protected:
+  MODSERIAL& serial;
+  Timer timer;
+};
+
+}
+
+#endif // ifdef MBED
+#endif 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/telemetry.cpp	Tue Mar 31 19:56:00 2015 +0000
@@ -0,0 +1,332 @@
+/*
+ * telemetry.cpp
+ *
+ *  Created on: Mar 2, 2015
+ *      Author: Ducky
+ *
+ * Implementation for the base Telemetry class.
+ */
+
+#include <telemetry.h>
+
+namespace telemetry {
+
+size_t Telemetry::add_data(Data& new_data) {
+  if (data_count >= MAX_DATA_PER_TELEMETRY) {
+    do_error("MAX_DATA_PER_TELEMETRY limit reached.");
+    return 0;
+  }
+  if (header_transmitted) {
+    do_error("Cannot add new data after header transmitted.");
+    return 0;
+  }
+  data[data_count] = &new_data;
+  data_updated[data_count] = true;
+  data_count++;
+  return data_count - 1;
+}
+
+void Telemetry::mark_data_updated(size_t data_id) {
+  data_updated[data_id] = true;
+}
+
+void Telemetry::transmit_header() {
+  if (header_transmitted) {
+    do_error("Cannot retransmit header.");
+    return;
+  }
+
+  size_t packet_legnth = 2; // opcode + sequence
+  for (int data_idx = 0; data_idx < data_count; data_idx++) {
+    packet_legnth += 2; // data ID, data type
+    packet_legnth += data[data_idx]->get_header_kvrs_length();
+    packet_legnth += 1; // terminator record id
+  }
+  packet_legnth++;  // terminator "record"
+
+  FixedLengthTransmitPacket packet(hal, packet_legnth);
+
+  packet.write_uint8(OPCODE_HEADER);
+  packet.write_uint8(packet_tx_sequence);
+  for (int data_idx = 0; data_idx < data_count; data_idx++) {
+    packet.write_uint8(data_idx+1);
+    packet.write_uint8(data[data_idx]->get_data_type());
+    data[data_idx]->write_header_kvrs(packet);
+    packet.write_uint8(RECORDID_TERMINATOR);
+  }
+  packet.write_uint8(DATAID_TERMINATOR);
+
+  packet.finish();
+
+  packet_tx_sequence++;
+  header_transmitted = true;
+}
+
+void Telemetry::do_io() {
+  transmit_data();
+  process_received_data();
+}
+
+void Telemetry::transmit_data() {
+  if (!header_transmitted) {
+    do_error("Must transmit header before transmitting data.");
+    return;
+  }
+
+  // Keep a local copy to make it more thread-safe
+  bool data_updated_local[MAX_DATA_PER_TELEMETRY];
+
+  size_t packet_legnth = 2; // opcode + sequence
+  for (int data_idx = 0; data_idx < data_count; data_idx++) {
+    data_updated_local[data_idx] = data_updated[data_idx];
+    data_updated[data_idx] = 0;
+    if (data_updated_local[data_idx]) {
+      packet_legnth += 1; // data ID
+      packet_legnth += data[data_idx]->get_payload_length();
+    }
+  }
+  packet_legnth++;  // terminator "record"
+
+  FixedLengthTransmitPacket packet(hal, packet_legnth);
+
+  packet.write_uint8(OPCODE_DATA);
+  packet.write_uint8(packet_tx_sequence);
+  for (int data_idx = 0; data_idx < data_count; data_idx++) {
+    if (data_updated_local[data_idx]) {
+      packet.write_uint8(data_idx+1);
+      data[data_idx]->write_payload(packet);
+    }
+  }
+  packet.write_uint8(DATAID_TERMINATOR);
+
+  packet.finish();
+
+  packet_tx_sequence++;
+}
+
+void Telemetry::process_received_data() {
+  uint32_t current_time = hal.get_time_ms();
+
+  if (decoder_last_receive_ms <= current_time) {
+	if (!decoder_last_received && decoder_state != SOF && decoder_pos != 0
+		&& (decoder_last_receive_ms - current_time > DECODER_TIMEOUT_MS)) {
+	  decoder_pos = 0;
+	  packet_length = 0;
+	  decoder_state = SOF;
+	  hal.do_error("RX timeout");
+	}
+  } else {
+	// timer overflowed, do nothing
+  }
+  decoder_last_receive_ms = current_time;
+
+  decoder_last_received = false;
+  while (hal.rx_available()) {
+	decoder_last_received = true;
+
+    uint8_t rx_byte = hal.receive_byte();
+
+    if (decoder_state == SOF) {
+      if (rx_byte == SOF_SEQ[decoder_pos]) {
+        decoder_pos++;
+        if (decoder_pos >= (sizeof(SOF_SEQ) / sizeof(SOF_SEQ[0]))) {
+          decoder_pos = 0;
+          packet_length = 0;
+          decoder_state = LENGTH;
+        }
+      } else {
+        decoder_pos = 0;
+        // TODO: pass rest of data through
+      }
+    } else if (decoder_state == LENGTH) {
+      packet_length = (packet_length << 8) | rx_byte;
+      decoder_pos++;
+      if (decoder_pos >= LENGTH_SIZE) {
+        decoder_pos = 0;
+        decoder_state = DATA;
+      }
+    } else if (decoder_state == DATA) {
+      received_packet.add_byte(rx_byte);
+      decoder_pos++;
+      if (decoder_pos >= packet_length) {
+        process_received_packet();
+
+        decoder_pos = 0;
+        if (rx_byte == SOF_SEQ[0]) {
+          decoder_state = DATA_DESTUFF_END;
+        } else {
+          decoder_state = SOF;
+        }
+      } else {
+        if (rx_byte == SOF_SEQ[0]) {
+          decoder_state = DATA_DESTUFF;
+        }
+      }
+    } else if (decoder_state == DATA_DESTUFF) {
+      decoder_state = DATA;
+    } else if (decoder_state == DATA_DESTUFF_END) {
+      decoder_state = SOF;
+    }
+  }
+}
+
+void Telemetry::process_received_packet() {
+  uint8_t opcode = received_packet.read_uint8();
+  if (opcode == OPCODE_DATA) {
+    uint8_t data_id = received_packet.read_uint8();
+    while (data_id != DATAID_TERMINATOR) {
+      if (data_id < data_count + 1) {
+        data[data_id - 1]->set_from_packet(received_packet);
+      } else {
+        hal.do_error("Unknown data ID");
+      }
+      data_id = received_packet.read_uint8();
+    }
+  } else {
+    hal.do_error("Unknown opcode");
+  }
+}
+
+size_t Telemetry::receive_available() {
+  // TODO: implement me
+  return 0;
+}
+
+uint8_t read_receive() {
+  // TODO: implement me
+  return 0;
+}
+
+FixedLengthTransmitPacket::FixedLengthTransmitPacket(HalInterface& hal,
+    size_t length) :
+        hal(hal),
+        length(length),
+        count(0) {
+  hal.transmit_byte(SOF1);
+  hal.transmit_byte(SOF2);
+
+  hal.transmit_byte((length >> 8) & 0xff);
+  hal.transmit_byte((length >> 0) & 0xff);
+
+  valid = true;
+}
+
+void FixedLengthTransmitPacket::write_byte(uint8_t data) {
+  if (!valid) {
+    hal.do_error("Writing to invalid packet");
+    return;
+  } else if (count + 1 > length) {
+    hal.do_error("Writing over packet length");
+    return;
+  }
+  hal.transmit_byte(data);
+  if (data == SOF1) {
+    hal.transmit_byte(0x00);  // TODO: proper abstraction and magic numbers
+  }
+  count++;
+}
+
+void FixedLengthTransmitPacket::write_uint8(uint8_t data) {
+  write_byte(data);
+}
+
+void FixedLengthTransmitPacket::write_uint16(uint16_t data) {
+  write_byte((data >> 8) & 0xff);
+  write_byte((data >> 0) & 0xff);
+}
+
+void FixedLengthTransmitPacket::write_uint32(uint32_t data) {
+  write_byte((data >> 24) & 0xff);
+  write_byte((data >> 16) & 0xff);
+  write_byte((data >> 8) & 0xff);
+  write_byte((data >> 0) & 0xff);
+}
+
+void FixedLengthTransmitPacket::write_float(float data) {
+  // TODO: THIS IS ENDIANNESS DEPENDENT, ABSTRACT INTO HAL?
+  uint8_t *float_array = (uint8_t*) &data;
+  write_byte(float_array[3]);
+  write_byte(float_array[2]);
+  write_byte(float_array[1]);
+  write_byte(float_array[0]);
+}
+
+void FixedLengthTransmitPacket::finish() {
+  if (!valid) {
+    hal.do_error("Finish invalid packet");
+    return;
+  } else if (count != length) {
+    hal.do_error("TX packet under length");
+    return;
+  }
+
+  // TODO: add CRC check here
+}
+
+ReceivePacketBuffer::ReceivePacketBuffer(HalInterface& hal) :
+    hal(hal) {
+  new_packet();
+}
+
+void ReceivePacketBuffer::new_packet() {
+  packet_length = 0;
+  read_loc = 0;
+}
+
+void ReceivePacketBuffer::add_byte(uint8_t byte) {
+  if (packet_length >= MAX_RECEIVE_PACKET_LENGTH) {
+    hal.do_error("RX packet over length");
+    return;
+  }
+
+  data[packet_length] = byte;
+  packet_length++;
+}
+
+uint8_t ReceivePacketBuffer::read_uint8() {
+  if (read_loc + 1 > packet_length) {
+    hal.do_error("Read uint8 over length");
+    return 0;
+  }
+  read_loc += 1;
+  return data[read_loc - 1];
+}
+
+uint16_t ReceivePacketBuffer::read_uint16() {
+  if (read_loc + 2 > packet_length) {
+    hal.do_error("Read uint16 over length");
+    return 0;
+  }
+  read_loc += 2;
+  return ((uint16_t)data[read_loc - 2] << 8)
+       | ((uint16_t)data[read_loc - 1] << 0);
+}
+
+uint32_t ReceivePacketBuffer::read_uint32() {
+  if (read_loc + 4 > packet_length) {
+    hal.do_error("Read uint32 over length");
+    return 0;
+  }
+  read_loc += 4;
+  return ((uint32_t)data[read_loc - 4] << 24)
+       | ((uint32_t)data[read_loc - 3] << 16)
+       | ((uint32_t)data[read_loc - 2] << 8)
+       | ((uint32_t)data[read_loc - 1] << 0);
+}
+
+float ReceivePacketBuffer::read_float() {
+  if (read_loc + 4 > packet_length) {
+    hal.do_error("Read float over length");
+    return 0;
+  }
+  read_loc += 4;
+  float out = 0;
+  uint8_t* out_array = (uint8_t*)&out;
+  out_array[0] = data[read_loc - 1];
+  out_array[1] = data[read_loc - 2];
+  out_array[2] = data[read_loc - 3];
+  out_array[3] = data[read_loc - 4];
+  return out;
+}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/telemetry.h	Tue Mar 31 19:56:00 2015 +0000
@@ -0,0 +1,524 @@
+#ifndef _TELEMETRY_H_
+#define _TELEMETRY_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+namespace telemetry {
+
+#ifndef TELEMETRY_DATA_LIMIT
+#define TELEMETRY_DATA_LIMIT 16
+#endif
+
+// Maximum number of DataInterface objects a Telemetry object can hold.
+// Used for array sizing.
+const size_t MAX_DATA_PER_TELEMETRY = TELEMETRY_DATA_LIMIT;
+
+// Maximum payload size for a received telemetry packet.
+const size_t MAX_RECEIVE_PACKET_LENGTH = 255;
+
+// Various wire protocol constants.
+const uint8_t SOF1 = 0x05;  // start of frame byte 1
+const uint8_t SOF2 = 0x39;  // start of frame byte 2
+const uint8_t SOF_SEQ[] = {0x05, 0x39};
+
+const size_t LENGTH_SIZE = 2;
+
+// TODO: make these length independent
+
+const uint8_t OPCODE_HEADER = 0x81;
+const uint8_t OPCODE_DATA = 0x01;
+
+const uint8_t DATAID_TERMINATOR = 0x00;
+
+const uint8_t DATATYPE_NUMERIC = 0x01;
+const uint8_t DATATYPE_NUMERIC_ARRAY = 0x02;
+
+const uint8_t RECORDID_TERMINATOR = 0x00;
+const uint8_t RECORDID_INTERNAL_NAME = 0x01;
+const uint8_t RECORDID_DISPLAY_NAME = 0x02;
+const uint8_t RECORDID_UNITS = 0x03;
+
+const uint8_t RECORDID_OVERRIDE_CTL = 0x08;
+const uint8_t RECORDID_OVERRIDE_DATA = 0x08;
+
+const uint8_t RECORDID_NUMERIC_SUBTYPE = 0x40;
+const uint8_t RECORDID_NUMERIC_LENGTH = 0x41;
+const uint8_t RECORDID_NUMERIC_LIMITS = 0x42;
+const uint8_t RECORDID_ARRAY_COUNT = 0x50;
+
+const uint8_t NUMERIC_SUBTYPE_UINT = 0x01;
+const uint8_t NUMERIC_SUBTYPE_SINT = 0x02;
+const uint8_t NUMERIC_SUBTYPE_FLOAT = 0x03;
+
+const uint32_t DECODER_TIMEOUT_MS = 100;
+
+// Hardware abstraction layer for the telemetry server.
+class HalInterface {
+public:
+  virtual ~HalInterface() {}
+
+  // Write a byte to the transmit buffer.
+  virtual void transmit_byte(uint8_t data) = 0;
+  // Returns the number of bytes available in the receive buffer.
+  virtual size_t rx_available() = 0;
+  // Returns the next byte in the receive stream. rx_available must return > 0.
+  virtual uint8_t receive_byte() = 0;
+
+  // TODO: more efficient block transmit operations?
+
+  // Called on a telemetry error.
+  virtual void do_error(const char* message) = 0;
+
+  // Return the current time in milliseconds. May overflow at any time.
+  virtual uint32_t get_time_ms() = 0;
+};
+
+// Abstract base class for building a packet to be transmitted.
+// Implementation is unconstrained - writes may either be buffered or passed
+// directly to the hardware transmit buffers.
+class TransmitPacketInterface {
+public:
+  virtual ~TransmitPacketInterface() {}
+
+  // Writes a 8-bit unsigned integer to the packet stream.
+  virtual void write_byte(uint8_t data) = 0;
+    
+  // Writes a 8-bit unsigned integer to the packet stream.
+  virtual void write_uint8(uint8_t data) = 0;
+  // Writes a 16-bit unsigned integer to the packet stream.
+  virtual void write_uint16(uint16_t data) = 0;
+  // Writes a 32-bit unsigned integer to the packet stream.
+  virtual void write_uint32(uint32_t data) = 0;
+  // Writes a float to the packet stream.
+  virtual void write_float(float data) = 0;
+
+  // Finish the packet and writes data to the transmit stream (if not already
+  // done). No more data may be written afterwards.
+  virtual void finish() = 0;
+};
+
+class ReceivePacketBuffer {
+public:
+  ReceivePacketBuffer(HalInterface& hal);
+
+  // Starts a new packet, resetting the packet length and read pointer.
+  void new_packet();
+
+  // Appends a new byte onto this packet, advancing the packet length
+  void add_byte(uint8_t byte);
+
+  // Reads a 8-bit unsigned integer from the packet stream, advancing buffer.
+  uint8_t read_uint8();
+  // Reads a 16-bit unsigned integer from the packet stream, advancing buffer.
+  uint16_t read_uint16();
+  // Reads a 32-bit unsigned integer from the packet stream, advancing buffer.
+  uint32_t read_uint32();
+  // Reads a float from the packet stream, advancing buffer.
+  float read_float();
+
+protected:
+  HalInterface& hal;
+
+  size_t packet_length;
+  size_t read_loc;
+  uint8_t data[MAX_RECEIVE_PACKET_LENGTH];
+};
+
+// Abstract base class for telemetry data objects.
+class Data {
+public:
+  Data(const char* internal_name, const char* display_name,
+      const char* units):
+      internal_name(internal_name),
+      display_name(display_name),
+      units(units) {};
+
+  virtual ~Data() {}
+
+  // Returns the data type code.
+  virtual uint8_t get_data_type() = 0;
+
+  // Returns the length of the header KVRs, in bytes. Does not include the
+  // terminator header.
+  virtual size_t get_header_kvrs_length();
+  // Writes the header KVRs to the transmit packet. Does not write the
+  // terminiator header.
+  virtual void write_header_kvrs(TransmitPacketInterface& packet);
+
+  // Returns the length of the payload, in bytes. Should be "fast".
+  virtual size_t get_payload_length() = 0;
+  // Writes the payload to the transmit packet. Should be "fast".
+  virtual void write_payload(TransmitPacketInterface& packet) = 0;
+
+  // Sets my value from the received packet, interpreting the current packet
+  // read position as my data type.
+  virtual void set_from_packet(ReceivePacketBuffer& packet) = 0;
+
+protected:
+  const char* internal_name;
+  const char* display_name;
+  const char* units;
+};
+
+// Telemetry Server object.
+class Telemetry {
+public:
+  Telemetry(HalInterface& hal) :
+    hal(hal),
+    data_count(0),
+    received_packet(ReceivePacketBuffer(hal)),
+    decoder_state(SOF),
+    decoder_pos(0),
+    packet_length(0),
+	decoder_last_received(false),
+	decoder_last_receive_ms(0),
+    header_transmitted(false),
+    packet_tx_sequence(0),
+    packet_rx_sequence(0) {};
+
+  // Associates a DataInterface with this object, returning the data ID.
+  size_t add_data(Data& new_data);
+
+  // Marks a data ID as updated, to be transmitted in the next packet.
+  void mark_data_updated(size_t data_id);
+
+  // Transmits header data. Must be called after all add_data calls are done
+  // and before and IO is done.
+  void transmit_header();
+
+  // Does IO, including transmitting telemetry packets. Should be called on
+  // a regular basis. Since this does IO, this may block depending on the HAL
+  // semantics.
+  void do_io();
+
+  // TODO: better docs defining in-band receive.
+  // Returns the number of bytes available in the receive stream.
+  size_t receive_available();
+  // Returns the next byte in the receive stream.
+  uint8_t read_receive();
+
+  // Calls the HAL's error function if some condition is false.
+  void do_error(const char* message) {
+    hal.do_error(message);
+  }
+
+protected:
+  // Transmits any updated data.
+  void transmit_data();
+
+  // Handles received data, splitting regular UART data from in-band packet
+  // data and processing received telemetry packets.
+  void process_received_data();
+
+  // Handles a received packet in received_packet.
+  void process_received_packet();
+
+  HalInterface& hal;
+
+  // Array of associated DataInterface objects. The index+1 is the
+  // DataInterface's data ID field.
+  Data* data[MAX_DATA_PER_TELEMETRY];
+  // Whether each data has been updated or not.
+  bool data_updated[MAX_DATA_PER_TELEMETRY];
+  // Count of associated DataInterface objects.
+  size_t data_count;
+
+  // Buffer holding the receive packet being assembled / parsed.
+  ReceivePacketBuffer received_packet;
+
+  enum DecoderState {
+    SOF,    // reading start-of-frame sequence (or just non-telemetry data)
+    LENGTH, // reading packet length
+    DATA,   // reading telemetry packet data
+    DATA_DESTUFF,     // reading a stuffed byte
+    DATA_DESTUFF_END  // last stuffed byte in a packet
+  } decoder_state;
+
+  size_t decoder_pos;
+  size_t packet_length;
+  bool decoder_last_received;
+  uint32_t decoder_last_receive_ms;
+
+  bool header_transmitted;
+
+  // Sequence number of the next packet to be transmitted.
+  uint8_t packet_tx_sequence;
+  uint8_t packet_rx_sequence; // TODO use this somewhere
+};
+
+// A telemetry packet with a length known before data is written to it.
+// Data is written directly to the hardware transmit buffers without packet
+// buffering. Assumes transmit buffers won't fill up.
+class FixedLengthTransmitPacket : public TransmitPacketInterface {
+public:
+  FixedLengthTransmitPacket(HalInterface& hal, size_t length);
+
+  virtual void write_byte(uint8_t data);
+
+  virtual void write_uint8(uint8_t data);
+  virtual void write_uint16(uint16_t data);
+  virtual void write_uint32(uint32_t data);
+  virtual void write_float(float data);
+
+  virtual void finish();
+
+protected:
+  HalInterface& hal;
+
+  // Predetermined length, in bytes, of this packet's payload, for sanity check.
+  size_t length;
+
+  // Current length, in bytes, of this packet's payload.
+  size_t count;
+
+  // Is the packet valid?
+  bool valid;
+};
+
+template <typename T> 
+class Numeric : public Data {
+public:
+  Numeric(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, T init_value):
+      Data(internal_name, display_name, units),
+      telemetry_container(telemetry_container),
+      value(init_value), min_val(init_value), max_val(init_value),
+      frozen(false) {
+    data_id = telemetry_container.add_data(*this);
+  }
+
+  T operator = (T b) {
+    if (!frozen) {
+      value = b;
+      telemetry_container.mark_data_updated(data_id);
+    }
+    return value;
+  }
+
+  operator T() {
+    return value;
+  }
+  
+  Numeric<T>& set_limits(T min, T max) {
+    min_val = min;
+    max_val = max;
+    return *this;
+  }
+  
+  virtual uint8_t get_data_type() { return DATATYPE_NUMERIC; }
+
+  virtual size_t get_header_kvrs_length() {
+    return Data::get_header_kvrs_length()
+        + 1 + 1   // subtype
+        + 1 + 1   // data length
+        + 1 + sizeof(value) + sizeof(value);  // limits
+  }
+
+  virtual void write_header_kvrs(TransmitPacketInterface& packet) {
+    Data::write_header_kvrs(packet);
+    packet.write_uint8(RECORDID_NUMERIC_SUBTYPE);
+    packet.write_uint8(get_subtype());
+    packet.write_uint8(RECORDID_NUMERIC_LENGTH);
+    packet.write_uint8(sizeof(value));
+    packet.write_uint8(RECORDID_NUMERIC_LIMITS);
+    serialize_data(min_val, packet);
+    serialize_data(max_val, packet);
+  }
+
+  uint8_t get_subtype();
+
+  virtual size_t get_payload_length() { return sizeof(value); }
+  virtual void write_payload(TransmitPacketInterface& packet) { serialize_data(value, packet); }
+  virtual void set_from_packet(ReceivePacketBuffer& packet) {
+    value = deserialize_data(packet);
+    telemetry_container.mark_data_updated(data_id); }
+  
+  void serialize_data(T data, TransmitPacketInterface& packet);
+  T deserialize_data(ReceivePacketBuffer& packet);
+
+protected:
+  Telemetry& telemetry_container;
+  size_t data_id;
+  T value;
+  T min_val, max_val;
+  bool frozen;
+};
+
+template <typename T, uint32_t array_count>
+class NumericArrayAccessor;
+
+// TODO: fix this partial specialization inheritance nightmare
+template <typename T, uint32_t array_count> 
+class NumericArrayBase : public Data {
+  friend class NumericArrayAccessor<T, array_count>;
+public:
+  NumericArrayBase(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, T elem_init_value):
+      Data(internal_name, display_name, units),
+      telemetry_container(telemetry_container),
+      min_val(elem_init_value), max_val(elem_init_value),
+      frozen(false) {
+    for (size_t i=0; i<array_count; i++) {
+      value[i] = elem_init_value;
+    }
+    data_id = telemetry_container.add_data(*this);
+  }
+
+  NumericArrayAccessor<T, array_count> operator[] (const int index) {
+    // TODO: add bounds checking here
+    // TODO: add "frozen" check
+    return NumericArrayAccessor<T, array_count>(*this, index);
+  }
+
+  NumericArrayBase<T, array_count>& set_limits(T min, T max) {
+    min_val = min;
+    max_val = max;
+    return *this;
+  }
+  
+  virtual uint8_t get_data_type() { return DATATYPE_NUMERIC_ARRAY; }
+
+  virtual size_t get_header_kvrs_length() {
+    return Data::get_header_kvrs_length()
+        + 1 + 1   // subtype
+        + 1 + 1   // data length
+        + 1 + 4   // array length
+        + 1 + sizeof(value[0]) + sizeof(value[0]);  // limits
+  }
+
+  virtual void write_header_kvrs(TransmitPacketInterface& packet) {
+    Data::write_header_kvrs(packet);
+    packet.write_uint8(RECORDID_NUMERIC_SUBTYPE);
+    packet.write_uint8(get_subtype());
+    packet.write_uint8(RECORDID_NUMERIC_LENGTH);
+    packet.write_uint8(sizeof(value[0]));
+    packet.write_uint8(RECORDID_ARRAY_COUNT);
+    packet.write_uint32(array_count);
+    packet.write_uint8(RECORDID_NUMERIC_LIMITS);
+    serialize_data(min_val, packet);
+    serialize_data(max_val, packet);
+  }
+
+  virtual uint8_t get_subtype() = 0;
+
+  virtual size_t get_payload_length() { return sizeof(value); }
+  virtual void write_payload(TransmitPacketInterface& packet) {
+    for (size_t i=0; i<array_count; i++) { serialize_data(this->value[i], packet); } }
+  virtual void set_from_packet(ReceivePacketBuffer& packet) {
+    for (size_t i=0; i<array_count; i++) { value[i] = deserialize_data(packet); }
+    telemetry_container.mark_data_updated(data_id); }
+
+  virtual void serialize_data(T data, TransmitPacketInterface& packet) = 0;
+  virtual T deserialize_data(ReceivePacketBuffer& packet) = 0;
+  
+protected:
+  Telemetry& telemetry_container;
+  size_t data_id;
+  T value[array_count];
+  T min_val, max_val;
+  bool frozen;
+};
+
+template <typename T, uint32_t array_count>
+class NumericArrayAccessor {
+public:
+  NumericArrayAccessor(NumericArrayBase<T, array_count>& container, size_t index) :
+    container(container), index(index) { }
+
+  T operator = (T b) {
+    if (!container.frozen) {
+      container.value[index] = b;
+      container.telemetry_container.mark_data_updated(container.data_id);
+    }
+    return container.value[index];
+  }
+
+  operator T() {
+    return container.value[index];
+  }
+
+protected:
+  NumericArrayBase<T, array_count>& container;
+  size_t index;
+};
+
+template <typename T, uint32_t array_count> 
+class NumericArray : public NumericArrayBase<T, array_count> {
+  NumericArray(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, T elem_init_value);
+  virtual uint8_t get_subtype();
+  virtual void write_payload(TransmitPacketInterface& packet);
+  virtual T deserialize_data(ReceivePacketBuffer& packet);
+};
+
+template <uint32_t array_count> 
+class NumericArray<uint8_t, array_count> : public NumericArrayBase<uint8_t, array_count> {
+public:
+  NumericArray(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, uint8_t elem_init_value):
+      NumericArrayBase<uint8_t, array_count>(
+          telemetry_container, internal_name, display_name,
+          units, elem_init_value) {};
+  virtual uint8_t get_subtype() {return NUMERIC_SUBTYPE_UINT; }
+  virtual void serialize_data(uint8_t data, TransmitPacketInterface& packet) {
+    packet.write_uint8(data); }
+  virtual uint8_t deserialize_data(ReceivePacketBuffer& packet) {
+    return packet.read_uint8(); }
+};
+
+template <uint32_t array_count> 
+class NumericArray<uint16_t, array_count> : public NumericArrayBase<uint16_t, array_count> {
+public:
+  NumericArray(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, uint16_t elem_init_value):
+      NumericArrayBase<uint16_t, array_count>(
+          telemetry_container, internal_name, display_name,
+          units, elem_init_value) {};
+  virtual uint8_t get_subtype() {return NUMERIC_SUBTYPE_UINT; }
+  virtual void serialize_data(uint16_t data, TransmitPacketInterface& packet) {
+    packet.write_uint16(data); }
+  virtual uint16_t deserialize_data(ReceivePacketBuffer& packet) {
+    return packet.read_uint16(); }
+};
+
+template <uint32_t array_count> 
+class NumericArray<uint32_t, array_count> : public NumericArrayBase<uint32_t, array_count> {
+public:
+  NumericArray(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, uint32_t elem_init_value):
+      NumericArrayBase<uint32_t, array_count>(
+          telemetry_container, internal_name, display_name,
+          units, elem_init_value) {};
+  virtual uint8_t get_subtype() {return NUMERIC_SUBTYPE_UINT; }
+  virtual void serialize_data(uint32_t data, TransmitPacketInterface& packet) {
+    packet.write_uint32(data); }
+  virtual uint32_t deserialize_data(ReceivePacketBuffer& packet) {
+    return packet.read_uint32(); }
+};
+
+template <uint32_t array_count> 
+class NumericArray<float, array_count> : public NumericArrayBase<float, array_count> {
+public:
+  NumericArray(Telemetry& telemetry_container, 
+      const char* internal_name, const char* display_name,
+      const char* units, float elem_init_value):
+      NumericArrayBase<float, array_count>(
+          telemetry_container, internal_name, display_name,
+          units, elem_init_value) {};
+  virtual uint8_t get_subtype() {return NUMERIC_SUBTYPE_FLOAT; }
+  virtual void serialize_data(float data, TransmitPacketInterface& packet) {
+    packet.write_float(data); }
+  virtual float deserialize_data(ReceivePacketBuffer& packet) {
+    return packet.read_float(); }
+};
+
+}
+
+#endif