code-lever/spektrum-log

View on GitHub
lib/spektrum/log/records.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Spektrum
  module Log

    # Represents a single record from the telemetry file.
    class Record

      attr_reader :timestamp

      def initialize(timestamp, raw_data)
        if raw_data.length != 16
          raise ArgumentError, "raw_data incorrectly sized (#{raw_data.length})"
        end
        @timestamp = timestamp
        @raw_data = raw_data
      end

      # Gets the 32-bit segmented hex string of the raw data for this record.
      #
      # @return [String] raw hex string for the record
      def raw_hex_string
        @raw_hex_string ||= @raw_data.unpack('H*')[0].gsub(/(.{8})(?=.)/, '\1 \2')
      end

      def type
        @type ||= byte_field(0)
      end

      # Determines if this record should be considered valid.  Definitions of valid
      # will vary by the type of record.
      #
      # @return [Boolean] true if the record is valid, false otherwise
      def valid?
        true
      end

      protected

      def byte_field(range)
        @raw_data[range].unpack('C')[0]
      end

      def hex_byte_field(range)
        @raw_data[range].unpack('H*')[0].to_i
      end

      def hex_string_field(range)
        @raw_data[range].unpack('H*')[0]
      end

      def two_byte_field(range, endian = :big)
        @raw_data[range].unpack(endian == :big ? 'n' : 'v')[0]
      end

      def four_byte_field(range, endian = :big)
        @raw_data[range].unpack(endian == :big ? 'N' : 'V')[0]
      end

    end

    class AltimeterRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      def altitude
        @altitude ||= two_byte_field(2..3)
      end

    end

    class BasicDataRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      def rpm(pole_count)
        raw_rpm * pole_count
      end

      def rpm?
        raw_rpm != 0xFFFF
      end

      # Gets the flight pack voltage data.
      #
      # @return [Float] flight voltage data, in volts
      # @note This conversion has been verified via Spektrum STi
      def voltage
        raw_voltage / 100.0
      end

      # Determines if there is flight voltage data contained within.
      #
      # @return [Boolean] true if there is flight voltage data, false otherwise
      def voltage?
        raw_voltage != 0xFFFF
      end

      def temperature(unit = :f)
        @temperature ||= two_byte_field(6..7)
        case unit
        when :f
          @temperature
        when :c
          (@temperature - 32) * (5.0 / 9.0)
        else
          @temperature
        end
      end

      def temperature?
        self.temperature != 0x7FFF
      end

      private

      def raw_rpm
        @raw_rpm ||= two_byte_field(2..3)
      end

      def raw_voltage
        @raw_voltage ||= two_byte_field(4..5)
      end

    end

    class FlightLogRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      # Gets the receiver pack voltage data.
      #
      # @return [Float] rx voltage data, in volts
      # @note This conversion has been verified via Spektrum STi
      def rx_voltage
        raw_rx_voltage / 100.0
      end

      # Determines if there is receiver voltage data contained within.
      #
      # @return [Boolean] true if there is rx voltage data, false otherwise
      def rx_voltage?
        raw_rx_voltage != 0x7FFF
      end

      private

      def raw_rx_voltage
        @raw_rx_voltage ||= two_byte_field(14..15)
      end

    end

    class GForceRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      def x
        @x ||= two_byte_field(2..3)
      end

      def y
        @y ||= two_byte_field(4..5)
      end

      def z
        @z ||= two_byte_field(6..7)
      end

      def x_max
        @x_max ||= two_byte_field(8..9)
      end

      def y_max
        @y_max ||= two_byte_field(10..11)
      end

      def z_max
        @z_max ||= two_byte_field(12..13)
      end

    end

    class GPSRecord1 < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      # Gets the altitude, in desired unit.
      #
      # @param unit one of :feet, :meters to define desired unit
      # @return [Float] altitude in the desired unit
      # @note This conversion has been verified via Spektrum STi
      def altitude(unit = :feet)
        @altitude ||= (hex_byte_field(3) * 100) + hex_byte_field(2)
        case unit
        when :feet
          @altitude * 0.32808399
        when :meters
          @altitude / 10.0
        else
          @altitude
        end
      end

      # Gets the latitude.  Positive values indicate North latitudes, negative
      # values indicate South.
      #
      # @return [Float] latitude in decimal-degress
      # @note This conversion has been verified via Spektrum STi
      # @note XXX Negative values are currently not supported!! XXX
      def latitude
        @latitude ||= build_latitude
      end

      # Gets the longitude.  Positive values indicate East longitudes, negative
      # values indicate West.
      #
      # @return [Float] longitude in decimal-degress
      # @note This conversion has been verified via Spektrum STi
      def longitude
        @lontitude ||= build_longitude
      end

      # Gets a composite coordinate value, containing longitude, latitude and
      # altitude in an array.
      #
      # @param unit unit for altitude, see {#altitude} for options
      # @return [Array] 3-element array of {#longitude}, {#latitude} and {#altitude}
      def coordinate
        [longitude, latitude, altitude(:meters)]
      end

      # Gets the current heading, in degrees.
      #
      # @return [Float] current heading
      # @note This conversion has been verified via Spektrum STi
      def heading
        @heading ||= (hex_byte_field(13) * 10) + (hex_byte_field(12) / 10.0)
      end

      def valid?
        !(latitude == 0.0 && longitude == 0.0 && altitude == 0.0)
      end

      private

      def build_latitude
        elements = 7.downto(4).map { |i| hex_string_field(i) }
        convert_latlon([0, elements].flatten)
      end

      def build_longitude
        elements = 11.downto(8).map { |i| hex_string_field(i) }

        # 100+ longitude indicator guesses (X marks proven invalid guess):
        #  X upper nybble of 13th byte
        #  - 2nd bit of 14th byte
        hundreds = ((byte_field(15) & 0x04) == 0x04) ? 1 : 0

        # +/- longitude indicator guesses (X marks proven invalid guess):
        #  - 1st bit of 14th byte (1 - pos, 0 - neg)
        multiplier = ((byte_field(15) & 0x02) == 0x02) ? 1 : -1

        multiplier * convert_latlon([hundreds, elements].flatten)
      end

      def convert_latlon(elts)
        raise ArgumentError unless elts.length == 5
        elts[0] * 100 + elts[1].to_i + ("#{elts[2]}.#{elts[3]}#{elts[4]}".to_f / 60.0)
      end

    end

    class GPSRecord2 < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      # Gets the speed, in desired unit.
      #
      # @param unit one of :knots, :mph, :kph to define desired unit
      # @return [Float] speed in the desired unit
      # @note This conversion has been verified via Spektrum STi
      def speed(unit = :knots)
        @speed ||= (hex_byte_field(3) * 100) + hex_byte_field(2)
        case unit
        when :knots
          @speed / 10.0
        when :mph
          @speed * 0.115078
        when :kph
          @speed * 0.1852
        else
          @speed
        end
      end

      # Gets the UTC 24-hour time.  In the format: 'HH:MM:SS:CS' (CS=centiseconds).
      #
      # @return [String] UTC 24-hour time
      # @note This conversion has been verified via Spektrum STi
      def time
        @time ||= '%.2i:%.2i:%.2i.%.2i' % 7.downto(4).map { |i| hex_byte_field(i) }
      end

      # Gets the number of satellites currently visible and in-use.
      #
      # @return [Integer] number of active satellites
      # @note This conversion has been verified via Spektrum STi
      def satellites
        @satellites ||= hex_byte_field(8)
      end

    end

    class SpeedRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

      # Gets the speed, in desired unit.
      #
      # @param unit one of :knots, :mph, :kph to define desired unit
      # @return [Float] speed in the desired unit
      def speed(unit = :knots)
        @speed ||= two_byte_field(2..3)
        case unit
        when :knots
          @speed * 0.539957
        when :mph
          @speed * 0.621371
        when :kph
          @speed
        else
          @speed
        end
      end

    end

    class MysteryRecord < Record

      def initialize(timestamp, raw_data)
        super timestamp, raw_data
      end

    end

    class Records

      @@types = {
          0x11 => SpeedRecord,
          0x12 => AltimeterRecord,
          0x14 => GForceRecord,
          0x16 => GPSRecord1,
          0x17 => GPSRecord2,
          0x7E => BasicDataRecord,
          0x7F => FlightLogRecord,
          0xFE => BasicDataRecord,
          0xFF => FlightLogRecord,
      }

      def self.create(type, timestamp, raw_data)
        @@types.fetch(type, MysteryRecord).new(timestamp, raw_data)
      end

    end

  end
end