fit4ruby 0.0.1

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,189 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = GlobalFitMessage.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2014 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/GlobalFitDictList'
14
+ require 'fit4ruby/Converters'
15
+
16
+ module Fit4Ruby
17
+
18
+ class GlobalFitMessage
19
+
20
+ attr_reader :name, :number, :fields
21
+
22
+ class Field
23
+
24
+ include Converters
25
+
26
+ attr_reader :type, :name, :opts
27
+
28
+ def initialize(type, name, opts = {})
29
+ @type = type
30
+ @name = name
31
+ @opts = opts
32
+ end
33
+
34
+ def to_machine(value)
35
+ return nil if value.nil?
36
+
37
+ if @opts.include?(:dict) &&
38
+ (dict = GlobalFitDictionaries[@opts[:dict]])
39
+ return [ dict.name(value) || "Undocumented value #{value}", nil ]
40
+ end
41
+
42
+ case @opts[:type]
43
+ when 'coordinate'
44
+ value *= 180.0 / 2147483648
45
+ when 'date_time'
46
+ value = fit_time_to_time(value)
47
+ end
48
+ value /= @opts[:scale].to_f if @opts[:scale]
49
+ value -= @opts[:offset] if @opts[:offset]
50
+ value
51
+ end
52
+
53
+ def to_human(value)
54
+ return nil if value.nil?
55
+
56
+ if @opts.include?(:dict) &&
57
+ (dict = GlobalFitDictionaries[@opts[:dict]])
58
+ return [ dict.name(value) || "Undocumented value #{value}", nil ]
59
+ end
60
+
61
+ value /= @opts[:scale].to_f if @opts[:scale]
62
+ value -= @opts[:offset] if @opts[:offset]
63
+
64
+ case @opts[:type]
65
+ when 'coordinate'
66
+ value *= 180.0 / 2147483648
67
+ when 'date_time'
68
+ value = fit_time_to_time(value).strftime("%Y-%m-%d %H:%M:%S")
69
+ when 'duration'
70
+ value = secsToDHMS(value)
71
+ end
72
+ [ value, @opts[:unit] ]
73
+ end
74
+
75
+ def native_to_fit(value)
76
+ return nil if value.nil?
77
+
78
+ if @opts.include?(:dict) && (dict = GlobalFitDictionaries[@opts[:dict]])
79
+ return dict.value_by_name(value)
80
+ end
81
+
82
+ value = (value * @opts[:scale].to_f).to_i if @opts[:scale]
83
+ value += @opts[:offset] if @opts[:offset]
84
+
85
+ case @opts[:type]
86
+ when 'coordinate'
87
+ value *= 2147483648.0 / 180.0
88
+ when 'date_time'
89
+ value = time_to_fit_time(value)
90
+ end
91
+
92
+ value
93
+ end
94
+
95
+ def to_s(value)
96
+ return "[no value]" if value.nil?
97
+
98
+ human_readable = to_human(value)
99
+ "#{human_readable[0]}" +
100
+ "#{ human_readable[1] ? " #{human_readable[1]}" : ''}"
101
+ end
102
+
103
+ end
104
+
105
+ def initialize(name, number)
106
+ @name = name
107
+ @number = number
108
+ @fields = {}
109
+ end
110
+
111
+ def field(number, type, name, opts = {})
112
+ if @fields.include?(number)
113
+ raise "Field #{number} has already been defined"
114
+ end
115
+ @fields[number] = Field.new(type, name, opts)
116
+ end
117
+
118
+ def write(io, local_message_type)
119
+ header = FitRecordHeader.new
120
+ header.normal = 0
121
+ header.message_type = 1
122
+ header.local_message_type = local_message_type
123
+ header.write(io)
124
+
125
+ definition = FitDefinition.new
126
+ definition.global_message_number = @number
127
+ definition.setup(self)
128
+ definition.write(io)
129
+ end
130
+
131
+ end
132
+
133
+ class GlobalFitMessageList
134
+
135
+ def initialize(&block)
136
+ @current_message = nil
137
+ @messages = {}
138
+ instance_eval(&block) if block_given?
139
+ end
140
+
141
+ def message(number, name)
142
+ if @messages.include?(number)
143
+ raise "Message #{number} has already been defined"
144
+ end
145
+ @messages[number] = @current_message = GlobalFitMessage.new(name, number)
146
+ end
147
+
148
+ def each
149
+ @messages.each_value do |message|
150
+ yield(message)
151
+ end
152
+ end
153
+
154
+ def write(io)
155
+ # The FIT file supports more than 16 global message types. But only 16
156
+ # can be declared as local message types at any point of time. If there
157
+ # are more then 16 messages, the current subset must be declared before
158
+ # it's being used in the data records of the file.
159
+ # Since we currently have 16 or less message types, we just write them
160
+ # all upfront before the data records.
161
+ if @messages.length > 16
162
+ raise 'Cannot support more than 16 message types!'
163
+ end
164
+
165
+ local_message_type = 0
166
+ @messages.each do |number, message|
167
+ message.write(io, local_message_type)
168
+ end
169
+ end
170
+
171
+ def find_by_name(name)
172
+ @messages.each_value{ |m| return m if m.name == name }
173
+ end
174
+
175
+ def field(number, type, name, opts = {})
176
+ unless @current_message
177
+ raise "You must define a message first"
178
+ end
179
+ @current_message.field(number, type, name, opts)
180
+ end
181
+
182
+ def [](number)
183
+ @messages[number]
184
+ end
185
+
186
+ end
187
+
188
+ end
189
+
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = GlobalFitMessages.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2014 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/GlobalFitMessage'
14
+
15
+ module Fit4Ruby
16
+
17
+ GlobalFitMessages = GlobalFitMessageList.new do
18
+ message 0, 'file_id'
19
+ field 0, 'enum', 'type', :dict => 'file'
20
+ field 1, 'uint16', 'manufacturer', :dict => 'manufacturer'
21
+ field 2, 'uint16', 'product', :dict => 'product'
22
+ field 3, 'uint32z', 'serial_number'
23
+ field 4, 'uint32', 'time_created', :type => 'date_time'
24
+ field 5, 'uint16', 'number'
25
+
26
+ message 18, 'session'
27
+ field 0, 'enum', 'event', :dict => 'event'
28
+ field 1, 'enum', 'event_type', :dict => 'event_type'
29
+ field 2, 'uint32', 'start_time', :type => 'date_time'
30
+ field 3, 'sint32', 'start_position_lat', :type => 'coordinate'
31
+ field 4, 'sint32', 'start_position_long', :type => 'coordinate'
32
+ field 5, 'enum', 'sport', :dict => 'sport'
33
+ field 6, 'enum', 'sub_sport', :dict => 'sub_sport'
34
+ field 7, 'uint32', 'total_elapsed_time', :type => 'duration', :scale => 1000
35
+ field 8, 'uint32', 'total_timer_time', :type => 'duration', :scale => 1000
36
+ field 9, 'uint32', 'total_distance', :scale => 100, :unit => 'm'
37
+ field 10, 'uint32', 'total_strides', :unit => 'strides'
38
+ field 11, 'uint16', 'total_calories', :unit => 'kcal'
39
+ field 13, 'uint16', 'total_fat_calories', :unit => 'kcal'
40
+ field 14, 'uint16', 'avg_speed', :scale => 1000, :unit => 'm/s'
41
+ field 15, 'uint16', 'max_speed', :scale => 1000, :unit => 'm/s'
42
+ field 16, 'uint8', 'avg_heart_rate', :unit => 'bpm'
43
+ field 17, 'uint8', 'max_heart_rate', :unit => 'bpm'
44
+ field 18, 'uint8', 'avg_running_cadence', :unit => 'strides/min'
45
+ field 19, 'uint8', 'max_running_cadence', :unit => 'strides/min'
46
+ field 22, 'uint16', 'total_ascent', :unit => 'm'
47
+ field 23, 'uint16', 'total_descent', :unit => 'm'
48
+ field 24, 'uint8', 'total_training_effect', :scale => 10
49
+ field 25, 'uint16', 'first_lap_index'
50
+ field 26, 'uint16', 'num_laps'
51
+ field 27, 'uint8', 'event_group'
52
+ field 28, 'enum', 'trigger', :dict => 'session_trigger'
53
+ field 29, 'sint32', 'nec_lat', :type => 'coordinate'
54
+ field 30, 'sint32', 'nec_long', :type => 'coordinate'
55
+ field 31, 'sint32', 'swc_lat', :type => 'coordinate'
56
+ field 32, 'sint32', 'swc_long', :type => 'coordinate'
57
+ field 81, 'enum', 'undocumented_field_81'
58
+ field 89, 'uint16', 'avg_vertical_oscillation', :scale => 10, :unit => 'mm'
59
+ field 90, 'uint16', 'avg_stance_time_percent', :scale => 100, :unit => 'percent'
60
+ field 91, 'uint16', 'avg_stance_time', :scale => 10, :unit => 'ms'
61
+ field 92, 'uint8', 'avg_fraction_cadence', :scale => 128
62
+ field 93, 'uint8', 'max_fractional_cadence', :scale => 128
63
+ field 94, 'uint8', 'total_fractional_cycles', :scale => 128
64
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
65
+ field 254, 'uint16', 'message_index'
66
+
67
+ message 19, 'lap'
68
+ field 0, 'enum', 'event', :dict => 'event'
69
+ field 1, 'enum', 'event_type', :dict => 'event_type'
70
+ field 2, 'uint32', 'start_time', :type => 'date_time'
71
+ field 3, 'sint32', 'start_position_lat', :type => 'coordinate'
72
+ field 4, 'sint32', 'start_position_long', :type => 'coordinate'
73
+ field 5, 'sint32', 'end_position_lat', :type => 'coordinate'
74
+ field 6, 'sint32', 'end_position_long', :type => 'coordinate'
75
+ field 7, 'uint32', 'total_elapsed_time', :type => 'duration', :scale => 1000
76
+ field 8, 'uint32', 'total_timer_time', :type => 'duration', :scale => 1000
77
+ field 9, 'uint32', 'total_distance', :scale => 100, :unit => 'm'
78
+ field 10, 'uint32', 'total_strides', :unit => 'strides'
79
+ field 11, 'uint16', 'total_calories', :unit => 'kcal'
80
+ field 12, 'uint16', 'total_fat_calories', :unit => 'kcal'
81
+ field 13, 'uint16', 'avg_speed', :scale => 1000, :unit => 'm/s'
82
+ field 14, 'uint16', 'max_speed', :scale => 1000, :unit => 'm/s'
83
+ field 15, 'uint8', 'avg_heart_rate', :unit => 'bpm'
84
+ field 16, 'uint8', 'max_heart_rate', :unit => 'bpm'
85
+ field 17, 'uint8', 'avg_running_cadence', :unit => 'strides'
86
+ field 18, 'uint8', 'max_running_cadence', :unit => 'strides'
87
+ field 21, 'uint16', 'total_ascent', :unit => 'm'
88
+ field 22, 'uint16', 'total_descent', :unit => 'm'
89
+ field 23, 'enum', 'intensity', :dict => 'intensity'
90
+ field 24, 'enum', 'lap_trigger', :dict => 'lap_trigger'
91
+ field 25, 'enum', 'sport', :dict => 'sport'
92
+ field 26, 'uint8', 'event_group'
93
+ field 27, 'sint32', 'nec_lat', :type => 'coordinate'
94
+ field 28, 'sint32', 'nec_long', :type => 'coordinate'
95
+ field 29, 'sint32', 'swc_lat', :type => 'coordinate'
96
+ field 30, 'sint32', 'swc_long', :type => 'coordinate'
97
+ field 39, 'enum', 'sub_sport', :dict => 'sub_sport'
98
+ field 71, 'uint16', 'wkt_step_index'
99
+ field 72, 'enum', 'undocumented_field_72'
100
+ field 77, 'uint16', 'avg_vertical_oscillation', :scale => 10, :unit => 'mm'
101
+ field 78, 'uint16', 'avg_stance_time_percent', :scale => 100, :unit => 'percent'
102
+ field 79, 'uint16', 'avg_stance_time', :scale => 10, :unit => 'ms'
103
+ field 80, 'uint8', 'avg_fractional_cadence', :scale => 128
104
+ field 81, 'uint8', 'max_fractional_cadence', :scale => 128
105
+ field 82, 'uint8', 'total_fractional_cycles', :scale => 128
106
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
107
+ field 254, 'uint16', 'message_index'
108
+
109
+ message 20, 'record'
110
+ field 0, 'sint32', 'position_lat', :type => 'coordinate'
111
+ field 1, 'sint32', 'position_long', :type => 'coordinate'
112
+ field 2, 'uint16', 'altitude', :scale => 5, :offset => 500, :unit => 'm'
113
+ field 3, 'uint8', 'heart_rate', :unit => 'bpm'
114
+ field 4, 'uint8', 'cadence', :unit => 'rpm'
115
+ field 5, 'uint32', 'distance', :scale => 100, :unit => 'm'
116
+ field 6, 'uint16', 'speed', :scale => 1000, :unit => 'm/s'
117
+ field 39, 'uint16', 'vertical_oscillation', :scale => 10, :unit => 'mm'
118
+ field 40, 'uint16', 'stance_time_percent', :scale => 100, :unit => 'percent'
119
+ field 41, 'uint16', 'stance_time', :scale => 10, :unit => 'ms'
120
+ field 42, 'enum', 'activity_type', :dict => 'activity_type'
121
+ field 53, 'uint8', 'fractional_cadence', :scale => 128 # Just a guess
122
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
123
+
124
+ message 21, 'event'
125
+ field 0, 'enum', 'event', :dict => 'event'
126
+ field 1, 'enum', 'event_type', :dict => 'event_type'
127
+ field 2, 'uint16', 'data16'
128
+ field 3, 'uint32', 'data'
129
+ field 4, 'uint8', 'event_group'
130
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
131
+
132
+ # Possibly the signal quality of the best 6 satellites; Not documented in
133
+ # FIT SDK
134
+ message 22, 'gps_signals'
135
+ field 0, 'uint8', 'Satellite 1'
136
+ field 1, 'uint8', 'Satellite 2'
137
+ field 2, 'uint8', 'Satellite 3'
138
+ field 3, 'uint8', 'Satellite 4'
139
+ field 4, 'uint8', 'Satellite 5'
140
+ field 5, 'enum', 'lock_status'
141
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
142
+
143
+ message 23, 'device_info'
144
+ field 0, 'uint8', 'device_index'
145
+ field 1, 'uint8', 'device_type', :dict => 'device_type'
146
+ field 2, 'uint16', 'manufacturer', :dict => 'manufacturer'
147
+ field 3, 'uint32z', 'serial_number'
148
+ field 4, 'uint16', 'product', :dict => 'product'
149
+ field 5, 'uint16', 'software_version', :scale => 100
150
+ field 6, 'uint8', 'hardware_version'
151
+ field 7, 'uint32', 'cum_operating_time', :type => 'duration'
152
+ field 8, 'uint32', 'undocumented_field_8'
153
+ field 9, 'uint8', 'undocumented_field_9'
154
+ field 10, 'uint16', 'battery_voltage', :scale => 256, :unit => 'V'
155
+ field 11, 'uint8', 'battery_status', :dict => 'battery_status'
156
+ field 15, 'uint32', 'rx_packets_ok' # just a guess
157
+ field 16, 'uint32', 'rx_packets_err' # just a guess
158
+ field 20, 'uint8z', 'undocumented_field_20'
159
+ field 21, 'uint16z', 'undocumented_field_21'
160
+ field 22, 'enum', 'undocumented_field_22'
161
+ field 23, 'uint8', 'undocumented_field_23'
162
+ field 25, 'enum', 'undocumented_field_25'
163
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
164
+
165
+ message 34, 'activity'
166
+ field 0, 'uint32', 'total_timer_time', :type => 'duration', :scale => 1000
167
+ field 1, 'uint16', 'num_sessions'
168
+ field 2, 'enum', 'type', :dict => 'activity_type'
169
+ field 3, 'enum', 'event', :dict => 'event'
170
+ field 4, 'enum', 'event_type', :dict => 'event_type'
171
+ field 5, 'uint32', 'local_timestamp', :type => 'date_time'
172
+ field 6, 'uint8', 'event_group'
173
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
174
+
175
+ message 49, 'file_creator'
176
+ field 0, 'uint16', 'software_version'
177
+ field 1, 'uint8', 'hardware_version'
178
+
179
+ # Not part of the official ANT SDK doc
180
+ message 79, 'user_profile'
181
+ field 0, 'uint16', 'undocumented_field_0' # seems to strongly correlate with vo2max
182
+ field 1, 'uint8', 'age', :unit => 'years'
183
+ field 2, 'uint8', 'height', :scale => 100, :unit => 'm'
184
+ field 3, 'uint16', 'weight', :scale => 10, :unit => 'kg'
185
+ field 4, 'enum', 'gender', :dict => 'gender'
186
+ field 5, 'enum', 'activity_class', :scale => 10
187
+ field 6, 'uint8', 'max_hr', :unit => 'bpm'
188
+ field 7, 'sint8', 'undocumented_field_7' # seems to be always 1
189
+ field 8, 'uint16', 'recovery_time', :scale => 60, :unit => 'hours'
190
+ field 9, 'uint16', 'undocumented_field_9' # maybe activity measurement
191
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
192
+
193
+ # Not part of the official ANT SDK doc
194
+ message 113, 'personal_records'
195
+ field 0, 'uint16', 'longest_distance'
196
+ field 1, 'enum', 'undocumented_field_1' # Seems to be always 1
197
+ field 2, 'uint32', 'distance', :scale => 100, :unit => 'm'
198
+ # If longest_distance is 1, field 3 is the distance, not a duration!
199
+ field 3, 'uint32', 'duration', :scale => 1000, :type => 'duration'
200
+ field 4, 'uint32', 'start_time', :type => 'date_time'
201
+ field 5, 'enum', 'new_record'
202
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
203
+
204
+ # Not part of the official ANT SDK doc
205
+ # The values in this message seem to be related to the activity history.
206
+ # If no HRM is used, most of them are 0. Fields 4, 7, 9 and 10 always have
207
+ # non-zero values.
208
+ message 140, 'undocumented_140'
209
+ field 0, 'uint8', 'max_heart_rate', :unit => 'bpm'
210
+ field 1, 'uint8', 'undocumented_field_1'
211
+ field 2, 'sint32', 'undocumented_field_2'
212
+ field 3, 'sint32', 'undocumented_field_3'
213
+ field 4, 'uint8', 'total_training_effect', :scale => 10
214
+ field 5, 'sint32', 'undocumented_field_5'
215
+ field 6, 'sint32', 'undocumented_field_6'
216
+ field 7, 'sint32', 'undocumented_field_7'
217
+ field 8, 'uint8', 'undocumented_field_8'
218
+ field 9, 'uint16', 'recovery_time', :scale => 60, :unit => 'hours'
219
+ field 10, 'uint16', 'undocumented_field_10' # always seems to be 340
220
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
221
+
222
+ # Not part of the official ANT SDK doc
223
+ message 141, 'gps_ephemeris'
224
+ field 0, 'enum', 'valid' # 0 if no data cached, 1 else
225
+ field 1, 'uint32', 'interval_start', :type => 'date_time'
226
+ field 2, 'uint32', 'interval_end', :type => 'date_time'
227
+ field 3, 'uint32', 'undocumented_field_3'
228
+ field 4, 'sint32', 'undocumented_field_4'
229
+ field 5, 'sint32', 'undocumented_field_5'
230
+ field 253, 'uint32', 'timestamp', :type => 'date_time'
231
+
232
+ end
233
+
234
+ end
235
+
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = Lap.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2014 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
+ class Lap
16
+
17
+ def set(field, value)
18
+ case field
19
+ when 'start_time'
20
+ @start_time = value
21
+ when 'total_timer_time'
22
+ @duration = value
23
+ when 'avg_speed'
24
+ @avg_speed = value
25
+ when 'avg_heart_rate'
26
+ @avg_heart_rate = value
27
+ when 'max_heart_rate'
28
+ @max_heart_rate = value
29
+ when 'avg_vertical_oscillation'
30
+ @avg_vertical_oscillation = value
31
+ when 'avg_stance_time'
32
+ @avg_stance_time = value
33
+ when 'avg_running_cadence'
34
+ @avg_running_cadence = 2 * value
35
+ when 'avg_fraction_cadence'
36
+ @avg_running_cadence += 2 * value
37
+ else
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+