fit4ruby 0.0.4 → 0.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6e5c32929d16d07a0fe95c04cd7699e56a8067a4
4
- data.tar.gz: 2315ae4059cf64dc4cd7d0eeb02c6c42ec5cde26
3
+ metadata.gz: 2b2445f82a3a5c423df2e01cf21fdddd2c77322e
4
+ data.tar.gz: 5e51fe986b7b3fa1435a5b6f0dd8b475d6091c47
5
5
  SHA512:
6
- metadata.gz: 66f5efa6875e7a10f79d304730a767c6bf0d330c3c4e0ce6adf297ff3ea8e440281c548f1064f0c42bf27891a001d70caf3a06980b8dfd7a15bc1536f36a78e6
7
- data.tar.gz: 4cb6346d5f3bfee72c1f23f3d7c567612731824842896cf64302549f39e1b7285aca67a3e8298826befb3a7c54af66dd1309f3298cea2c184cbc3af1d9afe9e4
6
+ metadata.gz: f11797d8a21e49791b04b5299d76e5ae7a14f110f38a7841aaffd5fc0b4e5d3471143c75b1b4f8d8f7e03051759e97bf96b8b370bedcfecee168f7240bad2cd0
7
+ data.tar.gz: 695da88503d5a491f5a4b50d6ed0334f18cf53fbdeaeba0a6a6c7f4ed1de501dea491745ffb8e86415fd858691f2cd02612f1abd778765657dbde47632d69fae
data/lib/fit4ruby.rb CHANGED
@@ -18,8 +18,8 @@ module Fit4Ruby
18
18
  FitFile.new.read(file, filter)
19
19
  end
20
20
 
21
- def self.write(file, activity)
22
- FitFile.new.write(file, activity)
21
+ def self.write(file, top_level_record)
22
+ FitFile.new.write(file, top_level_record)
23
23
  end
24
24
 
25
25
  end
@@ -99,7 +99,7 @@ module Fit4Ruby
99
99
  # @return recovery time in seconds.
100
100
  def recovery_time
101
101
  @events.each do |e|
102
- return e.data if e.event == 'recovery_time'
102
+ return e.recovery_time if e.event == 'recovery_time'
103
103
  end
104
104
 
105
105
  nil
@@ -109,7 +109,7 @@ module Fit4Ruby
109
109
  # based on multiple previous activities.
110
110
  def vo2max
111
111
  @events.each do |e|
112
- return e.data if e.event == 'vo2max'
112
+ return e.vo2max if e.event == 'vo2max'
113
113
  end
114
114
 
115
115
  nil
@@ -120,6 +120,11 @@ module Fit4Ruby
120
120
  @sessions[0].sport
121
121
  end
122
122
 
123
+ # Returns the sport subtype of this activity.
124
+ def sub_sport
125
+ @sessions[0].sub_sport
126
+ end
127
+
123
128
  # Write the Activity data to a file.
124
129
  # @param io [IO] File reference
125
130
  # @param id_mapper [FitMessageIdMapper] Maps global FIT record types to
@@ -24,9 +24,12 @@ module Fit4Ruby
24
24
 
25
25
  # Create instance variables that correspond to every field of the
26
26
  # corresponding FIT data record.
27
- @message.fields.each do |field_number, field|
28
- create_instance_variable(field.name)
27
+ @message.fields_by_name.each do |name, field|
28
+ create_instance_variable(name)
29
29
  end
30
+ # Meta fields are additional fields that are not part of the FIT
31
+ # specification but are convenient to have. These are typcially
32
+ # aggregated or converted values of regular fields.
30
33
  @meta_field_units = {}
31
34
  @timestamp = Time.now
32
35
  end
@@ -40,17 +43,18 @@ module Fit4Ruby
40
43
  def set(name, value)
41
44
  ivar_name = '@' + name
42
45
  unless instance_variable_defined?(ivar_name)
43
- Log.warn("Unknown FIT record field '#{ivar_name}'")
46
+ Log.warn("Unknown FIT record field '#{name}' in global message " +
47
+ "#{@message.name} (#{@message.number}).")
44
48
  return
45
49
  end
46
- instance_variable_set('@' + name, value)
50
+ instance_variable_set(ivar_name, value)
47
51
  end
48
52
 
49
53
  def get(name)
50
54
  ivar_name = '@' + name
51
55
  return nil unless instance_variable_defined?(ivar_name)
52
56
 
53
- instance_variable_get('@' + name)
57
+ instance_variable_get(ivar_name)
54
58
  end
55
59
 
56
60
  def get_as(name, to_unit)
@@ -60,7 +64,7 @@ module Fit4Ruby
60
64
  if @meta_field_units.include?(name)
61
65
  unit = @meta_field_units[name]
62
66
  else
63
- field = @message.find_by_name(name)
67
+ field = @message.fields_by_name[name]
64
68
  unless (unit = field.opts[:unit])
65
69
  Log.fatal "Field #{name} has no unit"
66
70
  end
@@ -70,15 +74,15 @@ module Fit4Ruby
70
74
  end
71
75
 
72
76
  def ==(fdr)
73
- @message.fields.each do |field_number, field|
74
- ivar_name = '@' + field.name
77
+ @message.fields_by_name.each do |name, field|
78
+ ivar_name = '@' + name
75
79
  v1 = field.fit_to_native(field.native_to_fit(
76
80
  instance_variable_get(ivar_name)))
77
81
  v2 = field.fit_to_native(field.native_to_fit(
78
82
  fdr.instance_variable_get(ivar_name)))
79
83
 
80
84
  unless v1 == v2
81
- Log.error "#{field.name}: #{v1} != #{v2}"
85
+ Log.error "#{name}: #{v1} != #{v2}"
82
86
  return false
83
87
  end
84
88
  end
@@ -91,16 +95,22 @@ module Fit4Ruby
91
95
  end
92
96
 
93
97
  def write(io, id_mapper)
94
- global_message_number = @message.number
98
+ # Construct a GlobalFitMessage object that matches exactly the provided
99
+ # set of fields. It does not contain any AltField objects.
100
+ fields = {}
101
+ @message.fields_by_name.each_key do |name|
102
+ fields[name] = instance_variable_get('@' + name)
103
+ end
104
+ global_fit_message = @message.construct(fields)
95
105
 
96
106
  # Map the global message number to the current local message number.
97
- unless (local_message_number = id_mapper.get_local(global_message_number))
107
+ unless (local_message_number = id_mapper.get_local(global_fit_message))
98
108
  # If the current dictionary does not contain the global message
99
109
  # number, we need to create a new entry for it. The index in the
100
110
  # dictionary is the local message number.
101
- local_message_number = id_mapper.add_global(global_message_number)
111
+ local_message_number = id_mapper.add_global(global_fit_message)
102
112
  # Write the definition of the global message number to the file.
103
- @message.write(io, local_message_number)
113
+ global_fit_message.write(io, local_message_number)
104
114
  end
105
115
 
106
116
  # Write data record header.
@@ -112,7 +122,7 @@ module Fit4Ruby
112
122
 
113
123
  # Create a BinData::Struct object to store the data record.
114
124
  fields = []
115
- @message.fields.each do |field_number, field|
125
+ global_fit_message.fields_by_number.each do |field_number, field|
116
126
  bin_data_type = FitDefinitionField.fit_type_to_bin_data(field.type)
117
127
  fields << [ bin_data_type, field.name ]
118
128
  end
@@ -120,7 +130,7 @@ module Fit4Ruby
120
130
 
121
131
  # Fill the BinData::Struct object with the values from the corresponding
122
132
  # instance variables.
123
- @message.fields.each do |field_number, field|
133
+ global_fit_message.fields_by_number.each do |field_number, field|
124
134
  iv = "@#{field.name}"
125
135
  if instance_variable_defined?(iv) &&
126
136
  !(iv_value = instance_variable_get(iv)).nil?
@@ -139,7 +149,7 @@ module Fit4Ruby
139
149
 
140
150
  def inspect
141
151
  fields = {}
142
- @message.fields.each do |field_number, field|
152
+ @message.fields_by_name.each do |name, field|
143
153
  ivar_name = '@' + field.name
144
154
  fields[field.name] = instance_variable_get(ivar_name)
145
155
  end
@@ -15,6 +15,13 @@ require 'fit4ruby/FitDefinitionField'
15
15
 
16
16
  module Fit4Ruby
17
17
 
18
+ # The FitDefinition contains the blueprints for FitMessageRecord segments of
19
+ # FIT files. Before a message record can occur in a FIT file, its definition
20
+ # must be included in the FIT file. The definition holds enough information
21
+ # about the message record to define its size. It also contains some basic
22
+ # information how to interpret the data in the record. To fully understand
23
+ # the message record data the full definition in the GlobalFitMessage is
24
+ # required.
18
25
  class FitDefinition < BinData::Record
19
26
 
20
27
  hide :reserved
@@ -33,11 +40,14 @@ module Fit4Ruby
33
40
  end
34
41
 
35
42
  def check
43
+ if architecture.snapshot > 1
44
+ Log.error "Illegal architecture value #{architecture.snapshot}"
45
+ end
36
46
  fields.each { |f| f.check }
37
47
  end
38
48
 
39
49
  def setup(fit_message_definition)
40
- fit_message_definition.fields.each do |number, f|
50
+ fit_message_definition.fields_by_number.each do |number, f|
41
51
  fdf = FitDefinitionField.new
42
52
  fdf.field_definition_number = number
43
53
  fdf.set_type(f.type)
@@ -17,6 +17,11 @@ require 'fit4ruby/GlobalFitMessage'
17
17
 
18
18
  module Fit4Ruby
19
19
 
20
+ # The FitDefinitionField models the part of the FIT file that contains the
21
+ # template definition for a field of a FitMessageRecord. It should match the
22
+ # corresponding definition in GlobalFitMessages. In case we don't have a
23
+ # known entry in GlobalFitMessages we can still read the file since we know
24
+ # the exact size of the binary records.
20
25
  class FitDefinitionField < BinData::Record
21
26
 
22
27
  @@TypeDefs = [
@@ -28,7 +33,7 @@ module Fit4Ruby
28
33
  [ 'uint16', 'uint16', 0xFFFF, 2 ],
29
34
  [ 'sint32', 'int32', 0x7FFFFFFF, 4 ],
30
35
  [ 'uint32', 'uint32', 0xFFFFFFFF, 4 ],
31
- [ 'string', 'stringz', '', 0 ],
36
+ [ 'string', 'string', '', 0 ],
32
37
  [ 'float32', 'float', 0xFFFFFFFF, 4 ],
33
38
  [ 'float63', 'double', 0xFFFFFFFF, 4 ],
34
39
  [ 'uint8z', 'uint8', 0, 1 ],
@@ -62,9 +67,10 @@ module Fit4Ruby
62
67
  @global_message_definition = GlobalFitMessages[@global_message_number]
63
68
  field_number = field_definition_number.snapshot
64
69
  if @global_message_definition &&
65
- (field = @global_message_definition.fields[field_number])
66
- @name = field.name
67
- @type = field.type
70
+ (field = @global_message_definition.fields_by_number[field_number])
71
+ @name = field.respond_to?('name') ? field.name :
72
+ "choice_#{field_number}"
73
+ @type = field.respond_to?('type') ? field.type : nil
68
74
 
69
75
  if @type && (td = @@TypeDefs[base_type_number]) && td[0] != @type
70
76
  Log.warn "#{@global_message_number}:#{@name} must be of type " +
@@ -88,11 +94,19 @@ module Fit4Ruby
88
94
  value = nil if value == undefined_value
89
95
 
90
96
  field_number = field_definition_number.snapshot
91
- if @global_message_definition &&
92
- (field = @global_message_definition.fields[field_number])
93
- field.to_machine(value)
97
+ if value.kind_of?(Array)
98
+ ary = []
99
+ value.each { |v| ary << to_machine(v) }
100
+ ary
94
101
  else
95
- value
102
+ if @global_message_definition &&
103
+ (field = @global_message_definition.
104
+ fields_by_number[field_number]) &&
105
+ field.respond_to?('to_machine')
106
+ field.to_machine(value)
107
+ else
108
+ value
109
+ end
96
110
  end
97
111
  end
98
112
 
@@ -100,12 +114,18 @@ module Fit4Ruby
100
114
  init unless @global_message_number
101
115
  value = nil if value == undefined_value
102
116
 
103
- field_number = field_definition_number.snapshot
104
- if @global_message_definition &&
105
- (field = @global_message_definition.fields[field_number])
106
- field.to_s(value)
117
+ if value.kind_of?(Array)
118
+ s = '[ '
119
+ value.each { |v| s << to_s(v) + ' ' }
120
+ s + ']'
107
121
  else
108
- "[#{value.to_s}]"
122
+ field_number = field_definition_number.snapshot
123
+ if @global_message_definition &&
124
+ (field = @global_message_definition.fields_by_number[field_number])
125
+ field.to_s(value)
126
+ else
127
+ "[#{value.to_s}]"
128
+ end
109
129
  end
110
130
  end
111
131
 
@@ -117,19 +137,41 @@ module Fit4Ruby
117
137
  end
118
138
 
119
139
  def type(fit_type = false)
120
- if @@TypeDefs.length <= base_type_number.snapshot
121
- Log.fatal "Unknown FIT Base type #{base_type_number.snapshot}"
140
+ check_fit_base_type
141
+ @@TypeDefs[base_type_number.snapshot][fit_type ? 0 : 1]
142
+ end
143
+
144
+ def is_array?
145
+ if total_bytes > base_type_bytes
146
+ if total_bytes % base_type_bytes != 0
147
+ Log.fatal "Total bytes (#{total_bytes}) must be multiple of " +
148
+ "base type bytes (#{base_type_bytes})."
149
+ end
150
+ return true
122
151
  end
152
+ false
153
+ end
123
154
 
124
- @@TypeDefs[base_type_number.snapshot][fit_type ? 0 : 1]
155
+ def total_bytes
156
+ self.byte_count.snapshot
157
+ end
158
+
159
+ def base_type_bytes
160
+ check_fit_base_type
161
+ @@TypeDefs[base_type_number.snapshot][3]
125
162
  end
126
163
 
127
164
  def undefined_value
165
+ check_fit_base_type
166
+ @@TypeDefs[base_type_number.snapshot][2]
167
+ end
168
+
169
+ private
170
+
171
+ def check_fit_base_type
128
172
  if @@TypeDefs.length <= base_type_number.snapshot
129
173
  Log.fatal "Unknown FIT Base type #{base_type_number.snapshot}"
130
174
  end
131
-
132
- @@TypeDefs[base_type_number.snapshot][2]
133
175
  end
134
176
 
135
177
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = FitFile.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015 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,6 +12,7 @@
12
12
 
13
13
  require 'fit4ruby/Log'
14
14
  require 'fit4ruby/FitHeader'
15
+ require 'fit4ruby/FitFileEntity'
15
16
  require 'fit4ruby/FitRecord'
16
17
  require 'fit4ruby/FitFilter'
17
18
  require 'fit4ruby/FitMessageIdMapper'
@@ -39,7 +40,7 @@ module Fit4Ruby
39
40
 
40
41
  check_crc(io, header.end_pos)
41
42
 
42
- activity = Activity.new
43
+ entity = FitFileEntity.new
43
44
  # This Array holds the raw data of the records that may be needed to
44
45
  # dump a human readable form of the FIT file.
45
46
  records = []
@@ -48,7 +49,7 @@ module Fit4Ruby
48
49
  record_counters = Hash.new { 0 }
49
50
  while io.pos < header.end_pos
50
51
  record = FitRecord.new(definitions)
51
- record.read(io, activity, filter, record_counters)
52
+ record.read(io, entity, filter, record_counters)
52
53
  records << record if filter
53
54
  end
54
55
 
@@ -57,11 +58,11 @@ module Fit4Ruby
57
58
  header.dump if filter && filter.record_numbers.nil?
58
59
  dump_records(records) if filter
59
60
 
60
- activity.check
61
- activity
61
+ entity.check
62
+ entity.top_level_record
62
63
  end
63
64
 
64
- def write(file_name, activity)
65
+ def write(file_name, top_level_record)
65
66
  begin
66
67
  io = ::File.open(file_name, 'wb+')
67
68
  rescue StandardError => e
@@ -74,7 +75,7 @@ module Fit4Ruby
74
75
  # Move the pointer behind the header section.
75
76
  io.seek(start_pos)
76
77
  id_mapper = FitMessageIdMapper.new
77
- activity.write(io, id_mapper)
78
+ top_level_record.write(io, id_mapper)
78
79
  end_pos = io.pos
79
80
 
80
81
  crc = write_crc(io, start_pos, end_pos)
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = FitMessageRecord.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2015 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
+ require 'fit4ruby/Activity'
14
+
15
+ module Fit4Ruby
16
+
17
+ # The FIT file is a generic container for all kinds of data. This could be
18
+ # activity data, config files, workout definitions, etc. All data is stored
19
+ # in FIT message records. Also the information what kind of FIT file this is
20
+ # is stored in such a record. When we start reading the file, we actually
21
+ # don't know what kind of file it is until we find the right record to tell
22
+ # us. Since we already need to have gathered some information at this point,
23
+ # we use this utility class to store the read data until we know what Ruby
24
+ # objec we need to use to store it for later consumption.
25
+ class FitFileEntity
26
+
27
+ attr_reader :top_level_record
28
+
29
+ # Create a FitFileEntity.
30
+ def initialize
31
+ @top_level_record = nil
32
+ end
33
+
34
+ # Set what kind of FIT file we are dealing with.
35
+ # @return The Ruby object that will hold the content of the FIT file. It's
36
+ # a derivative of FitDataRecord.
37
+ def set_type(type)
38
+ if @top_level_record
39
+ Log.fatal "FIT file type has already been set to " +
40
+ "#{@top_level_record.class}"
41
+ end
42
+ case type
43
+ when 4, 'activity'
44
+ @top_level_record = Activity.new
45
+ @type = 'activity'
46
+ else
47
+ Log.error "Unsupported FIT file type #{type}"
48
+ return nil
49
+ end
50
+
51
+ @top_level_record
52
+ end
53
+
54
+ # Add a new data record to the top-level object.
55
+ def new_fit_data_record(type)
56
+ return nil unless @top_level_record
57
+
58
+ # We already have a record for the top-level type. Just return it.
59
+ return @top_level_record if type == @type
60
+
61
+ # For all other types, we need to create a new record inside the
62
+ # top-level record.
63
+ @top_level_record.new_fit_data_record(type)
64
+ end
65
+
66
+ # Check the consistency of the top-level object.
67
+ def check
68
+ return false unless @top_level_record
69
+ @top_level_record.check
70
+ end
71
+
72
+ # Write the top-level object into a IO stream.
73
+ def write(io, id_mapper)
74
+ return unless @top_level_record
75
+ @top_level_record.write(io, id_mapper)
76
+ end
77
+
78
+ end
79
+
80
+ end