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 +4 -4
- data/lib/fit4ruby.rb +2 -2
- data/lib/fit4ruby/Activity.rb +7 -2
- data/lib/fit4ruby/FitDataRecord.rb +26 -16
- data/lib/fit4ruby/FitDefinition.rb +11 -1
- data/lib/fit4ruby/FitDefinitionField.rb +60 -18
- data/lib/fit4ruby/FitFile.rb +8 -7
- data/lib/fit4ruby/FitFileEntity.rb +80 -0
- data/lib/fit4ruby/FitMessageIdMapper.rb +16 -5
- data/lib/fit4ruby/FitMessageRecord.rb +89 -9
- data/lib/fit4ruby/FitRecord.rb +22 -11
- data/lib/fit4ruby/FitRecordHeader.rb +2 -2
- data/lib/fit4ruby/GlobalFitDictionaries.rb +50 -12
- data/lib/fit4ruby/GlobalFitMessage.rb +125 -8
- data/lib/fit4ruby/GlobalFitMessages.rb +135 -18
- data/lib/fit4ruby/version.rb +1 -1
- data/spec/FitFile_spec.rb +16 -12
- metadata +3 -2
@@ -12,16 +12,25 @@
|
|
12
12
|
|
13
13
|
module Fit4Ruby
|
14
14
|
|
15
|
+
# The FIT file maps GlobalFitMessage numbers to local numbers. Due to
|
16
|
+
# restrictions in the format, only 16 local messages can be active at any
|
17
|
+
# point in the file. If a GlobalFitMessage is needed that is currently not
|
18
|
+
# mapped, a new entry is generated and the least recently used message is
|
19
|
+
# evicted. The FitMessageIdMapper is the objects that stores those 16 active
|
20
|
+
# entries and can map global to local message numbers.
|
15
21
|
class FitMessageIdMapper
|
16
22
|
|
17
|
-
|
23
|
+
# The entry in the mapper.
|
24
|
+
class Entry < Struct.new(:global_message, :last_use)
|
18
25
|
end
|
19
26
|
|
20
27
|
def initialize
|
21
28
|
@entries = Array.new(16, nil)
|
22
29
|
end
|
23
30
|
|
24
|
-
|
31
|
+
# Add a new GlobalFitMessage to the mapper and return the local message
|
32
|
+
# number.
|
33
|
+
def add_global(message)
|
25
34
|
unless (slot = @entries.index { |e| e.nil? })
|
26
35
|
# No more free slots. We have to find the least recently used one.
|
27
36
|
slot = 0
|
@@ -31,14 +40,16 @@ module Fit4Ruby
|
|
31
40
|
end
|
32
41
|
end
|
33
42
|
end
|
34
|
-
@entries[slot] = Entry.new(
|
43
|
+
@entries[slot] = Entry.new(message, Time.now)
|
35
44
|
|
36
45
|
slot
|
37
46
|
end
|
38
47
|
|
39
|
-
|
48
|
+
# Get the local message number for a given GlobalFitMessage. If there is
|
49
|
+
# no message number, nil is returned.
|
50
|
+
def get_local(message)
|
40
51
|
0.upto(15) do |i|
|
41
|
-
if (entry = @entries[i]) && entry.
|
52
|
+
if (entry = @entries[i]) && entry.global_message == message
|
42
53
|
entry.last_use = Time.now
|
43
54
|
return i
|
44
55
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitMessageRecord.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
|
@@ -13,9 +13,16 @@
|
|
13
13
|
require 'bindata'
|
14
14
|
require 'fit4ruby/Log'
|
15
15
|
require 'fit4ruby/GlobalFitMessage'
|
16
|
+
require 'fit4ruby/FitFileEntity'
|
16
17
|
|
17
18
|
module Fit4Ruby
|
18
19
|
|
20
|
+
# The FitMessageRecord models a part of the FIT file that contains the
|
21
|
+
# FIT message records. Each message record has a number, a local type and a
|
22
|
+
# set of data fields. The content of a FitMessageRecord is defined by the
|
23
|
+
# FitDefinition. This class is only used for reading data from a FIT file.
|
24
|
+
# For writing FIT message records, the class FitDataRecord and its
|
25
|
+
# decendents are used.
|
19
26
|
class FitMessageRecord
|
20
27
|
|
21
28
|
attr_reader :global_message_number, :name, :message_record
|
@@ -33,35 +40,108 @@ module Fit4Ruby
|
|
33
40
|
@message_record = produce(definition)
|
34
41
|
end
|
35
42
|
|
36
|
-
def read(io,
|
43
|
+
def read(io, entity, filter = nil, fields_dump = nil)
|
37
44
|
@message_record.read(io)
|
38
45
|
|
39
|
-
|
46
|
+
if @name == 'file_id'
|
47
|
+
unless (entity_type = @message_record['type'].snapshot)
|
48
|
+
Log.fatal "Corrupted FIT file: file_id record has no type definition"
|
49
|
+
end
|
50
|
+
entity.set_type(entity_type)
|
51
|
+
end
|
52
|
+
obj = entity.new_fit_data_record(@name)
|
53
|
+
|
54
|
+
# It's important to ensure that alternative fields processed after the
|
55
|
+
# regular fields so that the decision field is already set.
|
56
|
+
sorted_fields = @definition.fields.sort do |f1, f2|
|
57
|
+
f1alt = is_alt_field?(f1)
|
58
|
+
f2alt = is_alt_field?(f2)
|
59
|
+
f1alt == f2alt ?
|
60
|
+
f1.field_definition_number.snapshot <=>
|
61
|
+
f2.field_definition_number.snapshot :
|
62
|
+
f1alt ? 1 : -1
|
63
|
+
end
|
40
64
|
|
41
|
-
|
65
|
+
sorted_fields.each do |field|
|
42
66
|
value = @message_record[field.name].snapshot
|
43
|
-
|
67
|
+
# Strings are null byte terminated. There may be more bytes in the
|
68
|
+
# file, but we have to discard all bytes from the first null byte
|
69
|
+
# onwards.
|
70
|
+
if value.is_a?(String) && (null_byte = value.index("\0"))
|
71
|
+
value = value[0..(null_byte - 1)]
|
72
|
+
end
|
73
|
+
|
74
|
+
field_name, field_def = get_field_name_and_global_def(field, obj)
|
75
|
+
obj.set(field_name, field.to_machine(value)) if obj
|
76
|
+
|
44
77
|
if filter && fields_dump &&
|
45
78
|
(filter.field_names.nil? ||
|
46
|
-
filter.field_names.include?(
|
79
|
+
filter.field_names.include?(field_name)) &&
|
47
80
|
(value != field.undefined_value || !filter.ignore_undef)
|
48
|
-
fields_dump[field
|
81
|
+
fields_dump << [ field.type(true), field_name,
|
82
|
+
(field_def ? field_def : field).to_s(value) ]
|
49
83
|
end
|
50
84
|
end
|
51
85
|
end
|
52
86
|
|
53
87
|
private
|
54
88
|
|
89
|
+
def is_alt_field?(field)
|
90
|
+
return false unless @gfm
|
91
|
+
|
92
|
+
field_def_number = field.field_definition_number.snapshot
|
93
|
+
field_def = @gfm.fields_by_number[field_def_number]
|
94
|
+
field_def.is_a?(GlobalFitMessage::AltField)
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_field_name_and_global_def(field, obj)
|
98
|
+
# If we don't have a corresponding GlobalFitMessage definition, we can't
|
99
|
+
# tell if the field is an alternative or not. We don't treat it as such.
|
100
|
+
return [ field.name, nil ] unless @gfm
|
101
|
+
|
102
|
+
field_def_number = field.field_definition_number.snapshot
|
103
|
+
# Get the corresponding GlobalFitMessage field definition.
|
104
|
+
field_def = @gfm.fields_by_number[field_def_number]
|
105
|
+
# If it's not an AltField, we just use the already given name.
|
106
|
+
unless field_def.is_a?(GlobalFitMessage::AltField)
|
107
|
+
return [ field.name, nil ]
|
108
|
+
end
|
109
|
+
|
110
|
+
# We have an AltField. Now we need to find the selection field and its
|
111
|
+
# value.
|
112
|
+
ref_field = field_def.ref_field
|
113
|
+
ref_value = obj ? obj.get(ref_field) : :default
|
114
|
+
|
115
|
+
# Based on that value, we select the Field of the AltField.
|
116
|
+
selected_field = field_def.fields[ref_value] ||
|
117
|
+
field_def.fields[:default]
|
118
|
+
Log.fatal "The value #{ref_value} of field #{ref_field} does not match " +
|
119
|
+
"any selection of alternative field #{field_def_number} in " +
|
120
|
+
"GlobalFitMessage #{@gfm.name}" unless selected_field
|
121
|
+
|
122
|
+
[ selected_field.name, selected_field ]
|
123
|
+
end
|
124
|
+
|
55
125
|
def produce(definition)
|
56
126
|
fields = []
|
57
127
|
definition.fields.each do |field|
|
58
|
-
|
128
|
+
field_def = [ field.type, field.name ]
|
129
|
+
if field.type == 'string'
|
130
|
+
# Strings need special handling. We need to also include the length
|
131
|
+
# of the String.
|
132
|
+
field_def << { :read_length => field.total_bytes }
|
133
|
+
elsif field.is_array?
|
134
|
+
field_def = [ :array, field.name,
|
135
|
+
{ :type => field.type.intern,
|
136
|
+
:initial_length => field.total_bytes /
|
137
|
+
field.base_type_bytes } ]
|
138
|
+
end
|
139
|
+
fields << field_def
|
59
140
|
end
|
60
141
|
|
61
142
|
BinData::Struct.new(:endian => definition.endian, :fields => fields)
|
62
143
|
end
|
63
144
|
|
64
|
-
|
65
145
|
end
|
66
146
|
|
67
147
|
end
|
data/lib/fit4ruby/FitRecord.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitRecord.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
|
@@ -15,10 +15,14 @@ require 'fit4ruby/FitRecordHeader'
|
|
15
15
|
require 'fit4ruby/FitDefinition'
|
16
16
|
require 'fit4ruby/FitMessageRecord'
|
17
17
|
require 'fit4ruby/FitFilter'
|
18
|
-
require 'fit4ruby/
|
18
|
+
require 'fit4ruby/FitFileEntity'
|
19
19
|
|
20
20
|
module Fit4Ruby
|
21
21
|
|
22
|
+
# The FitRecord is a basic building block of a FIT file. It can either
|
23
|
+
# contain a definition of a message record or an actual message record with
|
24
|
+
# FIT data. A FIT record always starts with a header that describes what
|
25
|
+
# kind of record this is.
|
22
26
|
class FitRecord
|
23
27
|
|
24
28
|
def initialize(definitions)
|
@@ -26,13 +30,13 @@ module Fit4Ruby
|
|
26
30
|
@name = @number = @fields = nil
|
27
31
|
end
|
28
32
|
|
29
|
-
def read(io,
|
33
|
+
def read(io, entity, filter, record_counters)
|
30
34
|
header = FitRecordHeader.read(io)
|
31
35
|
|
32
|
-
if header.
|
36
|
+
if !header.compressed?
|
33
37
|
# process normal headers
|
34
38
|
local_message_type = header.local_message_type.snapshot
|
35
|
-
if header.message_type == 1
|
39
|
+
if header.message_type.snapshot == 1
|
36
40
|
# process definition message
|
37
41
|
definition = FitDefinition.read(io)
|
38
42
|
@definitions[local_message_type] = FitMessageRecord.new(definition)
|
@@ -45,15 +49,19 @@ module Fit4Ruby
|
|
45
49
|
if filter
|
46
50
|
@number = @definitions[local_message_type].global_message_number
|
47
51
|
index = (record_counters[@number] += 1)
|
52
|
+
|
53
|
+
# Check if we have a filter defined to collect raw dumps of the
|
54
|
+
# data in the message records. The dump is collected in @fields
|
55
|
+
# for later output.
|
48
56
|
if (filter.record_numbers.nil? ||
|
49
57
|
filter.record_numbers.include?(@number)) &&
|
50
58
|
(filter.record_indexes.nil? ||
|
51
59
|
filter.record_indexes.include?(index))
|
52
60
|
@name = definition.name
|
53
|
-
@fields =
|
61
|
+
@fields = []
|
54
62
|
end
|
55
63
|
end
|
56
|
-
definition.read(io,
|
64
|
+
definition.read(io, entity, filter, @fields)
|
57
65
|
end
|
58
66
|
else
|
59
67
|
# process compressed timestamp header
|
@@ -67,10 +75,13 @@ module Fit4Ruby
|
|
67
75
|
def dump
|
68
76
|
return unless @fields
|
69
77
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
78
|
+
begin
|
79
|
+
puts "Message #{@number}: #{@name}" unless @fields.empty?
|
80
|
+
@fields.each do |type, name, value|
|
81
|
+
puts " [#{"%-7s" % type}] #{name}: " + "#{value}"
|
82
|
+
end
|
83
|
+
rescue Errno::EPIPE
|
84
|
+
# Avoid ugly error message when aborting a less/more pipe.
|
74
85
|
end
|
75
86
|
end
|
76
87
|
|
@@ -26,6 +26,12 @@ module Fit4Ruby
|
|
26
26
|
entry 6, 'walking'
|
27
27
|
entry 254, 'all'
|
28
28
|
|
29
|
+
dict 'ant_network'
|
30
|
+
entry 0, 'public'
|
31
|
+
entry 1, 'antplus'
|
32
|
+
entry 2, 'antfs'
|
33
|
+
entry 3, 'private'
|
34
|
+
|
29
35
|
dict 'battery_status'
|
30
36
|
entry 1, 'new'
|
31
37
|
entry 2, 'good'
|
@@ -53,6 +59,10 @@ module Fit4Ruby
|
|
53
59
|
entry 123, 'bike_speed'
|
54
60
|
entry 124, 'stride_speed_distance'
|
55
61
|
|
62
|
+
dict 'display_measure'
|
63
|
+
entry 0, 'metric'
|
64
|
+
entry 1, 'statute'
|
65
|
+
|
56
66
|
dict 'event'
|
57
67
|
entry 0, 'timer'
|
58
68
|
entry 3, 'workout'
|
@@ -86,6 +96,7 @@ module Fit4Ruby
|
|
86
96
|
entry 36, 'calibration'
|
87
97
|
entry 37, 'vo2max' # guess
|
88
98
|
entry 38, 'recovery_time' # guess (in minutes)
|
99
|
+
entry 39, 'recovery_info' # guess (in minutes, < 24 good, > 24h poor)
|
89
100
|
entry 42, 'front_gear_change'
|
90
101
|
entry 43, 'rear_gear_change'
|
91
102
|
|
@@ -118,6 +129,23 @@ module Fit4Ruby
|
|
118
129
|
entry 28, 'monitoring_daily'
|
119
130
|
entry 32, 'monitoring_b'
|
120
131
|
|
132
|
+
dict 'garmin_product'
|
133
|
+
entry 8, 'hrm_run_single_byte_product_id'
|
134
|
+
entry 1551, 'fenix'
|
135
|
+
entry 1623, 'fr620'
|
136
|
+
entry 1632, 'fr220'
|
137
|
+
entry 1752, 'hrm_run'
|
138
|
+
entry 1765, 'fr920xt'
|
139
|
+
entry 1928, 'fr620_japan'
|
140
|
+
entry 1929, 'fr620_china'
|
141
|
+
entry 1930, 'fr220_japan'
|
142
|
+
entry 1931, 'fr220_china'
|
143
|
+
entry 1967, 'fenix2'
|
144
|
+
entry 10007, 'sdm4'
|
145
|
+
entry 20119, 'training_center'
|
146
|
+
entry 65532, 'android_antplus_plugin'
|
147
|
+
entry 65534, 'connect'
|
148
|
+
|
121
149
|
dict 'gender'
|
122
150
|
entry 0, 'female'
|
123
151
|
entry 1, 'male'
|
@@ -139,8 +167,12 @@ module Fit4Ruby
|
|
139
167
|
entry 7, 'session_end'
|
140
168
|
entry 8, 'fitness_equipment'
|
141
169
|
|
170
|
+
dict 'left_right_balance_100'
|
171
|
+
entry 0x3FFF, 'mask'
|
172
|
+
entry 0x8000, 'right'
|
173
|
+
|
142
174
|
dict 'manufacturer'
|
143
|
-
entry 1, '
|
175
|
+
entry 1, 'garmin'
|
144
176
|
entry 2, 'garmin_fr405_antfs'
|
145
177
|
entry 3, 'zephyr'
|
146
178
|
entry 4, 'dayton'
|
@@ -232,6 +264,14 @@ module Fit4Ruby
|
|
232
264
|
entry 2, 'auto_multi_sport'
|
233
265
|
entry 3, 'fitness_equipment'
|
234
266
|
|
267
|
+
dict 'source_type'
|
268
|
+
entry 0, 'ant'
|
269
|
+
entry 1, 'antplus'
|
270
|
+
entry 2, 'bluetooth'
|
271
|
+
entry 3, 'bluetooth_low_enegery'
|
272
|
+
entry 4, 'wifi'
|
273
|
+
entry 5, 'local'
|
274
|
+
|
235
275
|
dict 'sport'
|
236
276
|
entry 0, 'generic'
|
237
277
|
entry 1, 'running'
|
@@ -255,6 +295,15 @@ module Fit4Ruby
|
|
255
295
|
entry 19, 'paddling'
|
256
296
|
entry 254, 'all'
|
257
297
|
|
298
|
+
dict 'swim_stroke'
|
299
|
+
entry 0, 'freestyle'
|
300
|
+
entry 1, 'backstroke'
|
301
|
+
entry 2, 'breaststrike'
|
302
|
+
entry 3, 'butterfly'
|
303
|
+
entry 4, 'drill'
|
304
|
+
entry 5, 'mixed'
|
305
|
+
entry 6, 'im'
|
306
|
+
|
258
307
|
dict 'sub_sport'
|
259
308
|
entry 0, 'generic'
|
260
309
|
entry 1, 'treadmill'
|
@@ -285,17 +334,6 @@ module Fit4Ruby
|
|
285
334
|
entry 26, 'cardio_training'
|
286
335
|
entry 254, 'all'
|
287
336
|
|
288
|
-
dict 'product'
|
289
|
-
entry 8, 'hrm_run_single_byte_product_id'
|
290
|
-
entry 1623, 'fr620'
|
291
|
-
entry 1632, 'fr220'
|
292
|
-
entry 1752, 'hrm_run'
|
293
|
-
entry 1928, 'fr620_japan'
|
294
|
-
entry 1929, 'fr620_china'
|
295
|
-
entry 1930, 'fr220_japan'
|
296
|
-
entry 1931 , 'fr220_china'
|
297
|
-
entry 10007, 'sdm4'
|
298
|
-
|
299
337
|
end
|
300
338
|
|
301
339
|
end
|
@@ -16,10 +16,16 @@ require 'fit4ruby/FitDefinitionField'
|
|
16
16
|
|
17
17
|
module Fit4Ruby
|
18
18
|
|
19
|
+
# The GlobalFitMessage stores an abstract description of a particular
|
20
|
+
# FitMessage. It holds information like the name, the global ID number and
|
21
|
+
# the data fields of the message.
|
19
22
|
class GlobalFitMessage
|
20
23
|
|
21
|
-
attr_reader :name, :number, :
|
24
|
+
attr_reader :name, :number, :fields_by_name, :fields_by_number
|
22
25
|
|
26
|
+
# The Field objects describe the name, type and optional attributes of a
|
27
|
+
# FitMessage definition field. It also provides methods to convert field
|
28
|
+
# values into various formats.
|
23
29
|
class Field
|
24
30
|
|
25
31
|
include Converters
|
@@ -131,24 +137,128 @@ module Fit4Ruby
|
|
131
137
|
|
132
138
|
end
|
133
139
|
|
140
|
+
# A GlobalFitMessage may have Field entries that are dependent on the
|
141
|
+
# value of another Field. These alternative fields all depend on the value
|
142
|
+
# of a specific other Field of the GlobalFitMessage and their presense is
|
143
|
+
# mutually exclusive. An AltField object models such a group of Field
|
144
|
+
# objects.
|
145
|
+
class AltField
|
146
|
+
|
147
|
+
attr_reader :fields, :ref_field
|
148
|
+
|
149
|
+
# Create a new AltField object.
|
150
|
+
# @param message [GlobalFitMessage] reference to the GlobalFitMessage
|
151
|
+
# this field belongs to.
|
152
|
+
# @param ref_field [String] The name of the field that is used to select
|
153
|
+
# the alternative.
|
154
|
+
def initialize(message, ref_field, &block)
|
155
|
+
@message = message
|
156
|
+
@ref_field = ref_field
|
157
|
+
@fields = {}
|
158
|
+
|
159
|
+
instance_eval(&block) if block_given?
|
160
|
+
end
|
161
|
+
|
162
|
+
def field(ref_value, type, name, opts = {})
|
163
|
+
field = Field.new(type, name, opts)
|
164
|
+
if ref_value.respond_to?('each')
|
165
|
+
ref_value.each do |rv|
|
166
|
+
@fields[rv] = field
|
167
|
+
end
|
168
|
+
else
|
169
|
+
@fields[ref_value] = field
|
170
|
+
end
|
171
|
+
@message.register_field_by_name(field, name)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Select the alternative field based on the actual field values of the
|
175
|
+
# FitMessageRecord.
|
176
|
+
def select(field_values_by_name)
|
177
|
+
unless (value = field_values_by_name[@ref_field])
|
178
|
+
Log.fatal "The selection field #{@ref_field} for the alternative " +
|
179
|
+
"field is undefined in global message #{@message.name}: " +
|
180
|
+
field_values_by_name.inspect
|
181
|
+
end
|
182
|
+
@fields.each do |ref_value, field|
|
183
|
+
return field if ref_value == value
|
184
|
+
end
|
185
|
+
return @fields[:default] if @fields[:default]
|
186
|
+
|
187
|
+
Log.fatal "The selector value #{value} for the alternative field " +
|
188
|
+
"is not supported in global message #{@message.name}."
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
# Create a new GlobalFitMessage definition.
|
194
|
+
# @param name [String] name of the FIT message
|
195
|
+
# @param number [Fixnum] global message number
|
134
196
|
def initialize(name, number)
|
135
197
|
@name = name
|
136
198
|
@number = number
|
137
|
-
|
199
|
+
# Field names must be unique. A name always matches a single Field.
|
200
|
+
@fields_by_name = {}
|
201
|
+
# Field numbers are not unique. A group of alternative fields shares the
|
202
|
+
# same number and is stored as an AltField. Otherwise as Field.
|
203
|
+
@fields_by_number = {}
|
138
204
|
end
|
139
205
|
|
206
|
+
# Two GlobalFitMessage objects are considered equal if they have the same
|
207
|
+
# number, name and list of named fields.
|
208
|
+
def ==(m)
|
209
|
+
@number == m.number && @name == m.name &&
|
210
|
+
@fields_by_name.keys.sort == m.fields_by_name.keys.sort
|
211
|
+
end
|
212
|
+
|
213
|
+
# Define a new Field for this message definition.
|
140
214
|
def field(number, type, name, opts = {})
|
141
|
-
|
215
|
+
field = Field.new(type, name, opts)
|
216
|
+
register_field_by_name(field, name)
|
217
|
+
register_field_by_number(field, number)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Define a new set of Field alternatives for this message definition.
|
221
|
+
def alt_field(number, ref_field, &block)
|
222
|
+
unless @fields_by_name.include?(ref_field)
|
223
|
+
raise "Unknown ref_field: #{ref_field}"
|
224
|
+
end
|
225
|
+
|
226
|
+
field = AltField.new(self, ref_field, &block)
|
227
|
+
register_field_by_number(field, number)
|
228
|
+
end
|
229
|
+
|
230
|
+
def register_field_by_name(field, name)
|
231
|
+
if @fields_by_name.include?(name)
|
232
|
+
raise "Field '#{name}' has already been defined"
|
233
|
+
end
|
234
|
+
|
235
|
+
@fields_by_name[name] = field
|
236
|
+
end
|
237
|
+
|
238
|
+
def register_field_by_number(field, number)
|
239
|
+
if @fields_by_name.include?(number)
|
142
240
|
raise "Field #{number} has already been defined"
|
143
241
|
end
|
144
|
-
|
242
|
+
|
243
|
+
@fields_by_number[number] = field
|
145
244
|
end
|
146
245
|
|
147
|
-
def
|
148
|
-
|
246
|
+
def construct(field_values_by_name)
|
247
|
+
gfm = GlobalFitMessage.new(@name, @number)
|
248
|
+
|
249
|
+
@fields_by_number.each do |number, field|
|
250
|
+
if field.is_a?(AltField)
|
251
|
+
# For alternative fields, we need to look at the value of the
|
252
|
+
# selector field and pick the corresponding Field.
|
253
|
+
field = field.select(field_values_by_name)
|
254
|
+
end
|
255
|
+
gfm.field(number, field.type, field.name, field.opts)
|
256
|
+
end
|
257
|
+
|
258
|
+
gfm
|
149
259
|
end
|
150
260
|
|
151
|
-
def write(io, local_message_type)
|
261
|
+
def write(io, local_message_type, global_fit_message = self)
|
152
262
|
header = FitRecordHeader.new
|
153
263
|
header.normal = 0
|
154
264
|
header.message_type = 1
|
@@ -157,7 +267,7 @@ module Fit4Ruby
|
|
157
267
|
|
158
268
|
definition = FitDefinition.new
|
159
269
|
definition.global_message_number = @number
|
160
|
-
definition.setup(
|
270
|
+
definition.setup(global_fit_message)
|
161
271
|
definition.write(io)
|
162
272
|
end
|
163
273
|
|
@@ -212,6 +322,13 @@ module Fit4Ruby
|
|
212
322
|
@current_message.field(number, type, name, opts)
|
213
323
|
end
|
214
324
|
|
325
|
+
def alt_field(number, ref_field, &block)
|
326
|
+
unless @current_message
|
327
|
+
raise "You must define a message first"
|
328
|
+
end
|
329
|
+
@current_message.alt_field(number, ref_field, &block)
|
330
|
+
end
|
331
|
+
|
215
332
|
def [](number)
|
216
333
|
@messages[number]
|
217
334
|
end
|