fit4ruby 3.3.0 → 3.8.0

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