svoop/ogn_client-ruby

View on GitHub
lib/ogn_client/message.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module OGNClient

  # Generic OGN flavoured APRS parser
  #
  # You can pass any raw OGN flavoured APRS message string to the +parse+ class
  # method and receive an instance of the appropriate subclass (+Comment+,
  # +Receiver+ or +Sender+) or +nil+ if the message string could not be parsed.
  #
  # Comment example:
  #   raw = "# aprsc 2.0.14-g28c5a6a 29 Jun 2014 07:46:15 GMT GLIDERN1 37.187.40.234:14580"
  #   obj = OGNClient::Message.parse(raw)   # => #<OGNClient::Comment:0x007feaf1012898>
  #   obj.comment   # => "aprsc 2.0.14-g28c5a6a 29 Jun 2014 07:46:15 GMT GLIDERN1 37.187.40.234:14580"
  #
  # Sender example:
  #   raw = "FLRDF0A52>APRS,qAS,LSTB:/220132h4658.70N/00707.72Ez090/054/A=001424 id06DF0A52 +020fpm +0.0rot 55.2dB 0e -6.2kHz"
  #   obj = OGNClient::Message.parse(raw)   # => #<OGNClient::Sender:0x007feaec1daba8>
  #   obj.id   # => "DF0A52"
  #
  # Malformed example:
  #   raw = "FOOBAR>not a valid message"
  #   obj = OGNClient::Message.parse(raw)   # => nil
  class Message

    POSITION_PATTERN = %r(^
      (?<callsign>.+?)>APRS,(?:.+?,){1,2}
      (?<receiver>.+?):[/>]
      (?<time>\d{6})h
      (?:(?<latitude>\d{4}\.\d{2}[NS]).(?<longitude>\d{5}\.\d{2}[EW]).)?
      (?:(?<heading>\d{3})/(?<ground_speed>\d{3}))?
      (?:/A=(?<altitude>\d{6}))?\s*
      (?:!W((?<latitude_enhancement>\d)(?<longitude_enhancement>\d))!)?
      (?:\s|$)
    )x

    attr_reader :raw
    attr_reader :callsign       # origin callsign
    attr_reader :receiver       # receiver callsign
    attr_reader :time           # zulu/UTC time with date
    attr_reader :longitude      # WGS84 degrees from -180 (W) to 180 (E)
    attr_reader :latitude       # WGS84 degrees from -90 (S) to 90 (N)
    attr_reader :altitude       # WGS84 meters above mean sea level QNH
    attr_reader :heading        # degrees from 1 to 360
    attr_reader :ground_speed   # kilometers per hour

    def self.parse(raw, date: nil)
      fail(OGNClient::MessageError, "raw message must be String but is #{raw.class}") unless raw.is_a? String
      raw = raw.chomp.force_encoding('ASCII-8BIT').encode('UTF-8')
      OGNClient::SenderBeacon.new.send(:parse, raw, date: date) ||
        OGNClient::ReceiverStatus.new.send(:parse, raw, date: date) ||
        OGNClient::ReceiverBeacon.new.send(:parse, raw, date: date) ||
        OGNClient::Comment.new.send(:parse, raw) ||
        fail(OGNClient::MessageError, "message payload parsing failed: `#{raw}'")
    end

    def to_s
      @raw
    end

    private

    def parse(raw, date: nil)
      @raw = raw
      @date = Date.parse(date) if date
      raw.match POSITION_PATTERN do |match|
        %i(callsign receiver time altitude).each do |attr|
          send("#{attr}=", match[attr]) if match[attr]
        end
        self.heading = match[:heading] if match[:heading] && match[:heading] != '000'
        self.ground_speed = match[:ground_speed] if match[:ground_speed] && match[:heading] != '000'
        self.longitude = [match[:longitude], match[:longitude_enhancement]] if match[:longitude]
        self.latitude = [match[:latitude], match[:latitude_enhancement]] if match[:latitude]
        self
      end or fail(OGNClient::MessageError, "message position parsing failed: `#{@raw}'")
      self
    end

    def callsign=(raw)
      @callsign = raw
    end

    def receiver=(raw)
      @receiver = raw
    end

    def time=(raw)
      if @date
        time = Time.new(@date.year, @date.month, @date.day, raw[0,2], raw[2,2], raw[4,2], 0)
      else
        now = Time.now.utc
        time = Time.new(now.year, now.month, now.day, raw[0,2], raw[2,2], raw[4,2], 0)
        time -= 86400 if time > now   # adjust date of beacons sent just before midnight
      end
      @time = time
    end

    def longitude=(raw)
      raw.first.match /(\d{3})([\d.]+)([EW])/
      @longitude = (($1.to_f + ("#{$2}#{raw.last}".to_f / 60)) * ($3 == 'E' ? 1 : -1)).round(6)
    end

    def latitude=(raw)
      raw.first.match /(\d{2})([\d.]+)([NS])/
      @latitude = (($1.to_f + ("#{$2}#{raw.last}".to_f / 60)) * ($3 == 'N' ? 1 : -1)).round(6)
    end

    def altitude=(raw)
      @altitude = (raw.to_i / 3.2808).round
    end

    def heading=(raw)
      @heading = raw.to_i
    end

    def ground_speed=(raw)
      @ground_speed = (raw.to_i * 1.852).round
    end

  end

end