postrunner 1.0.2 → 1.1.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
  SHA256:
3
- metadata.gz: ee79192eef591186d2dcbf992af1d193a60aee49b9ae9d90d6a880d4758c623c
4
- data.tar.gz: 7fefaf239fe43c71be7183b7773f909415126669f514e608b6c6f16fef9682f3
3
+ metadata.gz: b6df1b75dc7669dd99287084a3bc96c4528b2273b37d8203a75b12d9e26b0a2b
4
+ data.tar.gz: c2cf4a588aebb579e208f62b2c2c9d1ce76798ac482b4f1b0838bb92554691d1
5
5
  SHA512:
6
- metadata.gz: 65900650e3e1751f4f92994f4473c18d0131615ff60f72c1683ee7ed05c40abd6d060ba6eca2cfa4134c7507effb90edc4785901e5a8d006c0ed05fa64b6d23d
7
- data.tar.gz: 5de44344671bf391611e2636e4cebd0fc6e7a5f5636444ca2fc828c1d699e17086dfe7ea1a80aa89a0a593b279ca17fe0f5c26583af7fa21763ab7130f7f73ba
6
+ metadata.gz: d02b2921d8f7aedf63dc07ebf8d21765e3ab6e4e36872586eb5fffbca8d265e65a64c6564ba8a88406de39e4f9d534d93cd903563a9f28f3e44d5ddeaf9ce8d7
7
+ data.tar.gz: 8c6d305975888ef68fda07e2793d78da8d79c2d5aa114efa5b5cc86b4e97ff1d2882765772db3e26a99f33343278e29aa8bb4f208f4caf5601989ba4ec37f758
@@ -0,0 +1,33 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ os: [ ubuntu]
23
+ ruby: [2.4, 2.5, 2.6, 2.7, 3.0, head]
24
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
25
+ steps:
26
+ - uses: actions/checkout@v2
27
+ - uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby }}
30
+ bundler-cache: true
31
+
32
+ - name: Test with RSpec
33
+ run: bundle exec rspec
data/README.md CHANGED
@@ -48,23 +48,47 @@ typing.
48
48
 
49
49
  ## Usage
50
50
 
51
+ ### Mounting the watch
52
+
53
+ Watches that expose their data as FAT file system. Typically after connecting
54
+ your garmin watch, they are mount automatically if you have installed the
55
+ udev package.
56
+
57
+ For watches that expose their data via MTP (Media Transfer Procotol). For
58
+ Debian buster running `sudo apt install jmtpfs mtp-tools` will install the
59
+ needed packages.
60
+
61
+ `mkdir /tmp/forerunner; jmtpfs /tmp/forerunner`
62
+
63
+ This has been tested with a Garmin Forerunner 945.
64
+
65
+ For more information about MTP under Windows have a look at the
66
+ [Garmin FAQ](https://support.garmin.com/?faq=Itl8M6ARrh4gBQMHFqdqK8)
67
+
51
68
  ### Importing FIT files
52
69
 
53
70
  To get started you need to connect your device to your computer and
54
- mount it as a disk drive. Only devices that expose their data as FAT file
71
+ mount it as a disk drive. Only devices that expose their data as FAT or MTP file
55
72
  system are supported. Older devices use proprietary drivers and are
56
73
  not supported by PostRunner. Once the device is mounted find out the
57
74
  full path to the directory that contains your FIT files. You can then
58
75
  import all files on the device.
59
76
 
77
+ * for USB-FAT
60
78
  ```
61
79
  $ postrunner import /run/media/$USER/GARMIN/GARMIN/ACTIVITY/
62
80
  ```
63
-
64
- The above command assumes that your device is mounted as
65
- /run/media/$USER. Please replace $USER with your login name and the
66
- path with the path to your device. Files that have been imported
67
- previously will not be imported again.
81
+ The above command assumes that your device is mounted as /run/media/$USER.
82
+ Please replace $USER with your login name and the path with the path to
83
+ your device.
84
+
85
+ * for MTP (assuming you mounted it as described above)
86
+ ```
87
+ $ postrunner import /tmp/forerunner/Primary/GARMIN/Activity
88
+ ```
89
+
90
+ * Note
91
+ Files that have been imported previously will not be imported again.
68
92
 
69
93
  ### Viewing FIT file data on the console
70
94
 
data/Rakefile CHANGED
@@ -1,24 +1,9 @@
1
- $:.unshift File.join(File.dirname(__FILE__))
2
-
3
- # Add the lib directory to the search path if it isn't included already
4
- lib = File.expand_path('../lib', __FILE__)
5
- $:.unshift lib unless $:.include?(lib)
6
-
1
+ #!/usr/bin/env ruby
2
+ require_relative 'lib/postrunner/version'
3
+ require 'rake'
4
+ require 'rspec'
7
5
  require "bundler/gem_tasks"
8
- require "rspec/core/rake_task"
9
- require 'rake/clean'
10
- require 'yard'
11
- YARD::Rake::YardocTask.new
12
-
13
- Dir.glob( 'tasks/*.rake').each do |fn|
14
- begin
15
- load fn;
16
- rescue LoadError
17
- puts "#{fn.split('/')[1]} tasks unavailable: #{$!}"
18
- end
19
- end
20
-
21
- task :default => :spec
22
- task :test => :spec
6
+ require 'rspec/core/rake_task'
23
7
 
24
- desc 'Run all unit and spec tests'
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
@@ -65,6 +65,37 @@ module PostRunner
65
65
  'hiking' => 'Hiking',
66
66
  'multisport' => 'Multisport',
67
67
  'paddling' => 'Paddling',
68
+ 'training' => 'Training',
69
+ 'flying' => 'Flying',
70
+ 'e_biking' => 'E-Biking',
71
+ 'motorcycling' => 'Motor Cycling',
72
+ 'boating' => 'Boating',
73
+ 'driving' => 'Driving',
74
+ 'golf' => 'Golf',
75
+ 'hang_gliding' => 'Hang Gliding',
76
+ 'horseback_riding' => 'Horseback Riding',
77
+ 'hunting' => 'Hunting',
78
+ 'fishing' => 'Fishing',
79
+ 'inline_skating' => 'Inline Skating',
80
+ 'rock_climbing' => 'Rock Climbing',
81
+ 'sailing' => 'Sailing',
82
+ 'ice_skating' => 'Ice Skating',
83
+ 'sky_diving' => 'Sky Diving',
84
+ 'snowshoeing' => 'Snowshoeing',
85
+ 'snowmobiling' => 'Snowmobiling',
86
+ 'stand_up_paddleboarding' => 'Stand Up Paddleboarding',
87
+ 'surfing' => 'Surfing',
88
+ 'wakeboarding' => 'Wakeboarding',
89
+ 'water_skiing' => 'Water Skiing',
90
+ 'kayaking' => 'Kayaking',
91
+ 'rafting' => 'Rafting',
92
+ 'windsurfing' => 'Windsurfing',
93
+ 'kitesurfing' => 'Kitesurfing',
94
+ 'tactical' => 'Tactical',
95
+ 'jumpmaster' => 'Jumpmaster',
96
+ 'boxing' => 'Boxing',
97
+ 'floor_climbing' => 'Floor Climbing',
98
+ 'diving' => 'Diving',
68
99
  'all' => 'All'
69
100
  }
70
101
  ActivitySubTypes = {
@@ -95,7 +126,43 @@ module PostRunner
95
126
  'challenge' => 'Challenge',
96
127
  'indoor_skiing' => 'Indoor Skiing',
97
128
  'cardio_training' => 'Cardio Training',
129
+ 'e_bike_fitness' => 'E Bike Fittness',
130
+ 'bmx' => 'BMX',
131
+ 'casual_walking' => 'Casual Walking',
132
+ 'speed_walking' => 'Speed Walking',
133
+ 'bike_to_run_transition' => 'Bike to Run Transition',
134
+ 'run_to_bike_transition' => 'Run to Bike Transition',
135
+ 'swim_to_bike_transition' => 'Swim to Bike Transition',
136
+ 'atv' => 'ATV',
137
+ 'motocross' => 'Motocross',
138
+ 'backcountry' => 'Backcountry',
139
+ 'resort' => 'Resort',
140
+ 'rc_drone' => 'RC Drone',
141
+ 'wingsuit' => 'Wingsuite',
142
+ 'whitewater' => 'Whitemaster',
143
+ 'skate_skiing' => 'Skate Skiing',
144
+ 'yoga' => 'Yoga',
145
+ 'pilates' => 'Pilates',
146
+ 'indoor_running' => 'Indoor Running',
147
+ 'gravel_cycling' => 'Gravel Cycling',
148
+ 'e_bike_mountain' => 'E-Bike Mountain',
149
+ 'commuting' => 'Commuting',
150
+ 'mixed_surface' => 'Mixed Surface',
151
+ 'navigate' => 'Navigate',
152
+ 'track_me' => 'Track Me',
153
+ 'map' => 'Map',
154
+ 'single_gas_diving' => 'Single Gas Diving',
155
+ 'multi_gas_diving' => 'Multi Gas Diving',
156
+ 'gauge_diving' => 'Gauge Diving',
157
+ 'apnea_diving' => 'Apnea Diving',
158
+ 'apnea_hunting' => 'Apnea Hunting',
98
159
  'virtual_activity' => 'Virtual Activity',
160
+ 'obstacle' => 'Obstacle',
161
+ 'breathing' => 'Breating',
162
+ 'sail_race' => 'Sail Race',
163
+ 'ultra' => 'Ultra',
164
+ 'indoor_climbing' => 'Indoor Climbing',
165
+ 'bouldering' => 'Bouldering',
99
166
  'all' => 'All'
100
167
  }
101
168
 
@@ -77,7 +77,9 @@ module PostRunner
77
77
  t.body
78
78
  t.row([ 'Type:', @type ])
79
79
  t.row([ 'Sub Type:', @sub_type ])
80
- t.row([ 'Start Time:', session.start_time])
80
+ t.row([ 'Start Time:', session.start_time.localtime])
81
+ t.row([ 'Elapsed Time:', secsToHMS(session.total_elapsed_time) ])
82
+ t.row([ 'Moving Time:', secsToHMS(session.total_timer_time) ])
81
83
  t.row([ 'Distance:',
82
84
  local_value(session, 'total_distance', '%.2f %s',
83
85
  { :metric => 'km', :statute => 'mi'}) ])
@@ -86,26 +88,11 @@ module PostRunner
86
88
  local_value(@fit_activity, 'total_gps_distance', '%.2f %s',
87
89
  { :metric => 'km', :statute => 'mi'}) ])
88
90
  end
89
- t.row([ 'Time:', secsToHMS(session.total_timer_time) ])
90
- t.row([ 'Elapsed Time:', secsToHMS(session.total_elapsed_time) ])
91
91
  t.row([ 'Avg. Speed:',
92
92
  local_value(session, 'avg_speed', '%.1f %s',
93
93
  { :metric => 'km/h', :statute => 'mph' }) ])
94
94
  if @activity.sport == 'running' || @activity.sport == 'multisport'
95
95
  t.row([ 'Avg. Pace:', pace(session, 'avg_speed') ])
96
- end
97
- t.row([ 'Total Ascent:',
98
- local_value(session, 'total_ascent', '%.0f %s',
99
- { :metric => 'm', :statute => 'ft' }) ])
100
- t.row([ 'Total Descent:',
101
- local_value(session, 'total_descent', '%.0f %s',
102
- { :metric => 'm', :statute => 'ft' }) ])
103
- t.row([ 'Calories:', "#{session.total_calories} kCal" ])
104
- t.row([ 'Avg. HR:', session.avg_heart_rate ?
105
- "#{session.avg_heart_rate} bpm" : '-' ])
106
- t.row([ 'Max. HR:', session.max_heart_rate ?
107
- "#{session.max_heart_rate} bpm" : '-' ])
108
- if @activity.sport == 'running' || @activity.sport == 'multisport'
109
96
  t.row([ 'Avg. Run Cadence:',
110
97
  session.avg_running_cadence ?
111
98
  "#{(2 * session.avg_running_cadence).round} spm" : '-' ])
@@ -131,6 +118,21 @@ module PostRunner
131
118
  session.avg_cadence ?
132
119
  "#{(2 * session.avg_cadence).round} rpm" : '-' ])
133
120
  end
121
+ t.row([ 'Total Ascent:',
122
+ local_value(session, 'total_ascent', '%.0f %s',
123
+ { :metric => 'm', :statute => 'ft' }) ])
124
+ t.row([ 'Total Descent:',
125
+ local_value(session, 'total_descent', '%.0f %s',
126
+ { :metric => 'm', :statute => 'ft' }) ])
127
+ t.row([ 'Calories:', "#{session.total_calories} kCal" ])
128
+
129
+ if (est_sweat_loss = session.est_sweat_loss)
130
+ t.row([ 'Est. Sweat Loss:', "#{est_sweat_loss} ml" ])
131
+ end
132
+ t.row([ 'Avg. HR:', session.avg_heart_rate ?
133
+ "#{session.avg_heart_rate} bpm" : '-' ])
134
+ t.row([ 'Max. HR:', session.max_heart_rate ?
135
+ "#{session.max_heart_rate} bpm" : '-' ])
134
136
 
135
137
  if @fit_activity.physiological_metrics &&
136
138
  (physiological_metrics = @fit_activity.physiological_metrics.last)
@@ -53,7 +53,8 @@ module PostRunner
53
53
  def generate_html(doc)
54
54
  doc.unique(:activityview_style) {
55
55
  doc.head {
56
- [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
56
+ [ 'jquery/jquery-3.5.1.min.js', 'flot/jquery.flot.js',
57
+ #'flot/jquery.flot.time.js' ].each do |js|
57
58
  'flot/jquery.flot.time.js' ].each do |js|
58
59
  doc.script({ 'language' => 'javascript',
59
60
  'type' => 'text/javascript', 'src' => js })
@@ -30,7 +30,7 @@ module PostRunner
30
30
  :unit => select_unit('min/km'),
31
31
  :graph => :line_graph,
32
32
  :colors => '#0A7BEE',
33
- :show => @sport == 'running' || @sport == 'multisport',
33
+ :show => @sport == 'running' || @sport == 'multisport'
34
34
  },
35
35
  {
36
36
  :id => 'speed',
@@ -40,9 +40,17 @@ module PostRunner
40
40
  :colors => '#0A7BEE',
41
41
  :show => @sport != 'running'
42
42
  },
43
+ {
44
+ :id => 'Power_18FB2CF01A4B430DAD66988C847421F4',
45
+ :label => 'Power',
46
+ :unit => select_unit('Watts'),
47
+ :graph => :line_graph,
48
+ :colors => '#FFAC2E',
49
+ :show => @sport == 'running' || @sport == 'multisport'
50
+ },
43
51
  {
44
52
  :id => 'altitude',
45
- :label => 'Elevation',
53
+ :label => 'Altitude',
46
54
  :unit => select_unit('m'),
47
55
  :graph => :line_graph,
48
56
  :colors => '#5AAA44',
@@ -127,6 +135,22 @@ module PostRunner
127
135
  [ '#FF5558', nil ] ],
128
136
  :show => @sport == 'running' || @sport == 'multisport'
129
137
  },
138
+ {
139
+ :id => 'Form_Power_18FB2CF01A4B430DAD66988C847421F4',
140
+ :label => 'Form Power',
141
+ :unit => select_unit('Watts'),
142
+ :graph => :line_graph,
143
+ :colors => '#CBBB58',
144
+ :show => @sport == 'running' || @sport == 'multisport'
145
+ },
146
+ {
147
+ :id => 'Leg_Spring_Stiffness_18FB2CF01A4B430DAD66988C847421F4',
148
+ :label => 'Leg Spring Stiffness',
149
+ :unit => select_unit('kN/m'),
150
+ :graph => :line_graph,
151
+ :colors => '#358C88',
152
+ :show => @sport == 'running' || @sport == 'multisport'
153
+ },
130
154
  {
131
155
  :id => 'stance_time',
132
156
  :label => 'Ground Contact Time',
@@ -157,6 +181,14 @@ module PostRunner
157
181
  :colors => '#A88BBB',
158
182
  :show => @sport == 'cycling'
159
183
  },
184
+ {
185
+ :id => "Air_Power_18FB2CF01A4B430DAD66988C847421F4",
186
+ :label => 'Air Power',
187
+ :unit => select_unit('Watts'),
188
+ :graph => :line_graph,
189
+ :colors => '#919498',
190
+ :show => @sport == 'running' || @sport == 'multisport'
191
+ },
160
192
  {
161
193
  :id => 'temperature',
162
194
  :label => 'Temperature',
@@ -229,7 +261,7 @@ EOT
229
261
  end
230
262
 
231
263
  def java_script
232
- s = "$(function() {\n"
264
+ s = "$(document).ready(function() {\n"
233
265
 
234
266
  s << tooltip_div
235
267
  @charts.each do |chart|
@@ -44,7 +44,7 @@ module PostRunner
44
44
  def generate_html(doc)
45
45
  doc.unique(:dailymonitoringview_style) {
46
46
  doc.head {
47
- [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
47
+ [ 'jquery/jquery-3.5.1.min.js', 'flot/jquery.flot.js',
48
48
  'flot/jquery.flot.time.js' ].each do |js|
49
49
  doc.script({ 'language' => 'javascript',
50
50
  'type' => 'text/javascript', 'src' => js })
@@ -126,6 +126,19 @@ module PostRunner
126
126
  t.new_row
127
127
  end
128
128
 
129
+ if (ant_id = device.ant_id)
130
+ t.cell('ANT ID:')
131
+ t.cell(ant_id)
132
+ t.new_row
133
+ end
134
+
135
+ if ant_id && (sensor_settings = find_settings_by_ant_id(ant_id)) &&
136
+ (calibration_factor = sensor_settings.calibration_factor)
137
+ t.cell('Calibration Factor')
138
+ t.cell('%.1f' % calibration_factor)
139
+ t.new_row
140
+ end
141
+
129
142
  if (rx_ok = device.rx_packets_ok) && (rx_err = device.rx_packets_err)
130
143
  t.cell('Packet Errors:')
131
144
  t.cell('%d%%' % ((rx_err.to_f / (rx_ok + rx_err)) * 100).to_i)
@@ -148,6 +161,14 @@ module PostRunner
148
161
  tables
149
162
  end
150
163
 
164
+ private
165
+
166
+ def find_settings_by_ant_id(ant_id)
167
+ @fit_activity.sensor_settings.find do |sensor|
168
+ sensor.ant_id == ant_id
169
+ end
170
+ end
171
+
151
172
  end
152
173
 
153
174
  end
@@ -75,7 +75,7 @@ module PostRunner
75
75
  case event.event
76
76
  when 'timer'
77
77
  name = "Timer (#{event.event_type.gsub(/_/, ' ')})"
78
- value = event.timer_trigger
78
+ value = '-'
79
79
  when 'course_point'
80
80
  name = 'Course Point'
81
81
  value = event.message_index
@@ -121,12 +121,19 @@ module PostRunner
121
121
  when 'comm_timeout'
122
122
  name 'Communication timeout'
123
123
  value = event.comm_timeout
124
+ when 'off_course'
125
+ name = 'Off Course'
126
+ value = '-'
124
127
  when 'recovery_hr'
125
128
  name = 'Recovery heart rate'
126
129
  value = "#{event.recovery_hr} bpm"
127
130
  when 'recovery_time'
128
131
  name = 'Recovery time'
129
- value = "#{secsToDHMS(event.recovery_time * 60)}"
132
+ if event.recovery_time
133
+ value = "#{secsToDHMS(event.recovery_time * 60)}"
134
+ else
135
+ value = '-'
136
+ end
130
137
  when 'recovery_info'
131
138
  name = 'Recovery info'
132
139
  mins = event.recovery_info
@@ -140,6 +147,9 @@ module PostRunner
140
147
  when 'lactate_threshold_speed'
141
148
  name = 'Lactate Threshold Pace'
142
149
  value = pace(event, 'lactate_threshold_speed')
150
+ when 'functional_threshold_power'
151
+ name = 'Functional Threshold Power'
152
+ value = "#{event.functional_threshold_power} W"
143
153
  else
144
154
  name = event.event
145
155
  value = event.data
@@ -63,6 +63,7 @@ module PostRunner
63
63
  'hiking' => 'Hiking',
64
64
  'multisport' => 'Multisport',
65
65
  'paddling' => 'Paddling',
66
+ 'commuting' => 'Commuting',
66
67
  'all' => 'All'
67
68
  }
68
69
  ActivitySubTypes = {
@@ -508,9 +508,7 @@ module PostRunner
508
508
  # Not all FIT file have indexed device sections. In case the device
509
509
  # index is nil we'll take the first entry.
510
510
  if (di.device_index.nil? || di.device_index == 0) &&
511
- (di.manufacturer &&
512
- (di.garmin_product || di.product) &&
513
- di.numeric_product && di.serial_number)
511
+ di.numeric_manufacturer && di.numeric_product
514
512
  return {
515
513
  :manufacturer => di.manufacturer,
516
514
  :product => di.garmin_product || di.product,
@@ -310,6 +310,7 @@ EOT
310
310
  handle_version_update
311
311
  import_legacy_archive
312
312
 
313
+ retval = 0
313
314
  case (cmd = args.shift)
314
315
  when 'check'
315
316
  if args.empty?
@@ -334,7 +335,9 @@ EOT
334
335
  # is given, use the current date.
335
336
  @ffs.monthly_report(day_in_localtime(args, '%Y-%m-01'))
336
337
  when 'delete'
337
- process_activities(args, :delete)
338
+ unless process_activities(args, :delete)
339
+ retval = 1
340
+ end
338
341
  when 'dump'
339
342
  @filter = Fit4Ruby::FitFilter.new unless @filter
340
343
  process_files_or_activities(args, :dump)
@@ -344,9 +347,13 @@ EOT
344
347
  if args.empty?
345
348
  # If we have no file or directory for the import command, we get the
346
349
  # most recently used directory from the runtime config.
347
- process_files([ @db['config']['import_dir'] ], :import)
350
+ unless process_files([ @db['config']['import_dir'] ], :import)
351
+ retval = 1
352
+ end
348
353
  else
349
- process_files(args, :import)
354
+ unless process_files(args, :import)
355
+ retval = 1
356
+ end
350
357
  if args.length == 1 && Dir.exists?(args[0])
351
358
  # If only one directory was specified as argument we store the
352
359
  # directory for future use.
@@ -361,7 +368,9 @@ EOT
361
368
  unless (@name = args.shift)
362
369
  Log.abort 'You must provide a new name for the activity'
363
370
  end
364
- process_activities(args, :rename)
371
+ unless process_activities(args, :rename)
372
+ retval = 1
373
+ end
365
374
  when 'set'
366
375
  unless (@attribute = args.shift)
367
376
  Log.abort 'You must specify the attribute you want to change'
@@ -369,7 +378,9 @@ EOT
369
378
  unless (@value = args.shift)
370
379
  Log.abort 'You must specify the new value for the attribute'
371
380
  end
372
- process_activities(args, :set)
381
+ unless process_activities(args, :set)
382
+ retval = 1
383
+ end
373
384
  when 'show'
374
385
  if args.empty?
375
386
  @ffs.show_list_in_browser
@@ -378,12 +389,18 @@ EOT
378
389
  # given day in a browser.
379
390
  @ffs.show_monitoring(args[0])
380
391
  else
381
- process_activities(args, :show)
392
+ unless process_activities(args, :show)
393
+ retval = 1
394
+ end
382
395
  end
383
396
  when 'sources'
384
- process_activities(args, :sources)
397
+ unless process_activities(args, :sources)
398
+ retval = 1
399
+ end
385
400
  when 'summary'
386
- process_activities(args, :summary)
401
+ unless process_activities(args, :summary)
402
+ retval = 1
403
+ end
387
404
  when 'units'
388
405
  change_unit_system(args)
389
406
  when 'htmldir'
@@ -399,7 +416,7 @@ EOT
399
416
  # Ensure that all updates are written to the database.
400
417
  @db.sync
401
418
 
402
- 0
419
+ retval
403
420
  end
404
421
 
405
422
  def help
@@ -409,11 +426,13 @@ EOT
409
426
  def process_files_or_activities(files_or_activities, command)
410
427
  files_or_activities.each do |foa|
411
428
  if foa[0] == ':'
412
- process_activities([ foa ], command)
429
+ return false unless process_activities([ foa ], command)
413
430
  else
414
- process_files([ foa ], command)
431
+ return false unless process_files([ foa ], command)
415
432
  end
416
433
  end
434
+
435
+ true
417
436
  end
418
437
 
419
438
  def process_activities(activity_refs, command)
@@ -426,15 +445,19 @@ EOT
426
445
  activities = @ffs.find(a_ref[1..-1])
427
446
  if activities.empty?
428
447
  Log.warn "No matching activities found for '#{a_ref}'"
429
- return
448
+ return false
449
+ end
450
+ activities.each do |a|
451
+ unless process_activity(a, command)
452
+ return false
453
+ end
430
454
  end
431
- activities.each { |a| process_activity(a, command) }
432
455
  else
433
456
  Log.abort "Activity references must start with ':': #{a_ref}"
434
457
  end
435
458
  end
436
459
 
437
- nil
460
+ true
438
461
  end
439
462
 
440
463
  def process_files(files_or_dirs, command)
@@ -445,12 +468,14 @@ EOT
445
468
  files_or_dirs.each do |fod|
446
469
  if File.directory?(fod)
447
470
  Dir.glob(File.join(fod, '*.FIT'), File::FNM_CASEFOLD).each do |file|
448
- process_file(file, command)
471
+ return false unless process_file(file, command)
449
472
  end
450
473
  else
451
- process_file(fod, command)
474
+ return false unless process_file(fod, command)
452
475
  end
453
476
  end
477
+
478
+ true
454
479
  end
455
480
 
456
481
  # Process a single FIT file according to the given command.
@@ -115,6 +115,10 @@ module PostRunner
115
115
  t.localtime(utc_offset).strftime('%H:%M')
116
116
  end
117
117
 
118
+ def prefix_for_time(t, a)
119
+ a.window_start_time.strftime('%Y-%m-%d') == t.strftime('%Y-%m-%d') ? ' ' : '+'
120
+ end
121
+
118
122
  def daily_sleep_cycle_table(analyzer)
119
123
  ti = FlexiTable.new
120
124
  ti.head
@@ -129,8 +133,8 @@ module PostRunner
129
133
  if last_to_time && c.from_time > last_to_time
130
134
  # We have a gap in the sleep cycles.
131
135
  ti.cell('Wake')
132
- cell_right_aligned(ti, time_as_hm(last_to_time, utc_offset))
133
- cell_right_aligned(ti, time_as_hm(c.from_time, utc_offset))
136
+ cell_right_aligned(ti, prefix_for_time(last_to_time, analyzer) + time_as_hm(last_to_time, utc_offset))
137
+ cell_right_aligned(ti, prefix_for_time(c.from_time, analyzer) + time_as_hm(c.from_time, utc_offset))
134
138
  cell_right_aligned(ti, "(#{secsToHM(c.from_time - last_to_time)})")
135
139
  ti.cell('')
136
140
  ti.cell('')
@@ -139,8 +143,8 @@ module PostRunner
139
143
  end
140
144
 
141
145
  ti.cell((idx + 1).to_s, format)
142
- ti.cell(c.from_time.localtime(utc_offset).strftime('%H:%M'), format)
143
- ti.cell(c.to_time.localtime(utc_offset).strftime('%H:%M'), format)
146
+ ti.cell(prefix_for_time(c.from_time.localtime(utc_offset), analyzer) + c.from_time.localtime(utc_offset).strftime('%H:%M'), format)
147
+ ti.cell(prefix_for_time(c.to_time.localtime(utc_offset), analyzer) + c.to_time.localtime(utc_offset).strftime('%H:%M'), format)
144
148
 
145
149
  duration = c.to_time - c.from_time
146
150
  totals[:duration] += duration
@@ -111,7 +111,9 @@ module PostRunner
111
111
  [ 'Longest Distance', '%.3f km' % (@distance / 1000.0), '-' ] :
112
112
  [ PersonalRecords::SpeedRecordDistances[@sport][@distance],
113
113
  secsToHMS(@duration),
114
- speedToPace(@distance / @duration) ]) +
114
+ @sport == 'running' ?
115
+ speedToPace(@distance / @duration) :
116
+ '%.1f' % (@distance / @duration * 3.6) ]) +
115
117
  [ @store['file_store'].ref_by_activity(@activity),
116
118
  ActivityLink.new(@activity, false),
117
119
  @start_time.strftime("%Y-%m-%d") ])
@@ -220,7 +222,9 @@ module PostRunner
220
222
  def generate_table
221
223
  t = FlexiTable.new
222
224
  t.head
223
- t.row([ 'Record', 'Time/Dist.', 'Avg. Pace', 'Ref.', 'Activity',
225
+ t.row([ 'Record', 'Time/Dist.',
226
+ @sport == 'running' ? 'Avg. Pace' : 'Avg. Speed',
227
+ 'Ref.', 'Activity',
224
228
  'Date' ],
225
229
  { :halign => :center })
226
230
  t.set_column_attributes([