cycle_analyst_logger 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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