gps_pvt 0.7.2 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -765,9 +765,70 @@ __RINEX_CLK_TEXT__
765
765
  [:alpha, :beta].each{|k|
766
766
  puts "Iono #{k}: #{sn.iono_utc.send(k)}"
767
767
  }
768
+ proc{|raw|
769
+ expect(raw.size).to eq(10)
770
+ puts "Raw(IONO): #{raw.collect{|v| "0x%08X" % [v]}.join(', ')}"
771
+ iono2 = GPS::Ionospheric_UTC_Parameters::parse(raw)
772
+ [:alpha, :beta].collect{|f|
773
+ sn.iono_utc.send(f).zip(iono2.send(f))
774
+ }.flatten(1).zip([-30, -27, -24, -24, 11, 14, 16, 16]).each{|(a, b), sf|
775
+ expect(a).to be_within((2 ** sf) * 2).of(b)
776
+ }
777
+ [
778
+ [:A1, 2 ** -50], [:A0, 2 ** -30],
779
+ ].each{|k, sf|
780
+ #p [k, sf.to_f, sn.iono_utc.send(k) - iono2.send(k), sn.iono_utc.send(k), iono2.send(k)]
781
+ expect(sn.iono_utc.send(k)).to be_within((sf || 1).to_f * 2).of(iono2.send(k))
782
+ }
783
+ }.call(sn.iono_utc.dump(t_meas))
768
784
 
769
785
  meas.each{|prn, k, v|
770
786
  eph = sn.ephemeris(prn)
787
+ expect(eph.valid?(t_meas)).to eq(true)
788
+ sc2rad = 3.1415926535898
789
+ proc{|raw|
790
+ expect(raw.size).to eq(30)
791
+ eph2 = GPS::Ephemeris::new
792
+ raw.each_slice(10).with_index{|subframe, i|
793
+ puts "Raw(PRN:#{prn},SF:#{i+1}): #{subframe.collect{|v| "0x%08X" % [v]}.join(', ')}"
794
+ eph2.parse(subframe)
795
+ }
796
+ expect(eph.WN % 1024).to be(eph2.WN % 1024)
797
+ [
798
+ #:URA,
799
+ :SV_health, :iodc,
800
+ [:t_GD, 2 ** -31], [:t_oc, 2 ** 4], # SF1
801
+ [:a_f0, 2 ** -31], [:a_f1, 2 ** -43], [:a_f2, 2 ** -55], # SF1
802
+ :iode, # SF2
803
+ [:c_rs, 2 ** -5], [:delta_n, sc2rad * 2 ** -43], # SF2
804
+ [:M0, sc2rad * 2 ** -31], [:c_uc, 2 ** -29], [:e, 2 ** -33], # SF2
805
+ [:c_us, 2 ** -29], [:sqrt_A, 2 ** -19], [:t_oe, 2 ** 4], # SF2
806
+ :fit_interval, # SF2
807
+ [:c_ic, 2 ** -29], [:Omega0, sc2rad * 2 ** -31], [:c_is, 2 ** -29], # SF3
808
+ [:i0, sc2rad * 2 ** -31], [:c_rc, 2 ** -5], [:omega, sc2rad * 2 ** -31], # SF3
809
+ [:dot_Omega0, sc2rad * 2 ** -43], [:dot_i0, sc2rad * 2 ** -43], # SF3
810
+ ].each{|k, sf|
811
+ #p [k, sf.to_f, eph.send(k) - eph2.send(k), eph.send(k), eph2.send(k)]
812
+ expect(eph.send(k)).to be_within((sf || 1).to_f * 2).of(eph2.send(k))
813
+ }
814
+ }.call(eph.dump(t_meas))
815
+ proc{|raw| # Almanac -> Ephemeris
816
+ expect(raw.size).to eq(10)
817
+ puts "Raw(PRN:#{prn},Almanac): #{raw.collect{|v| "0x%08X" % [v]}.join(', ')}"
818
+ eph2 = GPS::Ephemeris::new
819
+ eph2.parse_almanac(raw)
820
+ [
821
+ :SV_health,
822
+ [:t_oc, 2 ** 12], [:a_f0, 2 ** -20], [:a_f1, 2 ** -38],
823
+ [:M0, sc2rad * 2 ** -23], [:e, 2 ** -21],
824
+ [:sqrt_A, 2 ** -11], [:t_oe, 2 ** 12],
825
+ [:Omega0, sc2rad * 2 ** -23], [:i0, sc2rad * 2 ** -19],
826
+ [:omega, sc2rad * 2 ** -23], [:dot_Omega0, sc2rad * 2 ** -38],
827
+ ].each{|k, sf|
828
+ #p [k, sf.to_f, eph.send(k) - eph2.send(k), eph.send(k), eph2.send(k)]
829
+ expect(eph.send(k)).to be_within((sf || 1).to_f * 2).of(eph2.send(k))
830
+ }
831
+ }.call(eph.dump_almanac(t_meas))
771
832
  puts "XYZ(PRN:#{prn}): #{eph.constellation(t_meas)[0].to_a} (iodc: #{eph.iodc}, iode: #{eph.iode})"
772
833
  }
773
834
 
@@ -0,0 +1,84 @@
1
+ #ifndef __BIT_COUNTER__
2
+ #define __BIT_COUNTER__
3
+
4
+ #include <climits>
5
+
6
+ using namespace std;
7
+
8
+ /**
9
+ * Counting bits utility
10
+ */
11
+ template <class T>
12
+ struct BitCounter {
13
+
14
+ static const int num_of_bits = CHAR_BIT * sizeof(T);
15
+
16
+ private:
17
+ template <int Interval>
18
+ static T make_mask(){
19
+ /*
20
+ * @see http://www.nminoru.jp/~nminoru/programming/bitcount.html
21
+ *
22
+ * 01010101010101010101010101010101 1 32
23
+ * 00110011001100110011001100110011 2 32
24
+ * 00001111000011110000111100001111 4 32
25
+ * 00000000111111110000000011111111 8 32
26
+ * 00000000000000001111111111111111 16 32
27
+ */
28
+
29
+ T mask(1);
30
+ for(int i(1); i < Interval; i++){
31
+ mask <<= 1;
32
+ mask |= 0x01;
33
+ }
34
+ /*
35
+ // Equivalent to the following if T is built-in type
36
+ mask <<= Interval;
37
+ mask = (mask & (-mask)) - 1;
38
+ */
39
+
40
+ T res(mask);
41
+ for(unsigned int i(1); i < num_of_bits / Interval / 2; i++){
42
+ res <<= Interval;
43
+ res <<= Interval;
44
+ res |= mask;
45
+ }
46
+ return res;
47
+ }
48
+
49
+ template <int Interval, class U = void>
50
+ struct count_loop {
51
+ static T run(T bits){
52
+ bits = count_loop<(Interval >> 1)>::run(bits);
53
+ static const T mask(make_mask<Interval>());
54
+ // bit shift and add operators are required at least
55
+ return (bits & mask) + ((bits >> Interval) & mask);
56
+ }
57
+ };
58
+ template <class U>
59
+ struct count_loop<1, U> {
60
+ static T run(T bits){
61
+ static const T mask(make_mask<1>());
62
+ return (bits & mask) + ((bits >> 1) & mask);
63
+ }
64
+ };
65
+
66
+ public:
67
+ /**
68
+ * Count bits in a value
69
+ * @param results
70
+ */
71
+ static T count(const T &v) {
72
+ return count_loop<(num_of_bits >> 1)>::run(v);
73
+ }
74
+
75
+ /**
76
+ * Count rightmost zeros before the first one (Number of trailing zeros)
77
+ * @param bits results
78
+ */
79
+ static T ntz(const T &v) {
80
+ return count((T)((~v) & (v - 1)));
81
+ }
82
+ };
83
+
84
+ #endif /* __BIT_COUNTER__ */
data/gps_pvt.gemspec CHANGED
@@ -58,6 +58,7 @@ Gem::Specification.new do |spec|
58
58
 
59
59
  # Uncomment to register a new dependency of your gem
60
60
  # spec.add_dependency "example-gem", "~> 1.0"
61
+ spec.add_dependency "rubyserial"
61
62
  spec.add_development_dependency "rake"
62
63
  spec.add_development_dependency "rake-compiler"
63
64
 
@@ -0,0 +1,51 @@
1
+ =begin
2
+ Receiver extension
3
+ =end
4
+
5
+ module GPS_PVT
6
+ class Receiver
7
+
8
+ # shortcut to access ephemeris registered in receiver
9
+ def ephemeris(t, sys, prn)
10
+ eph = case sys
11
+ when :GPS, :QZSS
12
+ critical{
13
+ @solver.gps_space_node.update_all_ephemeris(t)
14
+ @solver.gps_space_node.ephemeris(prn)
15
+ }
16
+ when :SBAS
17
+ critical{
18
+ @solver.sbas_space_node.update_all_ephemeris(t)
19
+ @solver.sbas_space_node.ephemeris(prn)
20
+ }
21
+ when :GLONASS
22
+ critical{
23
+ @solver.glonass_space_node.update_all_ephemeris(t)
24
+ @solver.glonass_space_node.ephemeris(prn)
25
+ }
26
+ else
27
+ return nil
28
+ end
29
+ return (eph.valid?(t) ? eph : nil)
30
+ end
31
+
32
+ def attach_online_ephemeris(uri_template = nil)
33
+ if (!uri_template) || (uri_template =~ /^\s*$/) then
34
+ uri_template = "ftp://gssc.esa.int/gnss/data/daily/%Y/brdc/BRDC00IGS_R_%Y%j0000_01D_MN.rnx.gz"
35
+ end
36
+ loader = proc{|t_meas|
37
+ utc = Time::utc(*t_meas.c_tm)
38
+ uri = URI::parse(utc.strftime(uri_template))
39
+ self.parse_rinex_nav(uri)
40
+ uri
41
+ }
42
+ run_orig = self.method(:run)
43
+ eph_list = {}
44
+ self.define_singleton_method(:run){|meas, t_meas, *args|
45
+ w_d = [t_meas.week, (t_meas.seconds.to_i / 86400)]
46
+ eph_list[w_d] ||= loader.call(t_meas)
47
+ run_orig.call(meas, t_meas, *args)
48
+ }
49
+ end
50
+ end
51
+ end
@@ -3,7 +3,7 @@ Receiver class to be an top level interface to a user
3
3
  (The origin is ninja-scan-light/tool/misc/receiver_debug.rb)
4
4
  =end
5
5
 
6
- require_relative 'GPS'
6
+ require 'gps_pvt/GPS' # in case GPS.so is generated under ext/gps_pvt
7
7
  require_relative 'util'
8
8
 
9
9
  module GPS_PVT
@@ -154,6 +154,7 @@ class Receiver
154
154
  :skip_exclusion => true, # default is to skip fault exclusion calculation
155
155
  }
156
156
  @debug = {}
157
+ @semaphore = Mutex::new
157
158
  solver_opts = [:gps_options, :sbas_options, :glonass_options].collect{|target|
158
159
  @solver.send(target)
159
160
  }
@@ -298,6 +299,24 @@ class Receiver
298
299
  :meas => Receiver::meas_items(output_options),
299
300
  }
300
301
  end
302
+
303
+ def critical(&b)
304
+ begin
305
+ @semaphore.synchronize{b.call}
306
+ rescue ThreadError # recovery from deadlock
307
+ b.call
308
+ end
309
+ end
310
+
311
+ class << self
312
+ def make_critical(fname)
313
+ f_orig = instance_method(fname)
314
+ define_method(fname){|*args, &b|
315
+ critical{f_orig.bind(self).call(*args, &b)}
316
+ }
317
+ end
318
+ private :make_critical
319
+ end
301
320
 
302
321
  GPS::Measurement.class_eval{
303
322
  proc{
@@ -331,7 +350,7 @@ class Receiver
331
350
  =end
332
351
 
333
352
  #@solver.gps_space_node.update_all_ephemeris(t_meas) # internally called in the following solver.solve
334
- pvt = @solver.solve(meas, t_meas)
353
+ pvt = critical{@solver.solve(meas, t_meas)}
335
354
  pvt.define_singleton_method(:rel_ENU){
336
355
  Coordinate::ENU::relative(xyz, ref_pos)
337
356
  } if (ref_pos && pvt.position_solved?)
@@ -378,60 +397,59 @@ class Receiver
378
397
  }
379
398
  }
380
399
 
381
- proc{
382
- eph_list = Hash[*((1..32).to_a + (193..202).to_a).collect{|prn|
400
+ def register_ephemeris(t_meas, sys, prn, bcast_data, *options)
401
+ @eph_list ||= Hash[*((1..32).to_a + (193..202).to_a).collect{|prn|
383
402
  eph = GPS::Ephemeris::new
384
403
  eph.svid = prn
385
404
  [prn, eph]
386
405
  }.flatten(1)]
387
- eph_glonass_list = Hash[*(1..24).collect{|num|
406
+ @eph_glonass_list ||= Hash[*(1..24).collect{|num|
388
407
  eph = GPS::Ephemeris_GLONASS::new
389
408
  eph.svid = num
390
409
  [num, eph]
391
410
  }.flatten(1)]
392
- define_method(:register_ephemeris){|t_meas, sys, prn, bcast_data, *options|
393
- opt = options[0] || {}
394
- case sys
395
- when :GPS, :QZSS
396
- next unless eph = eph_list[prn]
397
- sn = @solver.gps_space_node
398
- subframe, iodc_or_iode = eph.parse(bcast_data)
399
- if iodc_or_iode < 0 then
400
- begin
401
- sn.update_iono_utc(
402
- GPS::Ionospheric_UTC_Parameters::parse(bcast_data))
403
- [:alpha, :beta].each{|k|
404
- $stderr.puts "Iono #{k}: #{sn.iono_utc.send(k)}"
405
- } if false
406
- rescue
407
- end
408
- next
411
+ opt = options[0] || {}
412
+ case sys
413
+ when :GPS, :QZSS
414
+ return unless eph = @eph_list[prn]
415
+ sn = @solver.gps_space_node
416
+ subframe, iodc_or_iode = eph.parse(bcast_data)
417
+ if iodc_or_iode < 0 then
418
+ begin
419
+ sn.update_iono_utc(
420
+ GPS::Ionospheric_UTC_Parameters::parse(bcast_data))
421
+ [:alpha, :beta].each{|k|
422
+ $stderr.puts "Iono #{k}: #{sn.iono_utc.send(k)}"
423
+ } if false
424
+ rescue
409
425
  end
410
- if t_meas and eph.consistent? then
411
- eph.WN = ((t_meas.week / 1024).to_i * 1024) + (eph.WN % 1024)
412
- sn.register_ephemeris(prn, eph)
413
- eph.invalidate
414
- end
415
- when :SBAS
416
- case @solver.sbas_space_node.decode_message(bcast_data[0..7], prn, t_meas)
417
- when 26
418
- ['', "IGP broadcasted by PRN#{prn} @ #{Time::utc(*t_meas.c_tm)}",
419
- @solver.sbas_space_node.ionospheric_grid_points(prn)].each{|str|
420
- $stderr.puts str
421
- } if @debug[:SBAS_IGP]
422
- end if t_meas
423
- when :GLONASS
424
- next unless eph = eph_glonass_list[prn]
425
- leap_sec = @solver.gps_space_node.is_valid_utc ?
426
- @solver.gps_space_node.iono_utc.delta_t_LS :
427
- GPS::Time::guess_leap_seconds(t_meas)
428
- next unless eph.parse(bcast_data[0..3], leap_sec)
429
- eph.freq_ch = opt[:freq_ch] || 0
430
- @solver.glonass_space_node.register_ephemeris(prn, eph)
426
+ return
427
+ end
428
+ if t_meas and eph.consistent? then
429
+ eph.WN = ((t_meas.week / 1024).to_i * 1024) + (eph.WN % 1024)
430
+ sn.register_ephemeris(prn, eph)
431
431
  eph.invalidate
432
432
  end
433
- }
434
- }.call
433
+ when :SBAS
434
+ case @solver.sbas_space_node.decode_message(bcast_data[0..7], prn, t_meas)
435
+ when 26
436
+ ['', "IGP broadcasted by PRN#{prn} @ #{Time::utc(*t_meas.c_tm)}",
437
+ @solver.sbas_space_node.ionospheric_grid_points(prn)].each{|str|
438
+ $stderr.puts str
439
+ } if @debug[:SBAS_IGP]
440
+ end if t_meas
441
+ when :GLONASS
442
+ return unless eph = @eph_glonass_list[prn]
443
+ leap_sec = @solver.gps_space_node.is_valid_utc ?
444
+ @solver.gps_space_node.iono_utc.delta_t_LS :
445
+ GPS::Time::guess_leap_seconds(t_meas)
446
+ return unless eph.parse(bcast_data[0..3], leap_sec)
447
+ eph.freq_ch = opt[:freq_ch] || 0
448
+ @solver.glonass_space_node.register_ephemeris(prn, eph)
449
+ eph.invalidate
450
+ end
451
+ end
452
+ make_critical :register_ephemeris
435
453
 
436
454
  def parse_ubx(ubx_fname, &b)
437
455
  $stderr.print "Reading UBX file (%s) "%[ubx_fname]
@@ -505,6 +523,8 @@ class Receiver
505
523
  v
506
524
  }
507
525
  sys, svid = gnss_serial.call(*loader.call(36, 2).reverse)
526
+ # sigID check to restrict signal to L1 if version(>0); @see UBX-18010854
527
+ next if (packet[6 + 13] != 0) && (loader.call(38, 1, "C") != 0)
508
528
  case sys
509
529
  when :GPS, :SBAS, :QZSS;
510
530
  when :GLONASS
@@ -538,10 +558,13 @@ class Receiver
538
558
  register_ephemeris(
539
559
  t_meas,
540
560
  sys, svid,
541
- packet.slice(6 + 2, 40).each_slice(4).collect{|v|
542
- res = v.pack("C*").unpack("V")[0]
543
- (sys == :GPS) ? ((res & 0xFFFFFF) << 6) : res
544
- })
561
+ proc{|data|
562
+ case sys # adjust padding
563
+ when :GPS; data.collect!{|v| (v & 0xFFFFFF) << 6}
564
+ when :SBAS; data[7] <<= 6
565
+ end
566
+ data
567
+ }.call(packet.slice(6 + 2, 40).pack("C*").unpack("V*")))
545
568
  when [0x02, 0x13] # RXM-SFRBX
546
569
  sys, svid = gnss_serial.call(packet[6 + 1], packet[6])
547
570
  opt = {}
@@ -549,9 +572,7 @@ class Receiver
549
572
  register_ephemeris(
550
573
  t_meas,
551
574
  sys, svid,
552
- packet.slice(6 + 8, 4 * packet[6 + 4]).each_slice(4).collect{|v|
553
- v.pack("C*").unpack("V")[0]
554
- }, opt)
575
+ packet.slice(6 + 8, 4 * packet[6 + 4]).pack("C*").unpack("V*"), opt)
555
576
  end
556
577
  }
557
578
  $stderr.puts ", found packets are %s"%[ubx_kind.inspect]
@@ -564,7 +585,7 @@ class Receiver
564
585
  @solver.sbas_space_node,
565
586
  @solver.glonass_space_node,
566
587
  ].inject(0){|res, sn|
567
- loaded_items = sn.send(:read, fname)
588
+ loaded_items = critical{sn.send(:read, fname)}
568
589
  raise "Format error! (Not RINEX) #{src}" if loaded_items < 0
569
590
  res + loaded_items
570
591
  }
@@ -648,7 +669,7 @@ class Receiver
648
669
  next unless /^SYS_(?!SYSTEMS)(.*)/ =~ sys.to_s
649
670
  idx, sys_name = [@sp3.class.const_get(sys), $1]
650
671
  next unless sats[idx] > 0
651
- next unless @sp3.push(@solver, idx)
672
+ next unless critical{@sp3.push(@solver, idx)}
652
673
  $stderr.puts "Change ephemeris source of #{sys_name} to SP3"
653
674
  }
654
675
  end
@@ -656,7 +677,7 @@ class Receiver
656
677
  def attach_antex(src)
657
678
  fname = Util::get_txt(src)
658
679
  raise "Specify SP3 before ANTEX application!" unless @sp3
659
- applied_items = @sp3.apply_antex(fname)
680
+ applied_items = critical{@sp3.apply_antex(fname)}
660
681
  raise "Format error! (Not ANTEX) #{src}" unless applied_items >= 0
661
682
  $stderr.puts "SP3 correction with ANTEX file (%s): %d items have been processed."%[src, applied_items]
662
683
  end
@@ -672,9 +693,11 @@ class Receiver
672
693
  next unless /^SYS_(?!SYSTEMS)(.*)/ =~ sys.to_s
673
694
  idx, sys_name = [@clk.class.const_get(sys), $1]
674
695
  next unless sats[idx] > 0
675
- next unless @clk.push(@solver, idx)
696
+ next unless critical{@clk.push(@solver, idx)}
676
697
  $stderr.puts "Change clock error source of #{sys_name} to RINEX clock"
677
698
  }
678
699
  end
679
700
  end
680
701
  end
702
+
703
+ require_relative 'receiver/extension'
data/lib/gps_pvt/util.rb CHANGED
@@ -1,7 +1,39 @@
1
- require 'open-uri'
2
1
  require 'tempfile'
3
2
  require 'uri'
4
3
 
4
+ proc{
5
+ # port[:baudrate], baudrate default is 115200
6
+ Serial.class_eval{
7
+ const_set(:SPEC,
8
+ if RubySerial::ON_WINDOWS then
9
+ %r{^(?:\\\\.\\)?(COM\d+)(?::(\d+))?$}
10
+ elsif RubySerial::ON_LINUX then
11
+ %r{^(/dev/tty[^:]+)(?::(\d+))?$}
12
+ else
13
+ nil
14
+ end)
15
+ }
16
+ Serial.class_eval{
17
+ read_orig = instance_method(:read)
18
+ define_method(:read){|len|
19
+ buf = ''
20
+ f = read_orig.bind(self)
21
+ buf += f.call(len - buf.size) while buf.size < len
22
+ buf
23
+ }
24
+ def eof?; false; end
25
+ }
26
+ Kernel.instance_eval{
27
+ open_orig = method(:open)
28
+ define_method(:open){|*args, &b|
29
+ return open_orig.call(*args, &b) unless Serial::SPEC =~ args[0]
30
+ Serial::new($1, $2 ? $2.to_i : 115200)
31
+ }
32
+ }
33
+ }.call if require 'rubyserial'
34
+
35
+ require 'open-uri'
36
+
5
37
  module GPS_PVT
6
38
  module Util
7
39
  class << self
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GPS_PVT
4
- VERSION = "0.7.2"
4
+ VERSION = "0.8.1"
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gps_pvt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - fenrir(M.Naruoka)
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-07 00:00:00.000000000 Z
11
+ date: 2022-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rubyserial
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -98,11 +112,13 @@ files:
98
112
  - ext/ninja-scan-light/tool/swig/makefile
99
113
  - ext/ninja-scan-light/tool/swig/spec/GPS_spec.rb
100
114
  - ext/ninja-scan-light/tool/swig/spec/SylphideMath_spec.rb
115
+ - ext/ninja-scan-light/tool/util/bit_counter.h
101
116
  - ext/ninja-scan-light/tool/util/text_helper.h
102
117
  - gps_pvt.gemspec
103
118
  - gps_pvt.rbs
104
119
  - lib/gps_pvt.rb
105
120
  - lib/gps_pvt/receiver.rb
121
+ - lib/gps_pvt/receiver/extension.rb
106
122
  - lib/gps_pvt/ubx.rb
107
123
  - lib/gps_pvt/util.rb
108
124
  - lib/gps_pvt/version.rb