fit4ruby 3.3.0 → 3.8.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.
data/lib/fit4ruby/Lap.rb CHANGED
@@ -12,20 +12,37 @@
12
12
 
13
13
  require 'fit4ruby/FitDataRecord'
14
14
  require 'fit4ruby/RecordAggregator'
15
+ require 'fit4ruby/FDR_DevField_Extension'
15
16
 
16
17
  module Fit4Ruby
17
18
 
18
19
  class Lap < FitDataRecord
19
20
 
20
21
  include RecordAggregator
22
+ include FDR_DevField_Extension
21
23
 
22
- attr_reader :records
24
+ attr_reader :records, :lengths
23
25
 
24
- def initialize(records, previous_lap, field_values)
26
+ # Create a new Lap object.
27
+ # @param top_level_record [FitDataRecord] Top level record that is Lap
28
+ # belongs to.
29
+ # @param records [Array of Records] Records to associate with the Lap.
30
+ # @param lengths [Array of Lengths] Lengths to associate with the Lap.
31
+ # @param first_length_index [Fixnum] Index of the first Length in this Lap.
32
+ # @param previous_lap [Lap] Previous Lap on same Session.
33
+ # @param field_values [Hash] Hash that provides initial values for certain
34
+ # fields.
35
+ def initialize(top_level_record, records, previous_lap, field_values,
36
+ first_length_index, lengths)
25
37
  super('lap')
38
+ @top_level_record = top_level_record
39
+ @lengths = lengths
26
40
  @meta_field_units['avg_stride_length'] = 'm'
27
41
  @records = records
28
42
  @previous_lap = previous_lap
43
+ @lengths.each { |length| @records += length.records }
44
+ @first_length_index = first_length_index
45
+ @num_lengths = @lengths.length
29
46
 
30
47
  if previous_lap && previous_lap.records && previous_lap.records.last
31
48
  # Set the start time of the new lap to the timestamp of the last record
@@ -40,13 +57,31 @@ module Fit4Ruby
40
57
  @total_elapsed_time = records.last.timestamp - @start_time
41
58
  end
42
59
 
60
+ # Create instance variables for developer fields
61
+ create_dev_field_instance_variables
62
+
43
63
  set_field_values(field_values)
44
64
  end
45
65
 
46
- def check(index)
66
+ def check(index, activity)
47
67
  unless @message_index == index
48
68
  Log.fatal "message_index must be #{index}, not #{@message_index}"
49
69
  end
70
+
71
+ return if @num_lengths.zero?
72
+
73
+ unless @first_length_index
74
+ Log.fatal 'first_length_index is not set'
75
+ end
76
+
77
+ @first_length_index.upto(@first_length_index - @num_lengths) do |i|
78
+ if (length = activity.lengths[i])
79
+ @lengths << length
80
+ else
81
+ Log.fatal "Lap references length #{i} which is not contained in "
82
+ "the FIT file."
83
+ end
84
+ end
50
85
  end
51
86
 
52
87
  # Compute the average stride length for this Session.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = Length.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
7
+ #
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of version 2 of the GNU General Public License as
10
+ # published by the Free Software Foundation.
11
+ #
12
+
13
+ require 'fit4ruby/FitDataRecord'
14
+ require 'fit4ruby/RecordAggregator'
15
+
16
+ module Fit4Ruby
17
+
18
+ class Length < FitDataRecord
19
+
20
+ include RecordAggregator
21
+
22
+ attr_reader :records
23
+
24
+ def initialize(records, previous_length, field_values)
25
+ super('length')
26
+ @records = records
27
+ @previous_length = previous_length
28
+
29
+ if previous_length && previous_length.records && previous_length.records.last
30
+ # Set the start time of the new length to the timestamp of the last record
31
+ # of the previous length.
32
+ @start_time = previous_length.records.last.timestamp
33
+ elsif records.first
34
+ # Or to the timestamp of the first record.
35
+ @start_time = records.first.timestamp
36
+ end
37
+
38
+ if records.last
39
+ @total_elapsed_time = records.last.timestamp - @start_time
40
+ end
41
+
42
+ set_field_values(field_values)
43
+ end
44
+
45
+ def check(index)
46
+ unless @message_index == index
47
+ Log.fatal "message_index must be #{index}, not #{@message_index}"
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = Record.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2020 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -11,6 +11,7 @@
11
11
  #
12
12
 
13
13
  require 'fit4ruby/FitDataRecord'
14
+ require 'fit4ruby/FDR_DevField_Extension'
14
15
 
15
16
  module Fit4Ruby
16
17
 
@@ -18,13 +19,21 @@ module Fit4Ruby
18
19
  # of primary measurements that are associated with a certain timestamp.
19
20
  class Record < FitDataRecord
20
21
 
22
+ include FDR_DevField_Extension
23
+
21
24
  # Create a new Record object.
25
+ # @param fit_entity The FitEntity this record belongs to
22
26
  # @param field_values [Hash] Hash that provides initial values for certain
23
27
  # fields.
24
- def initialize(field_values = {})
28
+ def initialize(top_level_record, field_values = {})
25
29
  super('record')
30
+ @top_level_record = top_level_record
26
31
  @meta_field_units['pace'] = 'min/km'
27
32
  @meta_field_units['run_cadence'] = 'spm'
33
+
34
+ # Create instance variables for developer fields
35
+ create_dev_field_instance_variables
36
+
28
37
  set_field_values(field_values)
29
38
  end
30
39
 
@@ -1,4 +1,4 @@
1
1
  module Fit4Ruby
2
2
  # The version number of the library.
3
- VERSION = '3.3.0'
3
+ VERSION = '3.8.0'
4
4
  end
data/spec/FitFile_spec.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = FitFile_spec.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015, 2020 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -12,107 +12,251 @@
12
12
 
13
13
  $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
14
 
15
+ #require "super_diff/rspec"
15
16
  require 'fit4ruby'
16
17
 
17
18
  ENV['TZ'] = 'UTC'
18
19
 
19
20
  describe Fit4Ruby do
20
-
21
21
  before(:each) do
22
- ts = Time.now
23
- a = Fit4Ruby::Activity.new
24
- a.total_timer_time = 30 * 60.0
25
- a.new_user_data({ :age => 33, :height => 1.78, :weight => 73.0,
26
- :gender => 'male', :activity_class => 4.0,
27
- :max_hr => 178 })
28
-
29
- a.new_event({ :event => 'timer', :event_type => 'start_time' })
30
- a.new_device_info({ :timestamp => ts,
31
- :device_index => 0, :manufacturer => 'garmin',
32
- :garmin_product => 'fenix3',
33
- :serial_number => 123456789 })
34
- a.new_device_info({ :timestamp => ts,
35
- :device_index => 1, :manufacturer => 'garmin',
36
- :garmin_product => 'fenix3_gps'})
37
- a.new_device_info({ :timestamp => ts,
38
- :device_index => 2, :manufacturer => 'garmin',
39
- :garmin_product => 'hrm_run',
40
- :battery_status => 'ok' })
41
- a.new_device_info({ :timestamp => ts,
42
- :device_index => 3, :manufacturer => 'garmin',
43
- :garmin_product => 'sdm4',
44
- :battery_status => 'ok' })
45
- a.new_data_sources({ :timestamp => ts, :distance => 1,
46
- :speed => 1, :cadence => 3, :elevation => 1,
47
- :heart_rate => 2 })
48
- laps = 0
49
- 0.upto(a.total_timer_time / 60) do |mins|
50
- ts += 60
51
- a.new_record({
52
- :timestamp => ts,
53
- :position_lat => 51.5512 - mins * 0.0008,
54
- :position_long => 11.647 + mins * 0.002,
55
- :distance => 200.0 * mins,
56
- :altitude => (100 + mins * 0.5).to_i,
57
- :speed => 3.1,
58
- :vertical_oscillation => 9 + mins * 0.02,
59
- :stance_time => 235.0 * mins * 0.01,
60
- :stance_time_percent => 32.0,
61
- :heart_rate => 140 + mins,
62
- :cadence => 75,
63
- :activity_type => 'running',
64
- :fractional_cadence => (mins % 2) / 2.0
65
- })
22
+ File.delete(fit_file) if File.exist?(fit_file)
23
+ expect(File.exist?(fit_file)).to be false
24
+ end
25
+
26
+ after(:each) { File.delete(fit_file) }
27
+
28
+ let(:fit_file) { 'test.fit' }
29
+ # Round the timestamp to seconds. This is what the file format can store. A
30
+ # higher resolution would create errors during comparions.
31
+ let(:timestamp) { Time.at(Time.now.to_i) }
32
+ let(:user_data) do
33
+ {
34
+ :age => 33, :height => 1.78, :weight => 73.0,
35
+ :gender => 'male', :activity_class => 4.0,
36
+ :max_hr => 178
37
+ }
38
+ end
39
+ let(:user_profile) do
40
+ {
41
+ :friendly_name => 'Fast Runner',
42
+ :gender => 'male',
43
+ :age => 33,
44
+ :height => 1.78,
45
+ :weight => 73.0,
46
+ :resting_heart_rate => 43
47
+ }
48
+ end
49
+ def device_info_fenix3(ts)
50
+ {
51
+ :timestamp => ts,
52
+ :device_index => 0, :manufacturer => 'garmin',
53
+ :garmin_product => 'fenix3',
54
+ :serial_number => 123456789
55
+ }
56
+ end
57
+ def device_info_fenix3_gps(ts)
58
+ {
59
+ :timestamp => ts,
60
+ :device_index => 1, :manufacturer => 'garmin',
61
+ :garmin_product => 'fenix3_gps'
62
+ }
63
+ end
64
+ def device_info_hrm_run(ts, bs = 'ok')
65
+ {
66
+ :timestamp => ts,
67
+ :device_index => 2, :manufacturer => 'garmin',
68
+ :garmin_product => 'hrm_run',
69
+ :battery_status => bs
70
+ }
71
+ end
72
+ def device_info_sdm4(ts)
73
+ {
74
+ :timestamp => timestamp,
75
+ :device_index => 3, :manufacturer => 'garmin',
76
+ :garmin_product => 'sdm4',
77
+ :battery_status => 'ok'
78
+ }
79
+ end
80
+
81
+ context 'running activity' do
82
+ let(:activity) do
83
+ ts = timestamp
84
+ a = Fit4Ruby::Activity.new
85
+ a.total_timer_time = 30 * 60.0
86
+ a.new_user_data(user_data)
87
+ a.new_user_profile(user_profile)
88
+
89
+ a.new_event({ :event => 'timer', :event_type => 'start_time' })
90
+ a.new_device_info(device_info_fenix3(ts))
91
+ a.new_device_info(device_info_fenix3_gps(ts))
92
+ a.new_device_info(device_info_hrm_run(ts))
93
+ a.new_device_info(device_info_sdm4(ts))
94
+ a.new_data_sources({ :timestamp => ts, :distance => 1,
95
+ :speed => 1, :cadence => 3, :elevation => 1,
96
+ :heart_rate => 2 })
97
+ laps = 0
98
+ 0.upto(a.total_timer_time / 60) do |mins|
99
+ ts += 60
100
+ a.new_record({
101
+ :timestamp => ts,
102
+ :position_lat => 51.5512 - mins * 0.0008,
103
+ :position_long => 11.647 + mins * 0.002,
104
+ :distance => 200.0 * mins,
105
+ :altitude => (100 + mins * 0.5).to_i,
106
+ :speed => 3.1,
107
+ :vertical_oscillation => 9 + mins * 0.02,
108
+ :stance_time => 235.0 * mins * 0.01,
109
+ :stance_time_percent => 32.0,
110
+ :heart_rate => 140 + mins,
111
+ :cadence => 75,
112
+ :activity_type => 'running',
113
+ :fractional_cadence => (mins % 2) / 2.0
114
+ })
115
+
116
+ if mins > 0 && mins % 5 == 0
117
+ a.new_lap({ :timestamp => ts, :sport => 'running',
118
+ :message_index => laps, :total_cycles => 195 })
119
+ laps += 1
120
+ end
121
+ end
122
+ a.new_session({ :timestamp => ts, :sport => 'running' })
123
+ a.new_event({ :timestamp => ts, :event => 'recovery_time',
124
+ :event_type => 'marker',
125
+ :recovery_time => 2160 })
126
+ a.new_event({ :timestamp => ts, :event => 'vo2max',
127
+ :event_type => 'marker', :vo2max => 52 })
128
+ a.new_event({ :timestamp => ts, :event => 'timer',
129
+ :event_type => 'stop_all' })
130
+ ts += 1
131
+ a.new_device_info(device_info_fenix3(ts))
132
+ a.new_device_info(device_info_fenix3_gps(ts))
133
+ a.new_device_info(device_info_hrm_run(ts, 'low'))
134
+ a.new_device_info(device_info_sdm4(ts))
135
+ ts += 120
136
+ a.new_event({ :timestamp => ts, :event => 'recovery_hr',
137
+ :event_type => 'marker', :recovery_hr => 132 })
138
+
139
+ a.aggregate
140
+ a
141
+ end
142
+
143
+ it 'should write an Activity FIT file and read it back' do
144
+ Fit4Ruby.write(fit_file, activity)
145
+ expect(File.exist?(fit_file)).to be true
66
146
 
67
- if mins > 0 && mins % 5 == 0
68
- a.new_lap({ :timestamp => ts, :sport => 'running',
69
- :message_index => laps, :total_cycles => 195 })
70
- laps += 1
147
+ b = Fit4Ruby.read(fit_file)
148
+ expect(b.laps.count).to eq 6
149
+ expect(b.lengths.count).to eq 0
150
+ expect(b.export).to eq(activity.export)
151
+ end
152
+
153
+ end
154
+
155
+ context 'swimming activity' do
156
+ let(:activity) do
157
+ ts = timestamp
158
+ laps = 0
159
+ lengths = 0
160
+ a = Fit4Ruby::Activity.new
161
+
162
+ a.total_timer_time = 30 * 60.0
163
+ a.new_device_info(device_info_fenix3(ts))
164
+
165
+ 0.upto(a.total_timer_time / 60) do |mins|
166
+ ts += 60
167
+ if mins > 0 && mins % 5 == 0
168
+ a.new_lap({ :timestamp => ts, :sport => 'swimming',
169
+ :message_index => laps, :total_cycles => 195 })
170
+ laps += 1
171
+
172
+ a.new_length({ :timestamp => ts, :event => 'length',
173
+ :message_index => lengths, :total_strokes => 45 })
174
+ lengths += 1
175
+ end
71
176
  end
177
+ a.aggregate
178
+ a
179
+ end
180
+
181
+ it 'should write an Activity FIT file and read it back' do
182
+ Fit4Ruby.write(fit_file, activity)
183
+ expect(File.exist?(fit_file)).to be true
184
+
185
+ b = Fit4Ruby.read(fit_file)
186
+ expect(b.laps.count).to eq 6
187
+ expect(b.lengths.count).to eq 6
188
+ expect(b.laps.select { |l| l.sport == 'swimming' }.count).to eq 6
189
+ expect(b.lengths.select { |l| l.total_strokes == 45 }.count).to eq 6
190
+ expect(b.export).to eq(activity.export)
72
191
  end
73
- a.new_session({ :timestamp => ts, :sport => 'running' })
74
- a.new_event({ :timestamp => ts, :event => 'recovery_time',
75
- :event_type => 'marker',
76
- :recovery_time => 2160 })
77
- a.new_event({ :timestamp => ts, :event => 'vo2max',
78
- :event_type => 'marker', :vo2max => 52 })
79
- a.new_event({ :timestamp => ts, :event => 'timer',
80
- :event_type => 'stop_all' })
81
- ts += 1
82
- a.new_device_info({ :timestamp => ts,
83
- :device_index => 0, :manufacturer => 'garmin',
84
- :garmin_product => 'fenix3',
85
- :serial_number => 123456789 })
86
- a.new_device_info({ :timestamp => ts,
87
- :device_index => 1, :manufacturer => 'garmin',
88
- :garmin_product => 'fenix3_gps'})
89
- a.new_device_info({ :timestamp => ts,
90
- :device_index => 2, :manufacturer => 'garmin',
91
- :garmin_product => 'hrm_run',
92
- :battery_status => 'low' })
93
- a.new_device_info({ :timestamp => ts,
94
- :device_index => 3, :manufacturer => 'garmin',
95
- :garmin_product => 'sdm4',
96
- :battery_status => 'ok' })
97
- ts += 120
98
- a.new_event({ :timestamp => ts, :event => 'recovery_hr',
99
- :event_type => 'marker', :recovery_hr => 132 })
100
-
101
- a.aggregate
102
-
103
- @activity = a
192
+
104
193
  end
105
194
 
106
- it 'should write an Activity FIT file and read it back' do
107
- fit_file = 'test.fit'
195
+ context 'activity with developer fields' do
196
+ let(:activity) do
197
+ ts = timestamp
198
+ laps = 0
199
+ lengths = 0
200
+ a = Fit4Ruby::Activity.new
108
201
 
109
- File.delete(fit_file) if File.exists?(fit_file)
110
- Fit4Ruby.write(fit_file, @activity)
111
- expect(File.exists?(fit_file)).to be true
202
+ a.total_timer_time = 30 * 60.0
203
+ a.new_device_info(device_info_fenix3(ts))
204
+ a.new_developer_data_id({
205
+ application_id: [ 24, 251, 44, 240, 26, 75, 67, 13,
206
+ 173, 102, 152, 140, 132, 116, 33, 244 ],
207
+ application_version: 77
208
+ })
209
+ a.new_field_description({
210
+ developer_data_index: 0,
211
+ field_definition_number: 0,
212
+ fit_base_type_id: 132,
213
+ field_name: 'Power',
214
+ units: 'Watts',
215
+ native_mesg_num: 20,
216
+ native_field_num: 7
217
+ })
218
+ a.new_data_sources({ :timestamp => ts, :distance => 1,
219
+ :speed => 1, :cadence => 3, :elevation => 1,
220
+ :heart_rate => 2 })
221
+ laps = 0
222
+ 0.upto(a.total_timer_time / 60) do |mins|
223
+ ts += 60
224
+ a.new_record({
225
+ :timestamp => ts,
226
+ :position_lat => 51.5512 - mins * 0.0008,
227
+ :position_long => 11.647 + mins * 0.002,
228
+ :distance => 200.0 * mins,
229
+ :altitude => (100 + mins * 0.5).to_i,
230
+ :speed => 3.1,
231
+ :vertical_oscillation => 9 + mins * 0.02,
232
+ :stance_time => 235.0 * mins * 0.01,
233
+ :stance_time_percent => 32.0,
234
+ :heart_rate => 140 + mins,
235
+ :cadence => 75,
236
+ :activity_type => 'running',
237
+ :fractional_cadence => (mins % 2) / 2.0,
238
+ :Power_18FB2CF01A4B430DAD66988C847421F4 => 240
239
+ })
112
240
 
113
- b = Fit4Ruby.read(fit_file)
114
- expect(b.inspect).to eq(@activity.inspect)
115
- File.delete(fit_file)
241
+ if mins > 0 && mins % 5 == 0
242
+ a.new_lap({ :timestamp => ts, :sport => 'running',
243
+ :message_index => laps, :total_cycles => 195 })
244
+ laps += 1
245
+ end
246
+ end
247
+ a.aggregate
248
+ a
249
+ end
250
+
251
+ it 'should write an Activity FIT file and read it back' do
252
+ Fit4Ruby.write(fit_file, activity)
253
+ expect(File.exist?(fit_file)).to be true
254
+
255
+ # This currently does not work as the saving of developer fields is not
256
+ # yet implemented.
257
+ #b = Fit4Ruby.read(fit_file)
258
+ #expect(b.export).to eq(activity.export)
259
+ end
116
260
  end
117
261
 
118
262
  end