fit4ruby 3.7.0 → 3.10.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.
@@ -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 ],