fit4ruby 3.7.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- field = @message.fields_by_name[name]
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.fields_by_name.each do |name, field|
87
- ivar_name = '@' + name
88
- v1 = field.fit_to_native(field.native_to_fit(
89
- instance_variable_get(ivar_name)))
90
- v2 = field.fit_to_native(field.native_to_fit(
91
- fdr.instance_variable_get(ivar_name)))
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
- # Construct a GlobalFitMessage object that matches exactly the provided
108
- # set of fields. It does not contain any AltField objects.
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
- global_fit_message.fields_by_number.each do |field_number, field|
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.fields_by_number.each do |field_number, field|
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
- value = field.native_to_fit(iv_value)
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
- bd[field.name] = value
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 inspect
160
- fields = {}
161
- @message.fields_by_name.each do |name, field|
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
- fields[field.name] = instance_variable_get(ivar_name)
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
- fields.inspect
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 FitDefinitionField::write(io)
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
- Log.error "Total bytes (#{total_bytes}) must be multiple of " +
46
- "base type bytes (#{base_type_bytes}) of type " +
47
- "#{base_type_number.snapshot} in Global FIT " +
48
- "Message #{name}."
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.top_level_record
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
@@ -81,6 +81,8 @@ module Fit4Ruby
81
81
  io.close
82
82
  end
83
83
 
84
+ return nil if entities.empty?
85
+
84
86
  entities[0].top_level_record
85
87
  end
86
88
 
@@ -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 by Chris Schlaeger <cs@taskjuggler.org>
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
- unless (entity_type = @message_record['type'].snapshot)
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) && (null_byte = value.index("\0"))
80
- value = null_byte == 0 ? '' : value[0..(null_byte - 1)]
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.respond_to?('count') &&
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.field_name
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
- field_def = [ field.type, field.name ]
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, field.name,
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 } ]
@@ -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, @fit_entity)
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
@@ -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 ],