cycle_analyst_logger 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa99c57d5a7f7847435d357af10d11509fd73c02
4
- data.tar.gz: b6a2f5962e3f5b42306a2b8988bd307a25c75b37
3
+ metadata.gz: 24b86d3ddae10152675800b27ac5f8bf912ea5ac
4
+ data.tar.gz: 270d41a27b722386113154d05fc89c32740375c5
5
5
  SHA512:
6
- metadata.gz: 6e7ba577ce1a9ce9fd7e6530a178e675e3d0fd1af56128e2c59a79bb1fb3cde66514dd7d16c79df42e046ef2f0a90222ce347f547ec1e1b0760aa8a04ad1e2e6
7
- data.tar.gz: ceda4757f47b3fe2089fabea131de290e12ddb1258c192496f2b5bb83d31a66a4a891d9ffcc338cdfd42ac74252c0667fe34c4be28904667208e60e9be713cd6
6
+ metadata.gz: 5401db314c8ef3b9721834c8a02005bd52041891e394c22fa8a328e52456e12ff2182a3ed836e8faf6f32b41cddac57298b6dc075e0cd9e8c6771d947de291ef
7
+ data.tar.gz: b1504aef4be175e8a9226a2e0785aef2badbc6d4c15d06dd63b02c2284c2d954dac07816c6c577ffca3a0e14c727bc1cce79e7879700756b86e899eed5c9bd98
data/.gitignore CHANGED
File without changes
data/Gemfile CHANGED
File without changes
data/LICENSE.txt CHANGED
File without changes
data/README.rdoc CHANGED
@@ -8,9 +8,7 @@
8
8
  == SYNOPSIS
9
9
  cycle_analyst_logger [global options] command [command options] [arguments...]
10
10
  cycle_analyst_logger [global options] log [tty] [baudrate] [enable_phaserunner] [tty] [baudrate]
11
-
12
- == VERSION
13
- 0.2.0
11
+ cycle_analyst_logger [global options] location_from_gmx gpx_filename log_filename [output_filename]
14
12
 
15
13
  == GLOBAL OPTIONS
16
14
  -t, --tty_ca=arg - Cycle Analyst Serial (USB) device (default: /dev/ttyUSB1)
@@ -26,6 +24,7 @@
26
24
  == COMMANDS
27
25
  help - Shows a list of commands or help for one command
28
26
  log - Capture the logging output of the Cycle Analyst and optionally Phaserunner to a file
27
+ location_from_gmx, loc - Merge geo info from GMX file and log lines from log file
29
28
 
30
29
  === Attributes that are Logged
31
30
 
@@ -33,8 +32,8 @@
33
32
  * Cycle Analyst Amp Hours (Ah)
34
33
  * Cycle Analyst Volts (V)
35
34
  * Cycle Analyst Current (A)
36
- * Cycle Analyst Speed (Kph)
37
- * Cycle Analyst Distance (Km)
35
+ * Cycle Analyst Speed (Mph)
36
+ * Cycle Analyst Distance (Miles)
38
37
  * Cycle Analyst Motor Temp (DegC)
39
38
  * Cycle Analyst Human Cadence (RPM)
40
39
  * Cycle Analyst Human Power (W)
data/Rakefile CHANGED
File without changes
@@ -29,6 +29,9 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency 'gli', '~> 2.17'
30
30
  spec.add_runtime_dependency 'serialport', '~> 1.3'
31
31
  spec.add_runtime_dependency 'phaserunner', '~> 0.1', '>= 0.1.5'
32
+ spec.add_runtime_dependency 'haversine', '~> 0.3.2'
33
+ spec.add_runtime_dependency 'gpx', '~> 0.9.0'
34
+ spec.add_runtime_dependency 'nmea_plus', '~> 1.0', '>= 1.0.20'
32
35
 
33
36
  spec.add_development_dependency 'bundler', '~> 1.16'
34
37
  spec.add_development_dependency 'rake', '~> 10.0'
@@ -1,6 +1,6 @@
1
1
  == cycle_analyst_logger - Store the streaming data log output of a Grin Cycle Analyst V3 and optionally a Phaserunner
2
2
 
3
- v0.2.0
3
+ v0.2.4
4
4
 
5
5
  === Global Options
6
6
  === -b|--baud_ca arg
@@ -71,6 +71,10 @@ List commands one per line, to assist with shell completion
71
71
 
72
72
 
73
73
 
74
+ ==== Command: <tt>location_from_gmx|loc </tt>
75
+ Merge geo info from GMX file and log lines from log file
76
+
77
+
74
78
  ==== Command: <tt>log </tt>
75
79
  Capture the logging output of the Cycle Analyst and optionally Phaserunner to a file
76
80
 
@@ -3,5 +3,7 @@
3
3
 
4
4
  require 'cycle_analyst_logger/gli_patch.rb'
5
5
  require 'cycle_analyst_logger/version.rb'
6
- require 'cycle_analyst_logger/main.rb'
6
+ require 'cycle_analyst_logger/cli.rb'
7
7
  require 'cycle_analyst_logger/cycle_analyst.rb'
8
+ require 'cycle_analyst_logger/gps.rb'
9
+ require 'cycle_analyst_logger/gpx.rb'
@@ -8,7 +8,6 @@ module CycleAnalystLogger
8
8
  end
9
9
 
10
10
  class Cli
11
- attr_reader :cycle_analyst
12
11
  attr_reader :enable_phaserunner
13
12
  attr_reader :pr
14
13
  attr_reader :quiet
@@ -37,8 +36,7 @@ module CycleAnalystLogger
37
36
 
38
37
  desc 'Get PhaseRunner Logs also'
39
38
  default_value true
40
- arg 'enable_phaserunner', :optional
41
- flag [:enable_phaserunner]
39
+ switch [:enable_phaserunner]
42
40
 
43
41
  desc 'Phaserunner Serial (USB) device'
44
42
  default_value '/dev/ttyUSB0'
@@ -50,6 +48,20 @@ module CycleAnalystLogger
50
48
  arg 'baudrate', :optional
51
49
  flag [:baud_pr]
52
50
 
51
+ desc 'Get Gps Logs also'
52
+ default_value true
53
+ switch [:enable_gps]
54
+
55
+ desc 'Gps Serial (USB) device'
56
+ default_value '/dev/ttyUSB2'
57
+ arg 'tty', :optional
58
+ flag [:tty_gps]
59
+
60
+ desc 'Gps Serial port baudrate'
61
+ default_value 115200
62
+ arg 'baudrate', :optional
63
+ flag [:baud_gps]
64
+
53
65
  desc "How many lines to read"
54
66
  default_value :forever
55
67
  flag [:l, :loop_count]
@@ -57,12 +69,26 @@ module CycleAnalystLogger
57
69
  desc 'Do not output to stdout'
58
70
  switch [:q, :quiet]
59
71
 
60
- desc 'Capture the logging output of the Cycle Analyst and optionally Phaserunner to a file'
72
+ desc 'Log the Cycle Analyst and optionally GPS and Phaserunner to a file'
61
73
  command :log do |log|
62
74
  log.action do |global_options, options, args|
63
- filename = "cycle_analyst.#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}.csv"
64
- output_fd = File.open(filename, 'w')
65
- cycle_analyst.get_logs(output_fd, loop_count, quiet)
75
+ cycle_analyst = CycleAnalyst.new(global_options)
76
+ cycle_analyst.get_logs(loop_count, quiet)
77
+ end
78
+ end
79
+
80
+ desc 'Merge geo info from GMX file and log lines from log file'
81
+ arg :gpx_filename
82
+ arg :log_filename
83
+ arg :output_filename, :optional
84
+ command [:location_from_gmx, :loc] do |location_from_gmx|
85
+ location_from_gmx.action do |global_options, options, args|
86
+ gpx_filename = args[0]
87
+ log_filename = args[1]
88
+ output_filename = args[2]
89
+
90
+ gpx = Gpx.new(gpx_filename)
91
+ gpx.merge_location(log_filename, output_filename)
66
92
  end
67
93
  end
68
94
 
@@ -80,7 +106,8 @@ module CycleAnalystLogger
80
106
  global[:loop_count].to_i
81
107
  end
82
108
  @enable_phaserunner = global[:enable_phaserunner]
83
- @cycle_analyst = CycleAnalyst.new(global)
109
+ @enable_gps = global[:enable_gps]
110
+ true
84
111
  end
85
112
 
86
113
  post do |global,command,options,args|
@@ -16,19 +16,28 @@ module CycleAnalystLogger
16
16
  # Handle from the SerialPort object
17
17
  attr_reader :serial_io
18
18
 
19
- # If the phaeserunner should be read
19
+ # If the phaserunner should be read
20
20
  attr_reader :enable_phaserunner
21
21
 
22
22
  # Handle of the Phaserunner::Modbus object
23
23
  attr_reader :phaserunner
24
24
 
25
+ # If the gps should be read
26
+ attr_reader :enable_gps
27
+
28
+ # Handle of the Gps object
29
+ attr_reader :gps
30
+
31
+ # Shared data for gps data
32
+ attr_reader :gps_data
33
+
25
34
  # Hash definition that describes the names, values and units of the Cycle Analyst log data
26
35
  CA_DICT = {
27
36
  0 => {address: 0, name: "Amp Hours", units: "Ah", scale: 1},
28
37
  1 => { address: 1, name: "Volts", units: "V", scale: 1 },
29
38
  2 => { address: 2, name: "Current", units: "A", scale: 1},
30
- 3 => { address: 3, name: "Speed", units: "Kph", scale: 1},
31
- 4 => { address: 4, name: "Distance", units: "Km", scale: 1},
39
+ 3 => { address: 3, name: "Speed", units: "Mph", scale: 1},
40
+ 4 => { address: 4, name: "Distance", units: "Miles", scale: 1},
32
41
  5 => { address: 5, name: "Motor Temp", units: "DegC", scale: 1},
33
42
  6 => { address: 6, name: "Human Cadence", units: "RPM", scale: 1},
34
43
  7 => { address: 7, name: "Human Power", units: "W", scale: 1},
@@ -47,22 +56,35 @@ module CycleAnalystLogger
47
56
  @dict = CA_DICT
48
57
  @serial_io = SerialPort.new @tty, @baudrate, 8, 1
49
58
  @enable_phaserunner = opts[:enable_phaserunner]
59
+
50
60
  if @enable_phaserunner
51
61
  @phaserunner = Phaserunner::Modbus.new(
52
62
  tty: opts[:tty_pr], baudrate: opts[:baud_pr]
53
63
  )
54
64
  end
65
+
66
+ @enable_gps = opts[:enable_gps]
67
+
68
+ if @enable_gps
69
+ @gps_data = {}
70
+ @gps = Gps.new(@gps_data, {tty: opts[:tty_gps], baudrate: opts[:baud_gps]})
71
+ end
55
72
  end
56
73
 
57
74
  # Forms the proper header line
58
75
  # @return [String] of a printable CSV header line
59
- def logs_header
76
+ def log_header
60
77
  hdr = dict.map do |(address, node)|
61
78
  "#{node[:name]} (#{node[:units]})"
62
79
  end
63
80
  if enable_phaserunner
64
81
  hdr += phaserunner.bulk_log_header.map { |name| "PR #{name}" }
65
82
  end
83
+
84
+ if enable_gps
85
+ hdr += gps.log_header
86
+ end
87
+
66
88
  hdr.join(',')
67
89
  end
68
90
 
@@ -75,9 +97,14 @@ module CycleAnalystLogger
75
97
  # @param output_fd [File] File Descriptor of the output file to write to. Don't write to file if nil
76
98
  # @param loop_count [Integer, Symbol] Number of lines to output, or forever if :forever
77
99
  # @param quite [Boolean] Don't output to stdout if true
78
- def get_logs(output_fd, loop_count, quiet)
100
+ def get_logs(loop_count, quiet)
101
+ filename = "cycle_analyst.#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}.csv"
102
+ output_fd = File.open(filename, 'w')
103
+
104
+ gps_thread = Thread.new { gps.run } if enable_gps
105
+
79
106
  line_number = 0
80
- hdr = %Q(Timestamp,#{logs_header})
107
+ hdr = %Q(Timestamp,#{log_header})
81
108
 
82
109
  puts hdr if not quiet
83
110
  output_fd.puts hdr if output_fd
@@ -87,7 +114,11 @@ module CycleAnalystLogger
87
114
  [Time.now.utc.round(10).iso8601(6)] +
88
115
  tsv2array(line)
89
116
  )
117
+
90
118
  output += phaserunner.bulk_log_data if enable_phaserunner
119
+ #puts "gps_data: #{gps.log_data.inspect}"
120
+ output += gps.log_data if enable_gps
121
+
91
122
  output_line = output.flatten.join(',')
92
123
 
93
124
  puts output_line unless quiet
File without changes
@@ -0,0 +1,85 @@
1
+ require 'nmea_plus'
2
+ require 'pp'
3
+
4
+ module CycleAnalystLogger
5
+
6
+ class Gps
7
+ DEFAULTS = {
8
+ tty: '/dev/ttyUSB2',
9
+ baudrate: 115200
10
+ }
11
+
12
+ GPS_DICT = {
13
+ 0 => {address: 0, name: 'Time', key: :time, units: nil, scale: 1},
14
+ 1 => {address: 1, name: 'Latitude', key: :latitude, units: 'degrees', scale: 1},
15
+ 2 => {address: 2, name: 'Longitude', key: :longitude, units: 'degrees', scale: 1},
16
+ 3 => {address: 3, name: 'Altitude', key: :altitude, units: 'meters', scale: 1},
17
+ 4 => {address: 4, name: 'Speed', key: :speed, units: 'kph', scale: 1},
18
+ 5 => {address: 5, name: 'Fix Quality', key: :fix_quality, units: nil, scale: 1},
19
+ 6 => {address: 6, name: 'Satellites', key: :satellites, units: nil, scale: 1},
20
+ 7 => {address: 7, name: 'Geoid Height', key: :geoid_height, units: nil, scale: 1},
21
+ 8 => {address: 8, name: 'Horizontal Dilution', key: :horizontal_dilution, units: nil, scale: 1},
22
+ 9 => {address: 9, name: 'Faa Mode', key: :faa_mode, units: nil, scale: 1}
23
+ }
24
+
25
+ attr_reader :tty
26
+ attr_reader :baudrate
27
+ attr_reader :serial_io
28
+ attr_reader :dict
29
+ attr_reader :pre_data
30
+ attr_reader :shared_data
31
+ attr_reader :source_decoder
32
+
33
+ def initialize(shared_data, opts = {})
34
+ final_opts = DEFAULTS.merge(opts)
35
+ @tty = final_opts[:tty]
36
+ @baudrate = final_opts[:baudrate]
37
+ @serial_io = SerialPort.new @tty, @baudrate, 8, 1
38
+ @dict = GPS_DICT
39
+ @pre_data = {}
40
+ @shared_data = shared_data
41
+ @source_decoder = NMEAPlus::SourceDecoder.new(@serial_io)
42
+ end
43
+
44
+ def run
45
+ source_decoder.each_complete_message do |message|
46
+ case message.data_type
47
+ when 'GNGGA'
48
+ pre_data[:time] = message.fix_time
49
+ pre_data[:latitude] = message.latitude
50
+ pre_data[:longitude] = message.longitude
51
+ pre_data[:altitude] = message.altitude
52
+ pre_data[:dgps_station_id] = message.dgps_station_id
53
+ pre_data[:fix_quality] = message.fix_quality
54
+ pre_data[:geoid_height] = message.geoid_height
55
+ pre_data[:horizontal_dilution] = message.horizontal_dilution
56
+ pre_data[:satellites] = message.satellites
57
+ pre_data[:seconds_since_last_update] = message.seconds_since_last_update
58
+ when 'GNVTG'
59
+ pre_data[:speed_kmh] = message.speed_kmh
60
+ pre_data[:speed_knots] = message.speed_knots
61
+ pre_data[:faa_mode] = message.faa_mode
62
+ pre_data[:track_degrees_magnetic] = message.track_degrees_magnetic
63
+ pre_data[:track_degrees_true] = message.track_degrees_true
64
+ # Has to be a copy since shared_data is really a reference to an
65
+ # instance variable in the outer thread.
66
+ pre_data.each_pair { |k,v| shared_data[k] = v}
67
+ else
68
+ next
69
+ end
70
+ end
71
+ end
72
+
73
+ def log_header
74
+ dict.map do |(address, node)|
75
+ node[:name] + (node[:units] ? " (#{node[:units]})" : '')
76
+ end
77
+ end
78
+
79
+ def log_data
80
+ dict.map do |address, node|
81
+ shared_data[node[:key]]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,97 @@
1
+ require 'haversine'
2
+ require 'gpx'
3
+
4
+ module CycleAnalystLogger
5
+ class Gpx
6
+ attr_reader :gpx
7
+ # The clean info needed from the gpx
8
+ # @return [Hash<Time, Hash>] points The points organized by Timestamp.
9
+ # * :time (Hash<Symbol, Float) The Point Atrributes
10
+ # * :timestamp (Time) in seconds resolution
11
+ # * :lattitude (Float)
12
+ # * :longitude (Float)
13
+ # * :elevation (Float)
14
+ # * :speed (Float)
15
+ attr_reader :points
16
+
17
+ def initialize(filename)
18
+ @gpx = GPX::GPXFile.new(gpx_file: filename)
19
+ @points = gpx.tracks.flat_map(&:points).each_with_object({}) do |point, memo|
20
+ memo[point.time] = {
21
+ timestamp: point.time,
22
+ lattitude: point.lat,
23
+ longitude: point.lon,
24
+ elevation: point.elevation,
25
+ speed: point.speed
26
+ }
27
+ end
28
+ end
29
+
30
+ # Checks if the input is a valid Time String
31
+ # @param input [String] Potential Time Stamp or invalid string
32
+ # @return [Time, nil] Returns a Time object based on the input if it was valid. Otherwise returns nil
33
+ def valid_timestamp(input)
34
+ begin
35
+ Time.parse(input)
36
+ rescue ArgumentError
37
+ nil
38
+ end
39
+ end
40
+
41
+ # Finds the point that is close to the timestamp input.
42
+ # If there is no point with the same time, it returns the last point
43
+ # Also calculates the speed between the last point and this point and adds it to the response hash
44
+ # @param timestamp [Time] Timestamp to find. Will round to nearest second
45
+ def closest_point_in_time(timestamp)
46
+ point = if (point = points[timestamp.round(0)])
47
+ # Handle initial case
48
+ @last_point = point unless @last_point
49
+
50
+ if point[:timestamp] == @last_point[:timestamp]
51
+ @last_point
52
+ else # Its a new point so calculate speed and return the new point
53
+ duration = point[:timestamp] - @last_point[:timestamp]
54
+ distance = Haversine.distance(@last_point[:lattitude], @last_point[:longitude], point[:lattitude], point[:longitude]).to_miles
55
+ speed_mph = (distance / duration) * 3600 # Convert miles / second to miles per hour
56
+ @last_point = point.merge(speed: speed_mph)
57
+ end
58
+
59
+ else # Could not find a point with a matching timestamp so use the last_point (were probably not moving or Strava not recording)
60
+ @last_point.merge(speed: 0.0)
61
+ end
62
+ end
63
+
64
+ def merge_location(log_filename, output_filename = nil)
65
+ # Get the proper output File descriptor
66
+ out_fd = if output_filename
67
+ File.open(output_filename, 'w')
68
+ else
69
+ $stdout.dup
70
+ end
71
+
72
+ File.readlines(log_filename).each.with_index do |log_line, idx|
73
+ log_record = log_line.split(',')
74
+ if idx == 0
75
+ out_fd.puts 'Timestamp,Lattitude,Longitude,Elevation (feet),Speed (mph),' +
76
+ log_record[1..-1].join(',')
77
+ next
78
+ end
79
+
80
+ # Check that the line has a valid timestamp, skip this line if it isn't
81
+ next unless (timestamp = valid_timestamp log_record[0])
82
+
83
+ # Get the point from GPX that is closest to the timestamp from the log
84
+ if (point = closest_point_in_time(timestamp))
85
+ lattitude = point[:lattitude]
86
+ longitude = point[:longitude]
87
+ elevation = point[:elevation]
88
+ speed = point[:speed]
89
+ else
90
+ lattitude = longitude = elevation = speed = nil
91
+ end
92
+ out_fd.puts "#{log_record[0]},#{lattitude},#{longitude},#{elevation},#{speed}," +
93
+ log_record[1..-1].join(',')
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,3 +1,3 @@
1
1
  module CycleAnalystLogger
2
- VERSION = '0.2.3'
2
+ VERSION = '0.3.0'
3
3
  end
data/todo.txt CHANGED
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cycle_analyst_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert J. Berger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-14 00:00:00.000000000 Z
11
+ date: 2018-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli
@@ -58,6 +58,54 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: 0.1.5
61
+ - !ruby/object:Gem::Dependency
62
+ name: haversine
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.3.2
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.3.2
75
+ - !ruby/object:Gem::Dependency
76
+ name: gpx
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.9.0
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.9.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: nmea_plus
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 1.0.20
99
+ type: :runtime
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: '1.0'
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 1.0.20
61
109
  - !ruby/object:Gem::Dependency
62
110
  name: bundler
63
111
  requirement: !ruby/object:Gem::Requirement
@@ -162,9 +210,11 @@ files:
162
210
  - cycle_analyst_logger.rdoc
163
211
  - exe/cycle_analyst_logger
164
212
  - lib/cycle_analyst_logger.rb
213
+ - lib/cycle_analyst_logger/cli.rb
165
214
  - lib/cycle_analyst_logger/cycle_analyst.rb
166
215
  - lib/cycle_analyst_logger/gli_patch.rb
167
- - lib/cycle_analyst_logger/main.rb
216
+ - lib/cycle_analyst_logger/gps.rb
217
+ - lib/cycle_analyst_logger/gpx.rb
168
218
  - lib/cycle_analyst_logger/version.rb
169
219
  - todo.txt
170
220
  homepage: https://github.com/rberger/cycle_analyst_logger