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