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.
- checksums.yaml +5 -5
- data/Gemfile.lock +30 -6
- data/fit4ruby.gemspec +4 -2
- data/lib/fit4ruby/Activity.rb +145 -58
- data/lib/fit4ruby/BDFieldNameTranslator.rb +36 -0
- data/lib/fit4ruby/DeviceInfo.rb +37 -0
- data/lib/fit4ruby/FDR_DevField_Extension.rb +81 -0
- data/lib/fit4ruby/FieldDescription.rb +26 -8
- data/lib/fit4ruby/FileId.rb +2 -1
- data/lib/fit4ruby/FitDataRecord.rb +86 -33
- data/lib/fit4ruby/FitDefinition.rb +11 -4
- data/lib/fit4ruby/FitDefinitionField.rb +6 -6
- data/lib/fit4ruby/FitDefinitionFieldBase.rb +15 -6
- data/lib/fit4ruby/FitDeveloperDataFieldDefinition.rb +1 -1
- data/lib/fit4ruby/FitFile.rb +2 -0
- data/lib/fit4ruby/FitMessageRecord.rb +26 -21
- data/lib/fit4ruby/FitRecord.rb +2 -1
- data/lib/fit4ruby/FitTypeDefs.rb +2 -2
- data/lib/fit4ruby/GlobalFitDictionaries.rb +278 -22
- data/lib/fit4ruby/GlobalFitMessage.rb +84 -14
- data/lib/fit4ruby/GlobalFitMessages.rb +104 -10
- data/lib/fit4ruby/Lap.rb +38 -3
- data/lib/fit4ruby/Length.rb +53 -0
- data/lib/fit4ruby/Record.rb +11 -2
- data/lib/fit4ruby/version.rb +1 -1
- data/spec/FitFile_spec.rb +233 -89
- metadata +43 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 29f7e7354dfe1a0b9cfa30849b6131693deba24858f7cf6ec0745b923bcbd86a
|
4
|
+
data.tar.gz: 24aa3e37347db57f6e26078118bc20da45692097c395a21ad480702ebb66f0c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2638207885da4f16185df8ef71d7999855b2f9199be60d1156727473a855a01039ce0fd36faaa7093696f4576247f78c208c2628df200da2d5761832ebc46142
|
7
|
+
data.tar.gz: a7fd64132dba6d6594e9ade153e1596d66ef7f47c7846f79ad7971240ac8b6dfb0b457474e80dcd369faadc88dc79fee17233a3c3cc4cfe2d40f40f01df28e47
|
data/Gemfile.lock
CHANGED
@@ -1,15 +1,34 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
fit4ruby (
|
5
|
-
bindata (
|
4
|
+
fit4ruby (3.3.0)
|
5
|
+
bindata (= 2.3.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
bindata (2.
|
10
|
+
bindata (2.3.0)
|
11
|
+
coderay (1.1.2)
|
12
|
+
diff-lcs (1.3)
|
13
|
+
method_source (0.9.2)
|
14
|
+
pry (0.12.2)
|
15
|
+
coderay (~> 1.1.0)
|
16
|
+
method_source (~> 0.9.0)
|
11
17
|
rake (0.9.6)
|
12
|
-
|
18
|
+
rspec (3.8.0)
|
19
|
+
rspec-core (~> 3.8.0)
|
20
|
+
rspec-expectations (~> 3.8.0)
|
21
|
+
rspec-mocks (~> 3.8.0)
|
22
|
+
rspec-core (3.8.2)
|
23
|
+
rspec-support (~> 3.8.0)
|
24
|
+
rspec-expectations (3.8.4)
|
25
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
+
rspec-support (~> 3.8.0)
|
27
|
+
rspec-mocks (3.8.1)
|
28
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
29
|
+
rspec-support (~> 3.8.0)
|
30
|
+
rspec-support (3.8.2)
|
31
|
+
yard (0.9.20)
|
13
32
|
|
14
33
|
PLATFORMS
|
15
34
|
ruby
|
@@ -17,5 +36,10 @@ PLATFORMS
|
|
17
36
|
DEPENDENCIES
|
18
37
|
bundler (>= 1.6.4)
|
19
38
|
fit4ruby!
|
20
|
-
|
21
|
-
|
39
|
+
pry (>= 0.12)
|
40
|
+
rake (~> 12.0.0)
|
41
|
+
rspec (>= 3.8)
|
42
|
+
yard (~> 0.9.20)
|
43
|
+
|
44
|
+
BUNDLED WITH
|
45
|
+
2.0.2
|
data/fit4ruby.gemspec
CHANGED
@@ -24,8 +24,10 @@ EOT
|
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
spec.required_ruby_version = '>=2.0'
|
26
26
|
|
27
|
-
spec.add_dependency('bindata', '
|
27
|
+
spec.add_dependency('bindata', '~>2.4.8')
|
28
28
|
spec.add_development_dependency('yard', '~>0.9.20')
|
29
|
-
spec.add_development_dependency('rake', '~>0.
|
29
|
+
spec.add_development_dependency('rake', '~>12.0.0')
|
30
30
|
spec.add_development_dependency('bundler', '>=1.6.4')
|
31
|
+
spec.add_development_dependency('rspec', '>=3.8')
|
32
|
+
spec.add_development_dependency('pry', '>=0.12')
|
31
33
|
end
|
data/lib/fit4ruby/Activity.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Activity.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
|
@@ -24,6 +24,7 @@ require 'fit4ruby/UserProfile'
|
|
24
24
|
require 'fit4ruby/PhysiologicalMetrics'
|
25
25
|
require 'fit4ruby/Session'
|
26
26
|
require 'fit4ruby/Lap'
|
27
|
+
require 'fit4ruby/Length'
|
27
28
|
require 'fit4ruby/Record'
|
28
29
|
require 'fit4ruby/HRV'
|
29
30
|
require 'fit4ruby/HeartRateZones'
|
@@ -32,48 +33,74 @@ require 'fit4ruby/PersonalRecords'
|
|
32
33
|
|
33
34
|
module Fit4Ruby
|
34
35
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
36
|
+
# Activity files are arguably the most common type of FIT file. The Activity
|
37
|
+
# class represents the top-level structure of an activity FIT file.
|
38
|
+
# It holds references to all other data structures. Each of the objects it
|
39
|
+
# references are direct equivalents of the message record structures used in
|
40
|
+
# the FIT file.
|
38
41
|
class Activity < FitDataRecord
|
39
42
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
43
|
+
# These symbols are a complete list of all the sub-sections that an
|
44
|
+
# activity FIT file may contain. This list is used to generate accessors,
|
45
|
+
# instance variables and other sections of code. Some are just simple
|
46
|
+
# instance variables, but the majority can appear multiple times and hence
|
47
|
+
# are stored in an Array.
|
48
|
+
FILE_SECTIONS = [
|
49
|
+
:file_id,
|
50
|
+
:file_creator,
|
51
|
+
:events,
|
52
|
+
:device_infos,
|
53
|
+
:data_sources,
|
54
|
+
:epo_data,
|
55
|
+
:user_profiles,
|
56
|
+
:user_data,
|
57
|
+
:sensor_settings,
|
58
|
+
:developer_data_ids,
|
59
|
+
:field_descriptions,
|
60
|
+
:records,
|
61
|
+
:hrv,
|
62
|
+
:laps,
|
63
|
+
:lengths,
|
64
|
+
:heart_rate_zones,
|
65
|
+
:physiological_metrics,
|
66
|
+
:sessions,
|
67
|
+
:personal_records
|
68
|
+
]
|
69
|
+
|
70
|
+
attr_accessor *FILE_SECTIONS
|
45
71
|
|
46
72
|
# Create a new Activity object.
|
47
73
|
# @param field_values [Hash] A Hash that provides initial values for
|
48
74
|
# certain fields of the FitDataRecord.
|
49
75
|
def initialize(field_values = {})
|
50
76
|
super('activity')
|
51
|
-
@meta_field_units['total_gps_distance'] = 'm'
|
52
|
-
@num_sessions = 0
|
53
77
|
|
54
|
-
|
55
|
-
|
56
|
-
|
78
|
+
# The variables hold references to other parts of the FIT file. These
|
79
|
+
# can either be direct references to a certain FIT file section or an
|
80
|
+
# Array in case the section can appear multiple times in the FIT file.
|
81
|
+
@file_id = new_file_id()
|
82
|
+
@file_creator = new_file_creator()
|
57
83
|
@epo_data = nil
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
@events = []
|
66
|
-
@sessions = []
|
67
|
-
@laps = []
|
68
|
-
@records = []
|
69
|
-
@hrv = []
|
70
|
-
@heart_rate_zones = []
|
71
|
-
@personal_records = []
|
84
|
+
# Initialize the remaining variables as empty Array.
|
85
|
+
FILE_SECTIONS.each do |fs|
|
86
|
+
ivar_name = '@' + fs.to_s
|
87
|
+
unless instance_variable_defined?(ivar_name)
|
88
|
+
instance_variable_set(ivar_name, [])
|
89
|
+
end
|
90
|
+
end
|
72
91
|
|
92
|
+
# The following variables hold derived or auxilliary information that
|
93
|
+
# are not directly part of the FIT file.
|
94
|
+
@meta_field_units['total_gps_distance'] = 'm'
|
73
95
|
@cur_session_laps = []
|
96
|
+
|
74
97
|
@cur_lap_records = []
|
98
|
+
@cur_lap_lengths = []
|
99
|
+
|
100
|
+
@cur_length_records = []
|
75
101
|
|
76
102
|
@lap_counter = 1
|
103
|
+
@length_counter = 1
|
77
104
|
|
78
105
|
set_field_values(field_values)
|
79
106
|
end
|
@@ -92,11 +119,6 @@ module Fit4Ruby
|
|
92
119
|
end
|
93
120
|
@device_infos.each.with_index { |d, index| d.check(index) }
|
94
121
|
@sensor_settings.each.with_index { |s, index| s.check(index) }
|
95
|
-
unless @num_sessions == @sessions.count
|
96
|
-
Log.fatal "Activity record requires #{@num_sessions}, but "
|
97
|
-
"#{@sessions.length} session records were found in the "
|
98
|
-
"FIT file."
|
99
|
-
end
|
100
122
|
|
101
123
|
# Records must have consecutively growing timestamps and distances.
|
102
124
|
ts = Time.parse('1989-12-31')
|
@@ -141,11 +163,20 @@ module Fit4Ruby
|
|
141
163
|
|
142
164
|
# Laps must have a consecutively growing message index.
|
143
165
|
@laps.each.with_index do |lap, index|
|
144
|
-
lap.check(index)
|
166
|
+
lap.check(index, self)
|
145
167
|
# If we have heart rate zone records, there should be one for each
|
146
168
|
# lap
|
147
169
|
@heart_rate_zones[index].check(index) if @heart_rate_zones[index]
|
148
170
|
end
|
171
|
+
|
172
|
+
# Lengths must have a consecutively growing message index.
|
173
|
+
@lengths.each.with_index do |length, index|
|
174
|
+
length.check(index)
|
175
|
+
# If we have heart rate zone records, there should be one for each
|
176
|
+
# length
|
177
|
+
@heart_rate_zones[index].check(index) if @heart_rate_zones[index]
|
178
|
+
end
|
179
|
+
|
149
180
|
@sessions.each { |s| s.check(self) }
|
150
181
|
end
|
151
182
|
|
@@ -206,17 +237,22 @@ module Fit4Ruby
|
|
206
237
|
d
|
207
238
|
end
|
208
239
|
|
209
|
-
# Call this method to update the aggregated data fields stored in Lap
|
210
|
-
# Session objects.
|
240
|
+
# Call this method to update the aggregated data fields stored in Lap,
|
241
|
+
# Length, and Session objects.
|
211
242
|
def aggregate
|
212
243
|
@laps.each { |l| l.aggregate }
|
244
|
+
@lengths.each { |l| l.aggregate }
|
213
245
|
@sessions.each { |s| s.aggregate }
|
214
246
|
end
|
215
247
|
|
216
248
|
# Convenience method that averages the speed over all sessions.
|
217
249
|
def avg_speed
|
218
250
|
speed = 0.0
|
219
|
-
@sessions.each
|
251
|
+
@sessions.each do |s|
|
252
|
+
if (spd = s.avg_speed || s.enhanced_avg_speed)
|
253
|
+
speed += spd
|
254
|
+
end
|
255
|
+
end
|
220
256
|
speed / @sessions.length
|
221
257
|
end
|
222
258
|
|
@@ -287,13 +323,17 @@ module Fit4Ruby
|
|
287
323
|
def write(io, id_mapper)
|
288
324
|
@file_id.write(io, id_mapper)
|
289
325
|
@file_creator.write(io, id_mapper)
|
326
|
+
@epo_data.write(io, id_mapper) if @epo_data
|
290
327
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
328
|
+
ary_ivars = []
|
329
|
+
FILE_SECTIONS.each do |fs|
|
330
|
+
ivar_name = '@' + fs.to_s
|
331
|
+
if (ivar = instance_variable_get(ivar_name)) && ivar.respond_to?(:sort)
|
332
|
+
ary_ivars += ivar
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
ary_ivars.sort.each do |s|
|
297
337
|
s.write(io, id_mapper)
|
298
338
|
end
|
299
339
|
super
|
@@ -404,6 +444,16 @@ module Fit4Ruby
|
|
404
444
|
new_fit_data_record('lap', field_values)
|
405
445
|
end
|
406
446
|
|
447
|
+
# Add a new Length to the Activity. All previoulsy added Record objects are
|
448
|
+
# associated with this Length unless they have been associated with another
|
449
|
+
# Length before.
|
450
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
451
|
+
# certain fields of the FitDataRecord.
|
452
|
+
# @return [Length]
|
453
|
+
def new_length(field_values = {})
|
454
|
+
new_fit_data_record('length', field_values)
|
455
|
+
end
|
456
|
+
|
407
457
|
# Add a new HeartRateZones record to the Activity.
|
408
458
|
# @param field_values [Heash] A Hash that provides initial values for
|
409
459
|
# certain fields of the FitDataRecord.
|
@@ -433,18 +483,17 @@ module Fit4Ruby
|
|
433
483
|
# @return [TrueClass/FalseClass] true if both Activities are equal,
|
434
484
|
# otherwise false.
|
435
485
|
def ==(a)
|
436
|
-
super(a)
|
437
|
-
|
438
|
-
|
439
|
-
@
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
@sessions == a.sessions && personal_records == a.personal_records
|
486
|
+
return false unless super(a)
|
487
|
+
|
488
|
+
FILE_SECTIONS.each do |fs|
|
489
|
+
ivar_name = '@' + fs.to_s
|
490
|
+
ivar = instance_variable_get(ivar_name)
|
491
|
+
a_ivar = a.instance_variable_get(ivar_name)
|
492
|
+
|
493
|
+
return false unless ivar == a_ivar
|
494
|
+
end
|
495
|
+
|
496
|
+
true
|
448
497
|
end
|
449
498
|
|
450
499
|
# Create a new FitDataRecord.
|
@@ -490,14 +539,17 @@ module Fit4Ruby
|
|
490
539
|
# Ensure that all previous records have been assigned to a lap.
|
491
540
|
record = create_new_lap(lap_field_values)
|
492
541
|
end
|
493
|
-
@num_sessions += 1
|
494
542
|
@sessions << (record = Session.new(@cur_session_laps, @lap_counter,
|
495
543
|
field_values))
|
496
544
|
@cur_session_laps = []
|
497
545
|
when 'lap'
|
498
546
|
record = create_new_lap(field_values)
|
547
|
+
when 'length'
|
548
|
+
record = create_new_length(field_values)
|
499
549
|
when 'record'
|
500
|
-
|
550
|
+
record = Record.new(self, field_values)
|
551
|
+
@cur_lap_records << record
|
552
|
+
@cur_length_records << record
|
501
553
|
@records << record
|
502
554
|
when 'hrv'
|
503
555
|
@hrv << (record = HRV.new(field_values))
|
@@ -512,19 +564,54 @@ module Fit4Ruby
|
|
512
564
|
record
|
513
565
|
end
|
514
566
|
|
567
|
+
def export
|
568
|
+
# Collect all records in a consistent order.
|
569
|
+
records = []
|
570
|
+
FILE_SECTIONS.each do |fs|
|
571
|
+
ivar_name = '@' + fs.to_s
|
572
|
+
ivar = instance_variable_get(ivar_name)
|
573
|
+
|
574
|
+
next unless ivar
|
575
|
+
|
576
|
+
if ivar.respond_to?(:sort) and ivar.respond_to?(:empty?)
|
577
|
+
records += ivar.sort unless ivar.empty?
|
578
|
+
else
|
579
|
+
records << ivar if ivar
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
records.map do |record|
|
584
|
+
record.export
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
515
588
|
private
|
516
589
|
|
517
590
|
def create_new_lap(field_values)
|
518
|
-
lap = Lap.new(@cur_lap_records, @laps.last,
|
591
|
+
lap = Lap.new(self, @cur_lap_records, @laps.last,
|
592
|
+
field_values,
|
593
|
+
@length_counter, @cur_lap_lengths)
|
519
594
|
lap.message_index = @lap_counter - 1
|
520
595
|
@lap_counter += 1
|
521
596
|
@cur_session_laps << lap
|
522
597
|
@laps << lap
|
523
598
|
@cur_lap_records = []
|
599
|
+
@cur_lap_lengths = []
|
524
600
|
|
525
601
|
lap
|
526
602
|
end
|
527
603
|
|
604
|
+
def create_new_length(field_values)
|
605
|
+
length = Length.new(@cur_length_records, @lengths.last, field_values)
|
606
|
+
length.message_index = @length_counter - 1
|
607
|
+
@length_counter += 1
|
608
|
+
@cur_lap_lengths << length
|
609
|
+
@lengths << length
|
610
|
+
@cur_length_records = []
|
611
|
+
|
612
|
+
length
|
613
|
+
end
|
614
|
+
|
528
615
|
end
|
529
616
|
|
530
617
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitDataRecord.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2020 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
|
+
module Fit4Ruby
|
14
|
+
|
15
|
+
# Some FIT message field names conflict with BinData reserved names. We use
|
16
|
+
# this translation method to map the conflicting names to BinData compatible
|
17
|
+
# names.
|
18
|
+
module BDFieldNameTranslator
|
19
|
+
|
20
|
+
BD_DICT = {
|
21
|
+
'array' => '_array',
|
22
|
+
'type' => '_type'
|
23
|
+
}
|
24
|
+
|
25
|
+
def to_bd_field_name(name)
|
26
|
+
if (bd_name = BD_DICT[name])
|
27
|
+
return bd_name
|
28
|
+
end
|
29
|
+
|
30
|
+
name
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
data/lib/fit4ruby/DeviceInfo.rb
CHANGED
@@ -32,6 +32,43 @@ module Fit4Ruby
|
|
32
32
|
@timestamp <=> fdr.timestamp
|
33
33
|
end
|
34
34
|
|
35
|
+
def numeric_manufacturer
|
36
|
+
if @manufacturer && @manufacturer.is_a?(String)
|
37
|
+
if @manufacturer[0..17] == 'Undocumented value'
|
38
|
+
return @manufacturer[18..-1].to_i
|
39
|
+
else
|
40
|
+
return GlobalFitDictionaries['manufacturer'].
|
41
|
+
value_by_name(@manufacturer)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Log.fatal "Unexpected @manufacturer (#{@manufacturer}) value"
|
46
|
+
end
|
47
|
+
|
48
|
+
def numeric_product
|
49
|
+
# The numeric product ID must be an integer or nil. In case the
|
50
|
+
# dictionary did not contain an entry for the numeric ID in the fit file
|
51
|
+
# the @garmin_product or @product variables contain a String starting
|
52
|
+
# with 'Undocumented value ' followed by the ID.
|
53
|
+
if @garmin_product && @garmin_product.is_a?(String)
|
54
|
+
if @garmin_product[0..17] == 'Undocumented value'
|
55
|
+
return @garmin_product[18..-1].to_i
|
56
|
+
else
|
57
|
+
return GlobalFitDictionaries['garmin_product'].
|
58
|
+
value_by_name(@garmin_product)
|
59
|
+
end
|
60
|
+
elsif @product && @product.is_a?(String)
|
61
|
+
if @product[0..17] == 'Undocumented value'
|
62
|
+
return @product[18..-1].to_i
|
63
|
+
else
|
64
|
+
return GlobalFitDictionaries['product'].value_by_name(@product)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Log.fatal "Unexpected @product (#{@product}) or " +
|
69
|
+
"@garmin_product (#{@garmin_product}) values"
|
70
|
+
end
|
71
|
+
|
35
72
|
def check(index)
|
36
73
|
unless @device_index
|
37
74
|
Log.fatal 'device info record must have a device_index'
|