fit4ruby 3.3.0 → 3.8.0

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