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.
@@ -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
- "_#{@developer_data_index}_#{@field_name}", options)
75
+ @full_field_name, options)
58
76
  end
59
77
 
60
78
  end
@@ -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.warn("Unknown FIT record field '#{name}' in global message " +
54
- "#{@message.name} (#{@message.number}).")
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
- 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
@@ -54,8 +54,8 @@ module Fit4Ruby
54
54
  else
55
55
  @name = "field#{field_definition_number.snapshot}"
56
56
  @type = nil
57
- Log.warn { "Unknown field number #{field_definition_number} " +
58
- "in global message #{@global_message_number}" }
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