nmea_plus 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -0
  3. data/{gem/lib → lib}/nmea_plus.rb +19 -3
  4. data/lib/nmea_plus/ais_message_factory.rb +23 -0
  5. data/{gem/lib → lib}/nmea_plus/generated_parser/parser.rb +0 -0
  6. data/{gem/lib → lib}/nmea_plus/generated_parser/tokenizer.rb +1 -1
  7. data/{gem/lib → lib}/nmea_plus/message/ais/base_ais.rb +0 -0
  8. data/{gem/lib → lib}/nmea_plus/message/ais/vdm.rb +5 -4
  9. data/{gem/lib → lib}/nmea_plus/message/ais/vdm_payload/vdm_msg.rb +0 -0
  10. data/{gem/lib → lib}/nmea_plus/message/ais/vdm_payload/vdm_msg1.rb +0 -0
  11. data/{gem/lib → lib}/nmea_plus/message/ais/vdm_payload/vdm_msg5.rb +0 -0
  12. data/{gem/lib → lib}/nmea_plus/message/ais/vdm_payload/vdm_msg8.rb +0 -0
  13. data/lib/nmea_plus/message/base.rb +235 -0
  14. data/{gem/lib → lib}/nmea_plus/message/nmea/aam.rb +0 -0
  15. data/{gem/lib → lib}/nmea_plus/message/nmea/alm.rb +0 -0
  16. data/{gem/lib → lib}/nmea_plus/message/nmea/apa.rb +0 -0
  17. data/{gem/lib → lib}/nmea_plus/message/nmea/apb.rb +0 -0
  18. data/{gem/lib → lib}/nmea_plus/message/nmea/base_nmea.rb +0 -0
  19. data/{gem/lib → lib}/nmea_plus/message/nmea/bod.rb +0 -0
  20. data/{gem/lib → lib}/nmea_plus/message/nmea/bwc.rb +0 -0
  21. data/{gem/lib → lib}/nmea_plus/message/nmea/bwr.rb +0 -0
  22. data/{gem/lib → lib}/nmea_plus/message/nmea/bww.rb +0 -0
  23. data/{gem/lib → lib}/nmea_plus/message/nmea/dbk.rb +0 -0
  24. data/{gem/lib → lib}/nmea_plus/message/nmea/dbs.rb +0 -0
  25. data/{gem/lib → lib}/nmea_plus/message/nmea/dbt.rb +0 -0
  26. data/{gem/lib → lib}/nmea_plus/message/nmea/dcn.rb +0 -0
  27. data/{gem/lib → lib}/nmea_plus/message/nmea/dpt.rb +0 -0
  28. data/{gem/lib → lib}/nmea_plus/message/nmea/dtm.rb +0 -0
  29. data/{gem/lib → lib}/nmea_plus/message/nmea/fsi.rb +0 -0
  30. data/{gem/lib → lib}/nmea_plus/message/nmea/gbs.rb +0 -0
  31. data/{gem/lib → lib}/nmea_plus/message/nmea/gga.rb +0 -0
  32. data/{gem/lib → lib}/nmea_plus/message/nmea/glc.rb +0 -0
  33. data/{gem/lib → lib}/nmea_plus/message/nmea/gll.rb +0 -0
  34. data/{gem/lib → lib}/nmea_plus/message/nmea/gns.rb +0 -0
  35. data/{gem/lib → lib}/nmea_plus/message/nmea/grs.rb +0 -0
  36. data/{gem/lib → lib}/nmea_plus/message/nmea/gsa.rb +0 -0
  37. data/{gem/lib → lib}/nmea_plus/message/nmea/gst.rb +0 -0
  38. data/{gem/lib → lib}/nmea_plus/message/nmea/gsv.rb +0 -0
  39. data/{gem/lib → lib}/nmea_plus/message/nmea/gtd.rb +0 -0
  40. data/{gem/lib → lib}/nmea_plus/message/nmea/gxa.rb +0 -0
  41. data/{gem/lib → lib}/nmea_plus/message/nmea/hdg.rb +0 -0
  42. data/{gem/lib → lib}/nmea_plus/message/nmea/hdm.rb +0 -0
  43. data/{gem/lib → lib}/nmea_plus/message/nmea/hdt.rb +0 -0
  44. data/{gem/lib → lib}/nmea_plus/message/nmea/hfb.rb +0 -0
  45. data/{gem/lib → lib}/nmea_plus/message/nmea/hsc.rb +0 -0
  46. data/{gem/lib → lib}/nmea_plus/message/nmea/its.rb +0 -0
  47. data/{gem/lib → lib}/nmea_plus/message/nmea/lcd.rb +0 -0
  48. data/{gem/lib → lib}/nmea_plus/message/nmea/msk.rb +0 -0
  49. data/{gem/lib → lib}/nmea_plus/message/nmea/mss.rb +0 -0
  50. data/{gem/lib → lib}/nmea_plus/message/nmea/mtw.rb +0 -0
  51. data/{gem/lib → lib}/nmea_plus/message/nmea/mwv.rb +0 -0
  52. data/{gem/lib → lib}/nmea_plus/message/nmea/oln.rb +0 -0
  53. data/{gem/lib → lib}/nmea_plus/message/nmea/osd.rb +0 -0
  54. data/{gem/lib → lib}/nmea_plus/message/nmea/pashr.rb +0 -0
  55. data/{gem/lib → lib}/nmea_plus/message/nmea/r00.rb +0 -0
  56. data/{gem/lib → lib}/nmea_plus/message/nmea/rma.rb +0 -0
  57. data/{gem/lib → lib}/nmea_plus/message/nmea/rmb.rb +0 -0
  58. data/{gem/lib → lib}/nmea_plus/message/nmea/rmc.rb +0 -0
  59. data/{gem/lib → lib}/nmea_plus/message/nmea/rot.rb +0 -0
  60. data/{gem/lib → lib}/nmea_plus/message/nmea/rpm.rb +0 -0
  61. data/{gem/lib → lib}/nmea_plus/message/nmea/rsa.rb +0 -0
  62. data/{gem/lib → lib}/nmea_plus/message/nmea/rsd.rb +0 -0
  63. data/{gem/lib → lib}/nmea_plus/message/nmea/rte.rb +0 -0
  64. data/{gem/lib → lib}/nmea_plus/message/nmea/sfi.rb +0 -0
  65. data/{gem/lib → lib}/nmea_plus/message/nmea/stn.rb +0 -0
  66. data/{gem/lib → lib}/nmea_plus/message/nmea/tds.rb +0 -0
  67. data/{gem/lib → lib}/nmea_plus/message/nmea/tfi.rb +0 -0
  68. data/{gem/lib → lib}/nmea_plus/message/nmea/tpc.rb +0 -0
  69. data/{gem/lib → lib}/nmea_plus/message/nmea/tpr.rb +0 -0
  70. data/{gem/lib → lib}/nmea_plus/message/nmea/tpt.rb +0 -0
  71. data/{gem/lib → lib}/nmea_plus/message/nmea/trf.rb +0 -0
  72. data/{gem/lib → lib}/nmea_plus/message/nmea/ttm.rb +0 -0
  73. data/{gem/lib → lib}/nmea_plus/message/nmea/vbw.rb +0 -0
  74. data/{gem/lib → lib}/nmea_plus/message/nmea/vdr.rb +0 -0
  75. data/{gem/lib → lib}/nmea_plus/message/nmea/vhw.rb +0 -0
  76. data/{gem/lib → lib}/nmea_plus/message/nmea/vlw.rb +0 -0
  77. data/{gem/lib → lib}/nmea_plus/message/nmea/vpw.rb +0 -0
  78. data/{gem/lib → lib}/nmea_plus/message/nmea/vtg.rb +0 -0
  79. data/{gem/lib → lib}/nmea_plus/message/nmea/vwr.rb +0 -0
  80. data/{gem/lib → lib}/nmea_plus/message/nmea/wcv.rb +0 -0
  81. data/{gem/lib → lib}/nmea_plus/message/nmea/wnc.rb +0 -0
  82. data/{gem/lib → lib}/nmea_plus/message/nmea/wpl.rb +0 -0
  83. data/{gem/lib → lib}/nmea_plus/message/nmea/xdr.rb +0 -0
  84. data/{gem/lib → lib}/nmea_plus/message/nmea/xte.rb +0 -0
  85. data/{gem/lib → lib}/nmea_plus/message/nmea/xtr.rb +0 -0
  86. data/{gem/lib → lib}/nmea_plus/message/nmea/zda.rb +0 -0
  87. data/{gem/lib → lib}/nmea_plus/message/nmea/zfo.rb +0 -0
  88. data/{gem/lib → lib}/nmea_plus/message/nmea/ztg.rb +0 -0
  89. data/lib/nmea_plus/message_factory.rb +92 -0
  90. data/{gem/lib → lib}/nmea_plus/nmea_message_factory.rb +5 -0
  91. data/{gem/lib → lib}/nmea_plus/version.rb +1 -1
  92. metadata +117 -96
  93. data/gem/lib/nmea_plus/ais_message_factory.rb +0 -17
  94. data/gem/lib/nmea_plus/message/base.rb +0 -172
  95. data/gem/lib/nmea_plus/message_factory.rb +0 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dd8de99b8dfe9e24e6177dd8da3ded4bb1ee4ec8
4
- data.tar.gz: 3908a6334549db1c32e5e5a778d072bed59705f3
3
+ metadata.gz: 14b78eb70f5197e702eda48e3b297cfa9e6aed78
4
+ data.tar.gz: cc2f08b71f5841d8436f2e53baca38745aa67169
5
5
  SHA512:
6
- metadata.gz: d99daa0ad0e7157a3325a5dd7f4f8e60a7588b28f83605b1f8b44649cbb69a541f9a781e54f8a85122ad6da1a0a66454ac561b1fc2f9165565646f753b65bb96
7
- data.tar.gz: 54e1b1b32580ff94b291bde5249b2302301d244da2d770501d69cd9ab63439f8281c879ee570ad045c96a35ae498dc0d3eb2ffea23d1dc5f3f7d47e5945e6305
6
+ metadata.gz: f2db86ca89249ba01692b2cd4251eede675e1550defbb0bb80b486fad391e6ed2b1dbcfe9ef08c20b9854d8454510ba429cdb1f56c318d99f62d6e19a41ca2e8
7
+ data.tar.gz: 33a427ee981915f75cfdbf3f4143ddc2d6349db562f03fb59884f5f7562660c3f59d5f2e781b03addd7f33c1768750aa42fcf6430b39543a8594f02d4e1f2cfe
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # NMEA Ruby Gem (nmea_plus)
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/nmea_plus.svg)](https://rubygems.org/gems/nmea_plus)
4
+ [![Build Status](https://travis-ci.org/ifreecarve/nmea_plus.svg)](https://travis-ci.org/ifreecarve/nmea_plus)
5
+
6
+ [NMEA Plus](https://github.com/ifreecarve/nmea_plus) is a Ruby gem for parsing and decoding "GPS" messages: NMEA, AIS, and any other similar formats of short messaging typically used by marine equipment. It provides convenient access (by name) to the fields of each message type, and a stream reader designed for use with Ruby Blocks.
7
+
8
+
9
+ ## Install
10
+
11
+ `gem install nmea_plus`
12
+
13
+ ## Example
14
+
15
+ In its most basic use, you can decode messages as follows:
16
+
17
+ ```ruby
18
+ require 'nmea_plus'
19
+
20
+ decoder = NMEAPlus::Decoder.new
21
+ message = decoder.parse("$GPGLL,4916.45,N,12311.12,W,225444,A*00")
22
+ # message data -- specific to this message type
23
+ puts message.latitude # prints 49.27416666666666666666
24
+ puts message.longitude # prints -123.18533333333333333
25
+ puts message.fix_time # prints <today's date> 22:54:44 <your local time zone offset>
26
+ puts message.valid? # prints true
27
+ puts message.faa_mode # prints nil
28
+
29
+ # metadata
30
+ puts message.checksum_ok? # prints false -- because this checksum is made up
31
+ puts message.original # prints "$GPGLL,4916.45,N,12311.12,W,225444,A*00"
32
+ puts message.data_type # prints "GPGLL" -- what was specified in the message
33
+ puts message.interpreted_data_type # prints "GLL" -- the actual container used
34
+
35
+ # metadata that applies to multipart messages (also works for single messages)
36
+ puts message.all_messages_received? # prints true
37
+ puts message.all_checksums_ok? # prints false -- checksum is still made up
38
+
39
+ # safer way to do what we did above
40
+ if "GPGLL" == message.data_type # Alternately, if "GLL" == message.interpreted_data_type
41
+ puts message.latitude # prints 49.27416666666666666666
42
+ puts message.longitude # prints -123.18533333333333333
43
+ puts message.fix_time # prints <today's date> 22:54:44 <your local time zone offset>
44
+ puts message.valid? # prints true
45
+ puts message.faa_mode # prints nil
46
+ end
47
+ ```
48
+
49
+
50
+ Of course, decoding in practice is more complex than that. Some messages can have multiple parts, and AIS messages have their own complicated payload. NMEAPlus provides a SourceDecoder object that operates on IO objects (anything with `each_line` support) -- a File, SerialPort, etc. You can iterate over each message (literally `each_message`), or receive only fully assembled multipart messages by iterating over `each_complete_message`.
51
+
52
+ ```ruby
53
+ require 'nmea_plus'
54
+
55
+ input1 = "!AIVDM,2,1,0,A,58wt8Ui`g??r21`7S=:22058<v05Htp000000015>8OA;0sk,0*7B"
56
+ input2 = "!AIVDM,2,2,0,A,eQ8823mDm3kP00000000000,2*5D"
57
+ io_source = StringIO.new("#{input1}\n#{input2}") # source decoder works on any IO object
58
+
59
+ source_decoder = NMEAPlus::SourceDecoder.new(io_source)
60
+ source_decoder.each_complete_message do |message|
61
+ puts message_all_checksums_ok? # prints true -- the full message set has good checksums
62
+ puts message_all_messages_received? # prints true -- taken care of by each_complete_message
63
+ if "AIVDM" == message.data_type
64
+ puts message.ais.message_type # prints 5
65
+ puts message.ais.repeat_indicator # prints 0
66
+ puts message.ais.source_mmsi # prints 603916439
67
+ puts message.ais.ais_version # prints 0
68
+ puts message.ais.imo_number # prints 439303422
69
+ puts message.ais.callsign.strip # prints "ZA83R"
70
+ puts message.ais.name.strip # prints "ARCO AVON"
71
+ puts message.ais.ship_cargo_type # prints 69
72
+ puts message.ais.ship_dimension_to_bow # prints 113
73
+ puts message.ais.ship_dimension_to_stern # prints 31
74
+ puts message.ais.ship_dimension_to_port # prints 17
75
+ puts message.ais.ship_dimension_to_starboard # prints 11
76
+ puts message.ais.epfd_type # prints 0
77
+ puts message.ais.eta # prints <this year>-03-23 19:45:00 <your local time zone offset>
78
+ puts message.ais.static_draught # prints 13.2
79
+ puts message.ais.destination.strip # prints "HOUSTON"
80
+ puts message.ais.dte? # prints false
81
+ end
82
+ end
83
+
84
+ ```
85
+
86
+ ## Design documents
87
+
88
+ This gem was coded to accept the standard NMEA messages defined in the unoffical spec found here:
89
+ http://www.catb.org/gpsd/NMEA.txt
90
+
91
+ Because the message types are standard, if no override is found for a particular talker ID then the message will parse according to the command (the last 3 characters) of the data type. In other words, $GPGLL will use the general GLL message type. Currently, the following standard message types are supported:
92
+
93
+ > AAM, ALM, APA, APB, BOD, BWC, BWR, BWW, DBK, DBS, DBT, DCN, DPT, DTM, FSI, GBS, GGA, GLC, GLL, GNS, GRS, GSA, GST, GSV, GTD, GXA, HDG, HDM, HDT, HFB, HSC, ITS, LCD, MSK, MSS, MTW, MWV, OLN, OSD, R00, RMA, RMB, RMC, ROT, RPM, RSA, RSD, RTE, SFI, STN, TDS, TFI, TPC, TPR, TPT, TRF, TTM, VBW, VDR, VHW, VLW, VPW, VTG, VWR, WCV, WNC, WPL, XDR, XTE, XTR, ZDA, ZFO, ZTG
94
+
95
+
96
+ Additionally, AIS message type definitions were implemented from the unofficial spec found here:
97
+ http://catb.org/gpsd/AIVDM.html
98
+
99
+ > Currently AIVDM messages types 1, 2, 3, 5, and 8 are implemented.
100
+
101
+ Support for proprietary messages is also possible. PASHR is included as proof-of-concept.
102
+
103
+
104
+ ## Disclaimer
105
+
106
+ This module was written from information scraped together on the web, not from testing on actual devices. Please don't entrust your life or the safety of your ship to this code without doing your own rigorous testing.
107
+
108
+
109
+ ## Author
110
+
111
+ This gem was written by Ian Katz (ifreecarve@gmail.com) in 2015. It's released under the Apache 2.0 license.
112
+
113
+
114
+ ## See Also
115
+
116
+ * [Contributing](CONTRIBUTING.md)
@@ -3,11 +3,22 @@ require 'nmea_plus/version'
3
3
  require 'nmea_plus/generated_parser/parser'
4
4
  require 'nmea_plus/generated_parser/tokenizer'
5
5
 
6
+ # NMEAPlus contains classes for parsing and decoding NMEA and AIS messages.
7
+ # @author Ian Katz
6
8
  module NMEAPlus
9
+
10
+ # The NMEA source decoder wraps an IO object, converting each_line functionality
11
+ # to {#each_message} or {#each_complete_message}
7
12
  class SourceDecoder
13
+ # False by default.
14
+ # @return [bool] whether to throw an exception on lines that don't properly parse
8
15
  attr_accessor :throw_on_parse_fail
9
- attr_accessor :throw_on_unrecognized_type # typically for development
10
16
 
17
+ # False by default. Typically for development.
18
+ # @return [bool] whether to throw an exception on message types that aren't supported
19
+ attr_accessor :throw_on_unrecognized_type
20
+
21
+ # @param line_reader [IO] The source stream for messages
11
22
  def initialize(line_reader)
12
23
  unless line_reader.respond_to? :each_line
13
24
  fail ArgumentError, "line_reader must inherit from type IO (or implement each_line)"
@@ -17,7 +28,9 @@ module NMEAPlus
17
28
  @decoder = NMEAPlus::Decoder.new
18
29
  end
19
30
 
20
- # return each parsed message
31
+ # Executes the block for every valid NMEA message in the source stream
32
+ # @yield [NMEAPlus::Message] A parsed message
33
+ # @return [void]
21
34
  def each_message
22
35
  @source.each_line do |line|
23
36
  if @throw_on_parse_fail
@@ -34,7 +47,10 @@ module NMEAPlus
34
47
  end
35
48
  end
36
49
 
37
- # return messages grouped into multipart chains as required
50
+ # Executes the block for every valid NMEA message in the source stream, attempting to
51
+ # group multipart messages into message chains.
52
+ # @yield [NMEAPlus::Message] A parsed message that may contain subsequent parts
53
+ # @return [void]
38
54
  def each_complete_message
39
55
  partials = {}
40
56
  each_message do |msg|
@@ -0,0 +1,23 @@
1
+
2
+ require_relative 'message_factory'
3
+
4
+ require_relative 'message/ais/vdm'
5
+
6
+ module NMEAPlus
7
+
8
+ # Defines a factory for AIS messages, which will all use {NMEAPlus::Message::AIS::AISMessage} as their base
9
+ class AISMessageFactory < MessageFactory
10
+ # @return [String] The name of the parent module: AIS
11
+ def self.parent_module
12
+ "AIS"
13
+ end
14
+
15
+ # Match all AIS messages as their generic counterparts. AIVDM becomes VDM, etc.
16
+ # @param data_type [String] The data_type of the AIS message (e.g. the AIVDM of "$AIVDM,12,3,,4,5*00")
17
+ # @return [String] The data_type that we will attempt to use in decoding the message (e.g. VDM)
18
+ def self.alternate_data_type(data_type)
19
+ # match last 3 digits (get rid of talker)
20
+ data_type[2..4]
21
+ end
22
+ end
23
+ end
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
  module NMEAPlus
9
- class Decoder < Parser # not indented due to constraints of .rex format
9
+ class Decoder < Parser # This file is in .rex format, so no indenting. And yard docs are impossible.
10
10
  require 'strscan'
11
11
 
12
12
  class ScanError < StandardError ; end
File without changes
@@ -1,9 +1,5 @@
1
1
  require_relative "base_ais"
2
2
 
3
- require_relative "vdm_payload/vdm_msg1"
4
- require_relative "vdm_payload/vdm_msg5"
5
- require_relative "vdm_payload/vdm_msg8"
6
-
7
3
  =begin boilerplate for vdm payload objects
8
4
  require_relative 'vdm_msg'
9
5
 
@@ -21,6 +17,11 @@ end
21
17
 
22
18
  =end
23
19
 
20
+ require_relative "vdm_payload/vdm_msg1"
21
+ require_relative "vdm_payload/vdm_msg5"
22
+ require_relative "vdm_payload/vdm_msg8"
23
+
24
+
24
25
  module NMEAPlus
25
26
  module Message
26
27
  module AIS
@@ -0,0 +1,235 @@
1
+
2
+ module NMEAPlus
3
+
4
+ # The module containing all parsed message types.
5
+ # @see NMEAPlus::Message::Base
6
+ module Message
7
+
8
+ # The base message type, from which all others inherit
9
+ # @abstract
10
+ class Base
11
+ # make our own shortcut syntax for message attribute accessors
12
+ # @param name [String] What the accessor will be called
13
+ # @param field_num [Integer] The index of the field in the payload
14
+ # @param formatter [Symbol] The symbol for the formatting function to apply to the field (optional)
15
+ # @!macro [attach] field_reader
16
+ # @method $1
17
+ # @return field $2 of the payload, formatted with the function {#$3}
18
+ def self.field_reader(name, field_num, formatter = nil)
19
+ if formatter.nil?
20
+ self.class_eval("def #{name};@fields[#{field_num}];end")
21
+ else
22
+ self.class_eval("def #{name};#{formatter}(@fields[#{field_num}]);end")
23
+ end
24
+ end
25
+
26
+ # @return [String] The single character prefix for this NMEA 0183 message type
27
+ attr_accessor :prefix
28
+
29
+ # @return [String] The unprocessed payload of the message
30
+ attr_reader :payload
31
+
32
+ # @return [Array<String>] The payload of the message, split into fields
33
+ attr_reader :fields
34
+
35
+ # @return [String] The two-character checksum of the message
36
+ attr_accessor :checksum
37
+
38
+ # @return [String] The data type used by the {MessageFactory} to parse this message
39
+ attr_accessor :interpreted_data_type
40
+
41
+ # @return [NMEAPlus::Message] The next part of a multipart message, if available
42
+ attr_accessor :next_part
43
+
44
+ field_reader :data_type, 0, nil
45
+
46
+ # @!parse attr_reader :original
47
+ # @return [String] The original message
48
+ def original
49
+ "#{prefix}#{payload}*#{checksum}"
50
+ end
51
+
52
+ # @!parse attr_accessor :payload
53
+ def payload=(val)
54
+ @payload = val
55
+ @fields = val.split(',', -1)
56
+ end
57
+
58
+ # @return [bool] Whether the checksum calculated from the payload matches the checksum given in the message
59
+ def checksum_ok?
60
+ calculated_checksum.upcase == checksum.upcase
61
+ end
62
+
63
+ # return [bool] Whether the checksums for all available message parts are OK
64
+ def all_checksums_ok?
65
+ return false unless checksum_ok?
66
+ return true if @next_part.nil?
67
+ @next_part.all_checksums_ok?
68
+ end
69
+
70
+ # return [String] The calculated checksum for this payload as a two-character string
71
+ def calculated_checksum
72
+ "%02x" % @payload.each_byte.map(&:ord).reduce(:^)
73
+ end
74
+
75
+ # @!parse attr_reader :total_messages
76
+ # @abstract
77
+ # @see #message_number
78
+ # @return [Integer] The number of parts to this message
79
+ def total_messages
80
+ 1
81
+ end
82
+
83
+ # @!parse attr_reader :message_number
84
+ # @abstract
85
+ # @see #total_messages
86
+ # @return [Integer] The ordinal number of this message in its sequence
87
+ def message_number
88
+ 1
89
+ end
90
+
91
+ # Create a linked list of messages by appending a new message to the end of the chain that starts
92
+ # with this message. (O(n) implementation; message parts assumed to be < 10)
93
+ # @param msg [NMEAPlus::Message] The latest message in the chain
94
+ # @return [void]
95
+ def add_message_part(msg)
96
+ if @next_part.nil?
97
+ @next_part = msg
98
+ else
99
+ @next_part.add_message_part(msg)
100
+ end
101
+ end
102
+
103
+ # @return [bool] Whether all messages in a multipart message have been received.
104
+ def all_messages_received?
105
+ total_messages == highest_contiguous_index
106
+ end
107
+
108
+ # @return [Integer] The highest contiguous sequence number of linked message parts
109
+ # @see #message_number
110
+ # @see #_highest_contiguous_index
111
+ def highest_contiguous_index
112
+ _highest_contiguous_index(0)
113
+ end
114
+
115
+ # Helper function to calculate the contiguous index
116
+ # @param last_index [Integer] the index of the starting message
117
+ # @see #highest_contiguous_index
118
+ # @return [Integer] The highest contiguous sequence number of linked message parts
119
+ def _highest_contiguous_index(last_index)
120
+ mn = message_number # just in case this is expensive to compute
121
+ return last_index if mn - last_index != 1 # indicating a skip or restart
122
+ return mn if @next_part.nil? # indicating we're the last message
123
+ @next_part._highest_contiguous_index(mn) # recurse down
124
+ end
125
+
126
+ ######################### Conversion functions
127
+
128
+ # Convert A string latitude or longitude as fields into a signed number
129
+ # @param dm_string [String] An angular measurement in the form DDMM.MMM
130
+ # @param sign_letter [String] can be N,S,E,W
131
+ # @return [Float] A signed latitude or longitude
132
+ def _degrees_minutes_to_decimal(dm_string, sign_letter = "")
133
+ return nil if dm_string.nil? || dm_string.empty?
134
+ r = /(\d+)(\d{2}\.\d+)/ # (some number of digits) (2 digits for minutes).(decimal minutes)
135
+ m = r.match(dm_string)
136
+ raw = m.values_at(1)[0].to_f + (m.values_at(2)[0].to_f / 60)
137
+ _nsew_signed_float(raw, sign_letter)
138
+ end
139
+
140
+ # Use cardinal directions to assign positive or negative to mixed_val.
141
+ # Of possible directions NSEW (sign_letter) treat N/E as + and S/W as -
142
+ # @param mixed_val [String] input value, can be string or float
143
+ # @param sign_letter [String] can be N,S,E,W, or empty
144
+ # @return [Float] The input value signed as per the sign letter.
145
+ def _nsew_signed_float(mixed_val, sign_letter = "")
146
+ value = mixed_val.to_f
147
+ value *= -1 if !sign_letter.empty? && "SW".include?(sign_letter.upcase)
148
+ value
149
+ end
150
+
151
+ # integer or nil
152
+ # @param field [String] the field index to check
153
+ # @return [Integer] The value in the field or nil
154
+ def _integer(field)
155
+ return nil if field.nil? || field.empty?
156
+ field.to_i
157
+ end
158
+
159
+ # float or nil
160
+ # @param field [String] the field index to check
161
+ # @return [Float] The value in the field or nil
162
+ def _float(field)
163
+ return nil if field.nil? || field.empty?
164
+ field.to_f
165
+ end
166
+
167
+ # string or nil
168
+ # @param field [String] the field index to check
169
+ # @return [String] The value in the field or nil
170
+ def _string(field)
171
+ return nil if field.nil? || field.empty?
172
+ field
173
+ end
174
+
175
+ # hex to int or nil
176
+ # @param field [String] the field index to check
177
+ # @return [Integer] The value in the field or nil
178
+ def _hex_to_integer(field)
179
+ return nil if field.nil? || field.empty?
180
+ field.hex
181
+ end
182
+
183
+ # utc time or nil (HHMMSS or HHMMSS.SS)
184
+ # @param field [String] the field index to check
185
+ # @return [Time] The value in the field or nil
186
+ def _utctime_hms(field)
187
+ return nil if field.nil? || field.empty?
188
+ re_format = /(\d{2})(\d{2})(\d{2}(\.\d+)?)/
189
+ now = Time.now
190
+ begin
191
+ hms = re_format.match(field)
192
+ Time.new(now.year, now.month, now.day, hms[1].to_i, hms[2].to_i, hms[3].to_f)
193
+ rescue
194
+ nil
195
+ end
196
+ end
197
+
198
+ # time interval or nil (HHMMSS or HHMMSS.SS)
199
+ # @param field [String] the field index to check
200
+ # @return [Time] The value in the field or nil
201
+ def _interval_hms(field)
202
+ return nil if field.nil? || field.empty?
203
+ re_format = /(\d{2})(\d{2})(\d{2}(\.\d+)?)/
204
+ begin
205
+ hms = re_format.match(field)
206
+ Time.new(0, 0, 0, hms[1].to_i, hms[2].to_i, hms[3].to_f)
207
+ rescue
208
+ nil
209
+ end
210
+ end
211
+
212
+ # @param d_field [String] the date field index to check
213
+ # @param t_field [String] the time field index to check
214
+ # @return [Time] The value in the fields, or nil if either is not provided
215
+ def _utc_date_time(d_field, t_field)
216
+ return nil if t_field.nil? || t_field.empty?
217
+ return nil if d_field.nil? || d_field.empty?
218
+
219
+ # get formats and time
220
+ time_format = /(\d{2})(\d{2})(\d{2}(\.\d+)?)/
221
+ date_format = /(\d{2})(\d{2})(\d{2})/
222
+
223
+ # crunch numbers
224
+ begin
225
+ dmy = date_format.match(d_field)
226
+ hms = time_format.match(t_field)
227
+ Time.new(2000 + dmy[3].to_i, dmy[2].to_i, dmy[1].to_i, hms[1].to_i, hms[2].to_i, hms[3].to_f)
228
+ rescue
229
+ nil
230
+ end
231
+ end
232
+
233
+ end
234
+ end
235
+ end