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
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FDR_DevField_Extension.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
|
+
# This module extends FitDataRecord derived classes in case they have
|
16
|
+
# developer fields.
|
17
|
+
module FDR_DevField_Extension
|
18
|
+
|
19
|
+
def create_dev_field_instance_variables
|
20
|
+
# Create instance variables for developer fields
|
21
|
+
@dev_field_descriptions = {}
|
22
|
+
@top_level_record.field_descriptions.each do |field_description|
|
23
|
+
# Only create instance variables if the FieldDescription is for this
|
24
|
+
# message number.
|
25
|
+
if field_description.native_mesg_num == @message.number
|
26
|
+
name = field_description.full_field_name(@top_level_record.
|
27
|
+
developer_data_ids)
|
28
|
+
create_instance_variable(name)
|
29
|
+
@dev_field_descriptions[name] = field_description
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def each_developer_field
|
35
|
+
@top_level_record.field_descriptions.each do |field_description|
|
36
|
+
# Only create instance variables if the FieldDescription is for this
|
37
|
+
# message number.
|
38
|
+
if field_description.native_mesg_num == @message.number
|
39
|
+
name = field_description.full_field_name(@top_level_record.
|
40
|
+
developer_data_ids)
|
41
|
+
yield(field_description, instance_variable_get('@' + name))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_unit_by_name(name)
|
47
|
+
if /[A-Za-z_]+_[A-F0-9]{16}/ =~ name
|
48
|
+
@dev_field_descriptions[name].units
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def export
|
55
|
+
message = super
|
56
|
+
|
57
|
+
each_developer_field do |field_description, ivar|
|
58
|
+
field = field_description.message.fields_by_number[
|
59
|
+
field_description.native_field_num]
|
60
|
+
fit_value = field.native_to_fit(ivar)
|
61
|
+
unless field.is_undefined?(fit_value)
|
62
|
+
fld = {
|
63
|
+
'number' => field_description.native_field_num | 128,
|
64
|
+
'value' => field.fit_to_native(fit_value),
|
65
|
+
#'human' => field.to_human(fit_value),
|
66
|
+
'type' => field.type
|
67
|
+
}
|
68
|
+
fld['unit'] = field.opts[:unit] if field.opts[:unit]
|
69
|
+
fld['scale'] = field.opts[:scale] if field.opts[:scale]
|
70
|
+
|
71
|
+
message['fields'][field.name] = fld
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
message
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FieldDescription.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
5
|
#
|
6
|
-
# Copyright (c) 2017, 2018, 2019 by Chris Schlaeger <cs@taskjuggler.org>
|
6
|
+
# Copyright (c) 2017, 2018, 2019, 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
|
@@ -25,6 +25,24 @@ module Fit4Ruby
|
|
25
25
|
def initialize(field_values = {})
|
26
26
|
super('field_description')
|
27
27
|
set_field_values(field_values)
|
28
|
+
|
29
|
+
@full_field_name = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def full_field_name(developer_data_ids)
|
33
|
+
return @full_field_name if @full_field_name
|
34
|
+
|
35
|
+
if @developer_data_index >=
|
36
|
+
developer_data_ids.size
|
37
|
+
Log.error "Developer data index #{@developer_data_index} is too large"
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
app_id = developer_data_ids[@developer_data_index].application_id
|
42
|
+
# Convert the byte array with the app ID into a 16 character hex string.
|
43
|
+
app_id_str = app_id.map { |i| '%02X' % i }.join('')
|
44
|
+
@full_field_name =
|
45
|
+
"#{@field_name.gsub(/[^A-Za-z0-9_]/, '_')}_#{app_id_str}"
|
28
46
|
end
|
29
47
|
|
30
48
|
def create_global_definition(fit_entity)
|
@@ -35,18 +53,18 @@ module Fit4Ruby
|
|
35
53
|
return
|
36
54
|
end
|
37
55
|
|
38
|
-
if @developer_data_index >=
|
39
|
-
fit_entity.top_level_record.developer_data_ids.size
|
40
|
-
Log.error "Developer data index #{@developer_data_index} is too large"
|
41
|
-
return
|
42
|
-
end
|
43
|
-
|
44
56
|
msg = messages[@native_mesg_num] ||
|
45
57
|
messages.message(@native_mesg_num, gfm.name)
|
46
58
|
unless (@fit_base_type_id & 0x7F) < FIT_TYPE_DEFS.size
|
47
59
|
Log.error "fit_base_type_id #{@fit_base_type_id} is too large"
|
48
60
|
return
|
49
61
|
end
|
62
|
+
|
63
|
+
# A fit file may include multiple definitions of the same field. We
|
64
|
+
# ignore all subsequent definitions.
|
65
|
+
return if msg.has_field?(full_field_name(fit_entity.top_level_record.
|
66
|
+
developer_data_ids))
|
67
|
+
|
50
68
|
options = {}
|
51
69
|
options[:scale] = @scale if @scale
|
52
70
|
options[:offset] = @offset if @offset
|
@@ -54,7 +72,7 @@ module Fit4Ruby
|
|
54
72
|
options[:unit] = @units
|
55
73
|
msg.field(@field_definition_number,
|
56
74
|
FIT_TYPE_DEFS[@fit_base_type_id & 0x7F][1],
|
57
|
-
|
75
|
+
@full_field_name, options)
|
58
76
|
end
|
59
77
|
|
60
78
|
end
|
data/lib/fit4ruby/FileId.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FileId.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
|
@@ -23,6 +23,7 @@ module Fit4Ruby
|
|
23
23
|
@time_created = Time.at(Time.now.to_i)
|
24
24
|
@manufacturer = 'development'
|
25
25
|
@type = 'activity'
|
26
|
+
@product = 0
|
26
27
|
|
27
28
|
set_field_values(field_values)
|
28
29
|
end
|
@@ -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 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
24
|
RecordOrder = [ 'user_data', 'user_profile',
|
23
25
|
'device_info', 'data_sources', 'event',
|
24
|
-
'record', 'lap', 'session', 'heart_rate_zones',
|
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.
|
@@ -50,14 +53,15 @@ module Fit4Ruby
|
|
50
53
|
def set(name, value)
|
51
54
|
ivar_name = '@' + name
|
52
55
|
unless instance_variable_defined?(ivar_name)
|
53
|
-
Log.
|
54
|
-
|
56
|
+
Log.debug("Unknown FIT record field '#{name}' in global message " +
|
57
|
+
"#{@message.name} (#{@message.number}).")
|
55
58
|
return
|
56
59
|
end
|
57
60
|
instance_variable_set(ivar_name, value)
|
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
|
@@ -54,8 +54,8 @@ module Fit4Ruby
|
|
54
54
|
else
|
55
55
|
@name = "field#{field_definition_number.snapshot}"
|
56
56
|
@type = nil
|
57
|
-
Log.
|
58
|
-
|
57
|
+
Log.debug { "Unknown field number #{field_definition_number} " +
|
58
|
+
"in global message #{@global_message_number}" }
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
@@ -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
|