fit4ruby 3.7.0 → 3.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/fit4ruby.gemspec +2 -2
- data/lib/fit4ruby/Activity.rb +118 -55
- data/lib/fit4ruby/BDFieldNameTranslator.rb +36 -0
- data/lib/fit4ruby/FDR_DevField_Extension.rb +81 -0
- data/lib/fit4ruby/FieldDescription.rb +22 -10
- data/lib/fit4ruby/FileId.rb +2 -1
- data/lib/fit4ruby/FitDataRecord.rb +84 -31
- data/lib/fit4ruby/FitDefinition.rb +11 -4
- data/lib/fit4ruby/FitDefinitionField.rb +4 -4
- 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 +23 -18
- data/lib/fit4ruby/FitRecord.rb +2 -1
- data/lib/fit4ruby/FitTypeDefs.rb +2 -2
- data/lib/fit4ruby/GlobalFitDictionaries.rb +197 -2
- data/lib/fit4ruby/GlobalFitMessage.rb +79 -14
- data/lib/fit4ruby/GlobalFitMessages.rb +52 -6
- data/lib/fit4ruby/Lap.rb +10 -1
- data/lib/fit4ruby/Record.rb +11 -2
- data/lib/fit4ruby/Workout.rb +31 -0
- data/lib/fit4ruby/WorkoutStep.rb +32 -0
- data/lib/fit4ruby/version.rb +1 -1
- data/spec/FitFile_spec.rb +198 -100
- metadata +16 -13
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitDataRecord.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, 2021 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,19 +12,21 @@
|
|
12
12
|
|
13
13
|
require 'fit4ruby/FitMessageIdMapper'
|
14
14
|
require 'fit4ruby/GlobalFitMessages.rb'
|
15
|
+
require 'fit4ruby/BDFieldNameTranslator'
|
15
16
|
|
16
17
|
module Fit4Ruby
|
17
18
|
|
18
19
|
class FitDataRecord
|
19
20
|
|
20
21
|
include Converters
|
22
|
+
include BDFieldNameTranslator
|
21
23
|
|
22
|
-
RecordOrder = [ 'user_data', 'user_profile',
|
24
|
+
RecordOrder = [ 'user_data', 'user_profile', 'workout', 'workout_set',
|
23
25
|
'device_info', 'data_sources', 'event',
|
24
26
|
'record', 'lap', 'length', 'session', 'heart_rate_zones',
|
25
27
|
'personal_records' ]
|
26
28
|
|
27
|
-
attr_reader :message
|
29
|
+
attr_reader :message, :timestamp
|
28
30
|
|
29
31
|
def initialize(record_id)
|
30
32
|
@message = GlobalFitMessages.find_by_name(record_id)
|
@@ -34,6 +36,7 @@ module Fit4Ruby
|
|
34
36
|
@message.fields_by_name.each do |name, field|
|
35
37
|
create_instance_variable(name)
|
36
38
|
end
|
39
|
+
|
37
40
|
# Meta fields are additional fields that are not part of the FIT
|
38
41
|
# specification but are convenient to have. These are typcially
|
39
42
|
# aggregated or converted values of regular fields.
|
@@ -58,6 +61,7 @@ module Fit4Ruby
|
|
58
61
|
end
|
59
62
|
|
60
63
|
def get(name)
|
64
|
+
# This is a request for a native FIT field.
|
61
65
|
ivar_name = '@' + name
|
62
66
|
return nil unless instance_variable_defined?(ivar_name)
|
63
67
|
|
@@ -73,8 +77,7 @@ module Fit4Ruby
|
|
73
77
|
if @meta_field_units.include?(name)
|
74
78
|
unit = @meta_field_units[name]
|
75
79
|
else
|
76
|
-
|
77
|
-
unless (unit = field.opts[:unit])
|
80
|
+
unless (unit = get_unit_by_name(name))
|
78
81
|
Log.fatal "Field #{name} has no unit"
|
79
82
|
end
|
80
83
|
end
|
@@ -83,12 +86,13 @@ module Fit4Ruby
|
|
83
86
|
end
|
84
87
|
|
85
88
|
def ==(fdr)
|
86
|
-
@message.
|
87
|
-
ivar_name = '@' + name
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
@message.each_field(field_values_as_hash) do |number, field|
|
90
|
+
ivar_name = '@' + field.name
|
91
|
+
# Comparison of values is done in the fit file format as the accuracy
|
92
|
+
# of native formats is better and could lead to wrong results if a
|
93
|
+
# value hasn't been read back from a fit file yet.
|
94
|
+
v1 = field.native_to_fit(instance_variable_get(ivar_name))
|
95
|
+
v2 = field.native_to_fit(fdr.instance_variable_get(ivar_name))
|
92
96
|
|
93
97
|
return false unless v1 == v2
|
94
98
|
end
|
@@ -104,13 +108,8 @@ module Fit4Ruby
|
|
104
108
|
end
|
105
109
|
|
106
110
|
def write(io, id_mapper)
|
107
|
-
|
108
|
-
|
109
|
-
fields = {}
|
110
|
-
@message.fields_by_name.each_key do |name|
|
111
|
-
fields[name] = instance_variable_get('@' + name)
|
112
|
-
end
|
113
|
-
global_fit_message = @message.construct(fields)
|
111
|
+
global_fit_message = @message.construct(
|
112
|
+
field_values = field_values_as_hash)
|
114
113
|
|
115
114
|
# Map the global message number to the current local message number.
|
116
115
|
unless (local_message_number = id_mapper.get_local(global_fit_message))
|
@@ -131,42 +130,96 @@ module Fit4Ruby
|
|
131
130
|
|
132
131
|
# Create a BinData::Struct object to store the data record.
|
133
132
|
fields = []
|
134
|
-
|
135
|
-
bin_data_type = FitDefinitionFieldBase.fit_type_to_bin_data(field.type)
|
136
|
-
fields << [ bin_data_type, field.name ]
|
137
|
-
end
|
138
|
-
bd = BinData::Struct.new(:endian => :little, :fields => fields)
|
133
|
+
values = {}
|
139
134
|
|
140
135
|
# Fill the BinData::Struct object with the values from the corresponding
|
141
136
|
# instance variables.
|
142
|
-
global_fit_message.
|
137
|
+
global_fit_message.each_field(field_values) do |number, field|
|
138
|
+
bin_data_type = FitDefinitionFieldBase.fit_type_to_bin_data(field.type)
|
139
|
+
field_name = to_bd_field_name(field.name)
|
140
|
+
field_def = [ bin_data_type, field_name ]
|
141
|
+
|
143
142
|
iv = "@#{field.name}"
|
144
143
|
if instance_variable_defined?(iv) &&
|
145
144
|
!(iv_value = instance_variable_get(iv)).nil?
|
146
|
-
|
145
|
+
values[field.name] = field.native_to_fit(iv_value)
|
147
146
|
else
|
148
147
|
# If we don't have a corresponding variable or the variable is nil
|
149
148
|
# we write the 'undefined' value instead.
|
150
149
|
value = FitDefinitionFieldBase.undefined_value(field.type)
|
150
|
+
values[field.name] = field.opts[:array] ? [ value ] :
|
151
|
+
field.type == 'string' ? '' : value
|
151
152
|
end
|
152
|
-
|
153
|
+
|
154
|
+
# Some field types need special handling.
|
155
|
+
if field.type == 'string'
|
156
|
+
# Zero terminate the string.
|
157
|
+
values[field.name] += "\0"
|
158
|
+
elsif field.opts[:array]
|
159
|
+
# For Arrays we use a BinData::Array to write them.
|
160
|
+
field_def = [ :array, field_name,
|
161
|
+
{ :type => bin_data_type,
|
162
|
+
:initial_length => values[field.name].size } ]
|
163
|
+
end
|
164
|
+
fields << field_def
|
165
|
+
end
|
166
|
+
bd = BinData::Struct.new(:endian => :little, :fields => fields)
|
167
|
+
|
168
|
+
# Fill the BinData::Struct object with the values from the corresponding
|
169
|
+
# instance variables.
|
170
|
+
global_fit_message.each_field(field_values) do |number, field|
|
171
|
+
bd[to_bd_field_name(field.name)] = values[field.name]
|
153
172
|
end
|
154
173
|
|
155
174
|
# Write the data record to the file.
|
156
175
|
bd.write(io)
|
157
176
|
end
|
158
177
|
|
159
|
-
def
|
160
|
-
|
161
|
-
|
178
|
+
def export
|
179
|
+
message = {
|
180
|
+
'message' => @message.name,
|
181
|
+
'number' => @message.number,
|
182
|
+
'fields' => {}
|
183
|
+
}
|
184
|
+
|
185
|
+
@message.each_field(field_values_as_hash) do |number, field|
|
162
186
|
ivar_name = '@' + field.name
|
163
|
-
|
187
|
+
fit_value = field.native_to_fit(instance_variable_get(ivar_name))
|
188
|
+
unless field.is_undefined?(fit_value)
|
189
|
+
fld = {
|
190
|
+
'number' => number,
|
191
|
+
'value' => field.fit_to_native(fit_value),
|
192
|
+
#'human' => field.to_human(fit_value),
|
193
|
+
'type' => field.type
|
194
|
+
}
|
195
|
+
fld['unit'] = field.opts[:unit] if field.opts[:unit]
|
196
|
+
fld['scale'] = field.opts[:scale] if field.opts[:scale]
|
197
|
+
|
198
|
+
message['fields'][field.name] = fld
|
199
|
+
end
|
164
200
|
end
|
165
|
-
|
201
|
+
|
202
|
+
message
|
203
|
+
end
|
204
|
+
|
205
|
+
def get_unit_by_name(name)
|
206
|
+
field = @message.fields_by_name[name]
|
207
|
+
field.opts[:unit]
|
166
208
|
end
|
167
209
|
|
168
210
|
private
|
169
211
|
|
212
|
+
def field_values_as_hash
|
213
|
+
# Construct a GlobalFitMessage object that matches exactly the provided
|
214
|
+
# set of fields. It does not contain any AltField objects.
|
215
|
+
field_values = {}
|
216
|
+
@message.fields_by_name.each_key do |name|
|
217
|
+
field_values[name] = instance_variable_get('@' + name)
|
218
|
+
end
|
219
|
+
|
220
|
+
field_values
|
221
|
+
end
|
222
|
+
|
170
223
|
def create_instance_variable(name)
|
171
224
|
# Create a new instance variable for 'name'. We initialize it with a
|
172
225
|
# provided default value or nil.
|
@@ -63,9 +63,7 @@ module Fit4Ruby
|
|
63
63
|
super(io)
|
64
64
|
end
|
65
65
|
|
66
|
-
def
|
67
|
-
# We don't support writing developer data fields yet.
|
68
|
-
@@has_developer_data = false
|
66
|
+
def write(io)
|
69
67
|
super(io)
|
70
68
|
end
|
71
69
|
|
@@ -77,11 +75,20 @@ module Fit4Ruby
|
|
77
75
|
@@fit_entity
|
78
76
|
end
|
79
77
|
|
80
|
-
def setup(fit_message_definition)
|
78
|
+
def setup(fit_message_definition, field_values_by_name)
|
81
79
|
fit_message_definition.fields_by_number.each do |number, f|
|
82
80
|
fdf = FitDefinitionField.new
|
83
81
|
fdf.field_definition_number = number
|
84
82
|
fdf.set_type(f.type)
|
83
|
+
value = field_values_by_name[f.name]
|
84
|
+
# For String and Array fields we must adjust the length in the
|
85
|
+
# definition message.
|
86
|
+
if value.is_a?(String) && f.is_string?
|
87
|
+
# String plus 0 byte
|
88
|
+
fdf.set_length(value.bytes.length + 1)
|
89
|
+
elsif value.is_a?(Array) && f.is_array?
|
90
|
+
fdf.set_length(value.length)
|
91
|
+
end
|
85
92
|
|
86
93
|
data_fields << fdf
|
87
94
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitDefinitionField.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2017, 2018 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2017, 2018, 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
|
@@ -72,15 +72,15 @@ module Fit4Ruby
|
|
72
72
|
if value.kind_of?(Array)
|
73
73
|
ary = []
|
74
74
|
value.each { |v| ary << to_machine(v) }
|
75
|
-
ary
|
75
|
+
return ary
|
76
76
|
else
|
77
77
|
if @global_message_definition &&
|
78
78
|
(field = @global_message_definition.
|
79
79
|
fields_by_number[field_number]) &&
|
80
80
|
field.respond_to?('to_machine')
|
81
|
-
field.to_machine(value)
|
81
|
+
return field.to_machine(value)
|
82
82
|
else
|
83
|
-
value
|
83
|
+
return value
|
84
84
|
end
|
85
85
|
end
|
86
86
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitDefinitionFieldBase.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2017 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2014, 2017, 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
|
@@ -30,7 +30,7 @@ module Fit4Ruby
|
|
30
30
|
|
31
31
|
def set_type(fit_type)
|
32
32
|
idx = FIT_TYPE_DEFS.index { |x| x[0] == fit_type }
|
33
|
-
raise "Unknown type #{fit_type}" unless idx
|
33
|
+
raise ArgumentError, "Unknown type #{fit_type}" unless idx
|
34
34
|
self.base_type_number = idx
|
35
35
|
self.byte_count = FIT_TYPE_DEFS[idx][3]
|
36
36
|
end
|
@@ -39,13 +39,22 @@ module Fit4Ruby
|
|
39
39
|
FIT_TYPE_DEFS[checked_base_type_number][fit_type ? 0 : 1]
|
40
40
|
end
|
41
41
|
|
42
|
+
def set_length(count)
|
43
|
+
if (byte_count = FIT_TYPE_DEFS[self.base_type_number][3] * count) > 255
|
44
|
+
raise ArgumentError,
|
45
|
+
"FitDefinitionField byte count too large (#{byte_count})"
|
46
|
+
end
|
47
|
+
self.byte_count = byte_count
|
48
|
+
end
|
49
|
+
|
42
50
|
def is_array?
|
43
51
|
if total_bytes > base_type_bytes
|
44
52
|
if total_bytes % base_type_bytes != 0
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
53
|
+
raise RuntimeError,
|
54
|
+
"Total bytes (#{total_bytes}) must be multiple of " +
|
55
|
+
"base type bytes (#{base_type_bytes}) of type " +
|
56
|
+
"#{base_type_number.snapshot} in Global FIT " +
|
57
|
+
"Message #{name}."
|
49
58
|
end
|
50
59
|
return true
|
51
60
|
end
|
@@ -48,7 +48,7 @@ module Fit4Ruby
|
|
48
48
|
def find_field_definition
|
49
49
|
return @field_definition if @field_definition
|
50
50
|
|
51
|
-
tlr = parent.parent.fit_entity
|
51
|
+
tlr = parent.parent.fit_entity
|
52
52
|
@field_definition = tlr.field_descriptions.find do |fd|
|
53
53
|
fd.field_definition_number == field_number.snapshot &&
|
54
54
|
fd.developer_data_index == developer_data_index.snapshot
|
data/lib/fit4ruby/FitFile.rb
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
#
|
4
4
|
# = FitMessageRecord.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
5
|
#
|
6
|
-
# Copyright (c) 2014, 2015, 2018, 2019
|
6
|
+
# Copyright (c) 2014, 2015, 2018, 2019, 2020
|
7
|
+
# by Chris Schlaeger <cs@taskjuggler.org>
|
7
8
|
#
|
8
9
|
# This program is free software; you can redistribute it and/or modify
|
9
10
|
# it under the terms of version 2 of the GNU General Public License as
|
@@ -15,6 +16,7 @@ require 'fit4ruby/Log'
|
|
15
16
|
require 'fit4ruby/GlobalFitMessage'
|
16
17
|
require 'fit4ruby/FitFileEntity'
|
17
18
|
require 'fit4ruby/DumpedField'
|
19
|
+
require 'fit4ruby/BDFieldNameTranslator'
|
18
20
|
|
19
21
|
module Fit4Ruby
|
20
22
|
|
@@ -26,6 +28,8 @@ module Fit4Ruby
|
|
26
28
|
# decendents are used.
|
27
29
|
class FitMessageRecord
|
28
30
|
|
31
|
+
include BDFieldNameTranslator
|
32
|
+
|
29
33
|
attr_reader :global_message_number, :name, :message_record
|
30
34
|
|
31
35
|
def initialize(definition)
|
@@ -44,15 +48,9 @@ module Fit4Ruby
|
|
44
48
|
def read(io, entity, filter = nil, fields_dump = nil, fit_entity)
|
45
49
|
@message_record.read(io)
|
46
50
|
|
47
|
-
# Check if we have a developer defined message for this global message
|
48
|
-
# number.
|
49
|
-
if fit_entity.top_level_record
|
50
|
-
developer_fields = fit_entity.top_level_record.field_descriptions
|
51
|
-
@dfm = developer_fields[@global_message_number]
|
52
|
-
end
|
53
|
-
|
54
51
|
if @name == 'file_id'
|
55
|
-
|
52
|
+
# Caveat: 'type' is used as '_type' in BinData fields!
|
53
|
+
unless (entity_type = @message_record['_type'].snapshot)
|
56
54
|
Log.fatal "Corrupted FIT file: file_id record has no type definition"
|
57
55
|
end
|
58
56
|
entity.set_type(entity_type)
|
@@ -70,14 +68,18 @@ module Fit4Ruby
|
|
70
68
|
f1alt ? 1 : -1
|
71
69
|
end
|
72
70
|
|
73
|
-
#(sorted_fields + @definition.developer_fields).each do |field|
|
74
71
|
sorted_fields.each do |field|
|
75
|
-
value = @message_record[field.name].snapshot
|
72
|
+
value = @message_record[to_bd_field_name(field.name)].snapshot
|
76
73
|
# Strings are null byte terminated. There may be more bytes in the
|
77
74
|
# file, but we have to discard all bytes from the first null byte
|
78
75
|
# onwards.
|
79
|
-
if value.is_a?(String)
|
80
|
-
|
76
|
+
if value.is_a?(String)
|
77
|
+
if (null_byte = value.index("\0"))
|
78
|
+
value = null_byte == 0 ? '' : value[0..(null_byte - 1)]
|
79
|
+
end
|
80
|
+
if value.empty?
|
81
|
+
value = nil
|
82
|
+
end
|
81
83
|
end
|
82
84
|
|
83
85
|
field_name, field_def = get_field_name_and_global_def(field, obj)
|
@@ -86,8 +88,8 @@ module Fit4Ruby
|
|
86
88
|
if filter && fields_dump &&
|
87
89
|
(filter.field_names.nil? ||
|
88
90
|
filter.field_names.include?(field_name)) &&
|
89
|
-
!(((value.
|
90
|
-
(value.count(field.undefined_value) == value.length)) ||
|
91
|
+
!(((value.is_a?(String) &&
|
92
|
+
(value.count(field.undefined_value.chr) == value.length)) ||
|
91
93
|
value == field.undefined_value) && filter.ignore_undef)
|
92
94
|
fields_dump << DumpedField.new(
|
93
95
|
@global_message_number,
|
@@ -109,7 +111,7 @@ module Fit4Ruby
|
|
109
111
|
next
|
110
112
|
end
|
111
113
|
|
112
|
-
field_name = field_description.
|
114
|
+
field_name = field_description.full_field_name(fit_entity)
|
113
115
|
units = field_description.units
|
114
116
|
type = field.type
|
115
117
|
native_message_number = field_description.native_mesg_num
|
@@ -118,6 +120,8 @@ module Fit4Ruby
|
|
118
120
|
value = @message_record[field.name].snapshot
|
119
121
|
value = nil if value == field.undefined_value
|
120
122
|
|
123
|
+
obj.set(field_name, value) if obj
|
124
|
+
|
121
125
|
if filter && fields_dump &&
|
122
126
|
(filter.field_names.nil? ||
|
123
127
|
filter.field_names.include?(field_name)) &&
|
@@ -178,7 +182,8 @@ module Fit4Ruby
|
|
178
182
|
fields = []
|
179
183
|
(definition.data_fields.to_a +
|
180
184
|
definition.developer_fields.to_a).each do |field|
|
181
|
-
|
185
|
+
field_name = to_bd_field_name(field.name)
|
186
|
+
field_def = [ field.type, field_name ]
|
182
187
|
|
183
188
|
# Some field types need special handling.
|
184
189
|
if field.type == 'string'
|
@@ -186,7 +191,7 @@ module Fit4Ruby
|
|
186
191
|
field_def << { :read_length => field.total_bytes }
|
187
192
|
elsif field.is_array?
|
188
193
|
# For Arrays we have to break them into separte fields.
|
189
|
-
field_def = [ :array,
|
194
|
+
field_def = [ :array, field_name,
|
190
195
|
{ :type => field.type.intern,
|
191
196
|
:initial_length =>
|
192
197
|
field.total_bytes / field.base_type_bytes } ]
|
data/lib/fit4ruby/FitRecord.rb
CHANGED
@@ -49,7 +49,8 @@ module Fit4Ruby
|
|
49
49
|
if header.normal? && header.message_type.snapshot == 1
|
50
50
|
# process definition message
|
51
51
|
definition = FitDefinition.read(
|
52
|
-
io, entity, header.developer_data_flag.snapshot,
|
52
|
+
io, entity, header.developer_data_flag.snapshot,
|
53
|
+
@fit_entity.top_level_record)
|
53
54
|
@definitions[local_message_type] = FitMessageRecord.new(definition)
|
54
55
|
else
|
55
56
|
# process data message
|
data/lib/fit4ruby/FitTypeDefs.rb
CHANGED
@@ -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',
|
24
|
+
[ 'string', 'string', 0, 1 ],
|
25
25
|
[ 'float32', 'float', 0xFFFFFFFF, 4 ],
|
26
|
-
[ '
|
26
|
+
[ 'float64', 'double', 0xFFFFFFFFFFFFFFFF, 8 ],
|
27
27
|
[ 'uint8z', 'uint8', 0, 1 ],
|
28
28
|
[ 'uint16z', 'uint16', 0, 2 ],
|
29
29
|
[ 'uint32z', 'uint32', 0, 4 ],
|