fit4ruby 3.7.0 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -21,9 +21,9 @@ module Fit4Ruby
21
21
  [ 'uint16', 'uint16', 0xFFFF, 2 ],
22
22
  [ 'sint32', 'int32', 0x7FFFFFFF, 4 ],
23
23
  [ 'uint32', 'uint32', 0xFFFFFFFF, 4 ],
24
- [ 'string', 'string', '', 0 ],
24
+ [ 'string', 'string', 0, 1 ],
25
25
  [ 'float32', 'float', 0xFFFFFFFF, 4 ],
26
- [ 'float63', 'double', 0xFFFFFFFF, 4 ],
26
+ [ 'float64', 'double', 0xFFFFFFFFFFFFFFFF, 8 ],
27
27
  [ 'uint8z', 'uint8', 0, 1 ],
28
28
  [ 'uint16z', 'uint16', 0, 2 ],
29
29
  [ 'uint32z', 'uint32', 0, 4 ],
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = GlobalFitMessage.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
@@ -21,7 +21,8 @@ module Fit4Ruby
21
21
  # the data fields of the message.
22
22
  class GlobalFitMessage
23
23
 
24
- attr_reader :name, :number, :fields_by_name, :fields_by_number
24
+ attr_reader :name, :number, :fields_by_name, :fields_by_number,
25
+ :field_values_by_name
25
26
 
26
27
  # The Field objects describe the name, type and optional attributes of a
27
28
  # FitMessage definition field. It also provides methods to convert field
@@ -38,8 +39,21 @@ module Fit4Ruby
38
39
  @opts = opts
39
40
  end
40
41
 
42
+ def is_undefined?(value)
43
+ value == FitDefinitionFieldBase.undefined_value(@type)
44
+ end
45
+
46
+ def is_string?
47
+ @type == 'string'
48
+ end
49
+
50
+ def is_array?
51
+ @opts[:array] == true
52
+ end
53
+
41
54
  def to_machine(value)
42
- return nil if value.nil?
55
+ return nil if value.nil? ||
56
+ value == FitDefinitionFieldBase.undefined_value(@type)
43
57
 
44
58
  if @opts.include?(:dict) &&
45
59
  (dict = GlobalFitDictionaries[@opts[:dict]])
@@ -51,6 +65,13 @@ module Fit4Ruby
51
65
  value *= 180.0 / 2147483648
52
66
  when 'date_time'
53
67
  value = fit_time_to_time(value)
68
+ when 'float'
69
+ if value >= 4294967295.0
70
+ return nil
71
+ end
72
+ end
73
+ if value.is_a?(Float) && value >= 4294967295.0
74
+ return nil
54
75
  end
55
76
  value /= @opts[:scale].to_f if @opts[:scale]
56
77
  value -= @opts[:offset] if @opts[:offset]
@@ -133,7 +154,7 @@ module Fit4Ruby
133
154
  value
134
155
  end
135
156
 
136
- def to_s(value)
157
+ def to_s(value = nil)
137
158
  return "[no value]" if value.nil?
138
159
 
139
160
  human_readable = to_human(value)
@@ -167,7 +188,7 @@ module Fit4Ruby
167
188
 
168
189
  def field(ref_value, type, name, opts = {})
169
190
  field = Field.new(type, name, opts)
170
- if ref_value.respond_to?('each')
191
+ if ref_value.respond_to?(:each)
171
192
  ref_value.each do |rv|
172
193
  @fields[rv] = field
173
194
  end
@@ -180,13 +201,13 @@ module Fit4Ruby
180
201
  # Select the alternative field based on the actual field values of the
181
202
  # FitMessageRecord.
182
203
  def select(field_values_by_name)
183
- unless (value = field_values_by_name[@ref_field])
204
+ unless (value_of_referenced_field = field_values_by_name[@ref_field])
184
205
  Log.fatal "The selection field #{@ref_field} for the alternative " +
185
206
  "field is undefined in global message #{@message.name}: " +
186
207
  field_values_by_name.inspect
187
208
  end
188
209
  @fields.each do |ref_value, field|
189
- return field if ref_value == value
210
+ return field if ref_value == value_of_referenced_field
190
211
  end
191
212
  return @fields[:default] if @fields[:default]
192
213
 
@@ -199,7 +220,7 @@ module Fit4Ruby
199
220
  # Create a new GlobalFitMessage definition.
200
221
  # @param name [String] name of the FIT message
201
222
  # @param number [Fixnum] global message number
202
- def initialize(name, number)
223
+ def initialize(name, number, field_values_by_name = {})
203
224
  @name = name
204
225
  @number = number
205
226
  # Field names must be unique. A name always matches a single Field.
@@ -207,13 +228,57 @@ module Fit4Ruby
207
228
  # Field numbers are not unique. A group of alternative fields shares the
208
229
  # same number and is stored as an AltField. Otherwise as Field.
209
230
  @fields_by_number = {}
231
+ # To generate the proper definition message we need to know the length
232
+ # of String and Array fields. This is only needed when writing FIT
233
+ # files.
234
+ @field_values_by_name = field_values_by_name
210
235
  end
211
236
 
212
237
  # Two GlobalFitMessage objects are considered equal if they have the same
213
- # number, name and list of named fields.
238
+ # number, name and list of named fields. In case they have String or Array
239
+ # values, they must have identical size.
214
240
  def ==(m)
215
- @number == m.number && @name == m.name &&
216
- @fields_by_name.keys.sort == m.fields_by_name.keys.sort
241
+ unless @number == m.number && @name == m.name &&
242
+ @fields_by_name.keys.sort == m.fields_by_name.keys.sort
243
+ return false
244
+ end
245
+
246
+ unless @field_values_by_name.size == m.field_values_by_name.size
247
+ return false
248
+ end
249
+
250
+ unless @field_values_by_name.empty?
251
+ @field_values_by_name.keys.each do |name|
252
+ a = @field_values_by_name[name]
253
+ b = m.field_values_by_name[name]
254
+ if a.class != b.class
255
+ return false
256
+ end
257
+ if a.is_a?(String)
258
+ if a.bytes.length != b.bytes.length
259
+ return false
260
+ end
261
+ elsif a.is_a?(Array)
262
+ if a.length != b.length
263
+ return false
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ true
270
+ end
271
+
272
+ def each_field(field_values)
273
+ @fields_by_number.each do |number, field|
274
+ if field.is_a?(AltField)
275
+ # For alternative fields, we need to look at the value of the
276
+ # selector field and pick the corresponding Field.
277
+ field = field.select(field_values)
278
+ end
279
+
280
+ yield(number, field)
281
+ end
217
282
  end
218
283
 
219
284
  # Define a new Field for this message definition.
@@ -255,7 +320,7 @@ module Fit4Ruby
255
320
  end
256
321
 
257
322
  def construct(field_values_by_name)
258
- gfm = GlobalFitMessage.new(@name, @number)
323
+ gfm = GlobalFitMessage.new(@name, @number, field_values_by_name)
259
324
 
260
325
  @fields_by_number.each do |number, field|
261
326
  if field.is_a?(AltField)
@@ -269,7 +334,7 @@ module Fit4Ruby
269
334
  gfm
270
335
  end
271
336
 
272
- def write(io, local_message_type, global_fit_message = self)
337
+ def write(io, local_message_type)
273
338
  header = FitRecordHeader.new
274
339
  header.normal = 0
275
340
  header.message_type = 1
@@ -278,7 +343,7 @@ module Fit4Ruby
278
343
 
279
344
  definition = FitDefinition.new
280
345
  definition.global_message_number = @number
281
- definition.setup(global_fit_message)
346
+ definition.setup(self, @field_values_by_name)
282
347
  definition.write(io)
283
348
  end
284
349
 
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = GlobalFitMessages.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
@@ -1017,7 +1017,7 @@ module Fit4Ruby
1017
1017
  field 18, 'uint8z', 'undocumented_field_18'
1018
1018
  field 19, 'uint8z', 'undocumented_field_19'
1019
1019
  field 20, 'uint8z', 'undocumented_field_20'
1020
- field 21, 'uint16', 'undocumented_field_21'
1020
+ field 21, 'uint16', 'wheel_size', :unit => 'mm'
1021
1021
  field 25, 'uint16', 'undocumented_field_25'
1022
1022
  field 26, 'uint16', 'undocumented_field_26'
1023
1023
  field 27, 'uint8', 'undocumented_field_27'
data/lib/fit4ruby/Lap.rb CHANGED
@@ -12,24 +12,30 @@
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
24
  attr_reader :records, :lengths
23
25
 
24
26
  # Create a new Lap object.
27
+ # @param top_level_record [FitDataRecord] Top level record that is Lap
28
+ # belongs to.
25
29
  # @param records [Array of Records] Records to associate with the Lap.
26
30
  # @param lengths [Array of Lengths] Lengths to associate with the Lap.
27
31
  # @param first_length_index [Fixnum] Index of the first Length in this Lap.
28
32
  # @param previous_lap [Lap] Previous Lap on same Session.
29
33
  # @param field_values [Hash] Hash that provides initial values for certain
30
34
  # fields.
31
- def initialize(records, previous_lap, field_values, first_length_index, lengths)
35
+ def initialize(top_level_record, records, previous_lap, field_values,
36
+ first_length_index, lengths)
32
37
  super('lap')
38
+ @top_level_record = top_level_record
33
39
  @lengths = lengths
34
40
  @meta_field_units['avg_stride_length'] = 'm'
35
41
  @records = records
@@ -51,6 +57,9 @@ module Fit4Ruby
51
57
  @total_elapsed_time = records.last.timestamp - @start_time
52
58
  end
53
59
 
60
+ # Create instance variables for developer fields
61
+ create_dev_field_instance_variables
62
+
54
63
  set_field_values(field_values)
55
64
  end
56
65
 
@@ -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.7.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,115 +12,147 @@
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
21
+ before(:each) do
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
+
20
28
  let(:fit_file) { 'test.fit' }
21
- let(:timestamp) { Time.now }
22
- let(:activity) do
23
- ts = timestamp
24
- a = Fit4Ruby::Activity.new
25
- a.total_timer_time = 30 * 60.0
26
- a.new_user_data({ :age => 33, :height => 1.78, :weight => 73.0,
27
- :gender => 'male', :activity_class => 4.0,
28
- :max_hr => 178 })
29
-
30
- a.new_event({ :event => 'timer', :event_type => 'start_time' })
31
- a.new_device_info({ :timestamp => ts,
32
- :device_index => 0, :manufacturer => 'garmin',
33
- :garmin_product => 'fenix3',
34
- :serial_number => 123456789 })
35
- a.new_device_info({ :timestamp => ts,
36
- :device_index => 1, :manufacturer => 'garmin',
37
- :garmin_product => 'fenix3_gps'})
38
- a.new_device_info({ :timestamp => ts,
39
- :device_index => 2, :manufacturer => 'garmin',
40
- :garmin_product => 'hrm_run',
41
- :battery_status => 'ok' })
42
- a.new_device_info({ :timestamp => ts,
43
- :device_index => 3, :manufacturer => 'garmin',
44
- :garmin_product => 'sdm4',
45
- :battery_status => 'ok' })
46
- a.new_data_sources({ :timestamp => ts, :distance => 1,
47
- :speed => 1, :cadence => 3, :elevation => 1,
48
- :heart_rate => 2 })
49
- laps = 0
50
- 0.upto(a.total_timer_time / 60) do |mins|
51
- ts += 60
52
- a.new_record({
53
- :timestamp => ts,
54
- :position_lat => 51.5512 - mins * 0.0008,
55
- :position_long => 11.647 + mins * 0.002,
56
- :distance => 200.0 * mins,
57
- :altitude => (100 + mins * 0.5).to_i,
58
- :speed => 3.1,
59
- :vertical_oscillation => 9 + mins * 0.02,
60
- :stance_time => 235.0 * mins * 0.01,
61
- :stance_time_percent => 32.0,
62
- :heart_rate => 140 + mins,
63
- :cadence => 75,
64
- :activity_type => 'running',
65
- :fractional_cadence => (mins % 2) / 2.0
66
- })
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
+ })
67
115
 
68
- if mins > 0 && mins % 5 == 0
69
- a.new_lap({ :timestamp => ts, :sport => 'running',
70
- :message_index => laps, :total_cycles => 195 })
71
- laps += 1
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
72
121
  end
73
- end
74
- a.new_session({ :timestamp => ts, :sport => 'running' })
75
- a.new_event({ :timestamp => ts, :event => 'recovery_time',
76
- :event_type => 'marker',
77
- :recovery_time => 2160 })
78
- a.new_event({ :timestamp => ts, :event => 'vo2max',
79
- :event_type => 'marker', :vo2max => 52 })
80
- a.new_event({ :timestamp => ts, :event => 'timer',
81
- :event_type => 'stop_all' })
82
- ts += 1
83
- a.new_device_info({ :timestamp => ts,
84
- :device_index => 0, :manufacturer => 'garmin',
85
- :garmin_product => 'fenix3',
86
- :serial_number => 123456789 })
87
- a.new_device_info({ :timestamp => ts,
88
- :device_index => 1, :manufacturer => 'garmin',
89
- :garmin_product => 'fenix3_gps'})
90
- a.new_device_info({ :timestamp => ts,
91
- :device_index => 2, :manufacturer => 'garmin',
92
- :garmin_product => 'hrm_run',
93
- :battery_status => 'low' })
94
- a.new_device_info({ :timestamp => ts,
95
- :device_index => 3, :manufacturer => 'garmin',
96
- :garmin_product => 'sdm4',
97
- :battery_status => 'ok' })
98
- ts += 120
99
- a.new_event({ :timestamp => ts, :event => 'recovery_hr',
100
- :event_type => 'marker', :recovery_hr => 132 })
101
-
102
- a.aggregate
103
- a
104
- 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 })
105
138
 
106
- before do
107
- File.delete(fit_file) if File.exist?(fit_file)
108
- expect(File.exist?(fit_file)).to be false
109
- end
139
+ a.aggregate
140
+ a
141
+ end
110
142
 
111
- after { File.delete(fit_file) }
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
112
146
 
113
- it 'should write an Activity FIT file and read it back' do
114
- Fit4Ruby.write(fit_file, activity)
115
- expect(File.exist?(fit_file)).to be true
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
116
152
 
117
- b = Fit4Ruby.read(fit_file)
118
- expect(b.laps.count).to eq 6
119
- expect(b.lengths.count).to eq 0
120
- expect(b.inspect).to eq(activity.inspect)
121
153
  end
122
154
 
123
- context 'activity with Lengths' do
155
+ context 'swimming activity' do
124
156
  let(:activity) do
125
157
  ts = timestamp
126
158
  laps = 0
@@ -128,9 +160,7 @@ describe Fit4Ruby do
128
160
  a = Fit4Ruby::Activity.new
129
161
 
130
162
  a.total_timer_time = 30 * 60.0
131
- a.new_device_info({ :timestamp => ts,
132
- :device_index => 0, :manufacturer => 'garmin',
133
- :serial_number => 123456789 })
163
+ a.new_device_info(device_info_fenix3(ts))
134
164
 
135
165
  0.upto(a.total_timer_time / 60) do |mins|
136
166
  ts += 60
@@ -144,6 +174,7 @@ describe Fit4Ruby do
144
174
  lengths += 1
145
175
  end
146
176
  end
177
+ a.aggregate
147
178
  a
148
179
  end
149
180
 
@@ -156,9 +187,76 @@ describe Fit4Ruby do
156
187
  expect(b.lengths.count).to eq 6
157
188
  expect(b.laps.select { |l| l.sport == 'swimming' }.count).to eq 6
158
189
  expect(b.lengths.select { |l| l.total_strokes == 45 }.count).to eq 6
159
- expect(b.inspect).to eq(activity.inspect)
190
+ expect(b.export).to eq(activity.export)
191
+ end
192
+
193
+ end
194
+
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
201
+
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
+ })
240
+
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
160
249
  end
161
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
162
260
  end
163
261
 
164
262
  end