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