fit4ruby 0.0.2 → 0.0.3
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/README.md +87 -0
- data/Rakefile +3 -0
- data/lib/fit4ruby/Activity.rb +94 -9
- data/lib/fit4ruby/Converters.rb +26 -3
- data/lib/fit4ruby/FitDataRecord.rb +27 -1
- data/lib/fit4ruby/GlobalFitMessage.rb +8 -1
- data/lib/fit4ruby/GlobalFitMessages.rb +12 -1
- data/lib/fit4ruby/Lap.rb +24 -43
- data/lib/fit4ruby/Record.rb +19 -0
- data/lib/fit4ruby/RecordAggregator.rb +173 -0
- data/lib/fit4ruby/Session.rb +28 -23
- data/lib/fit4ruby/UserProfile.rb +4 -0
- data/lib/fit4ruby/version.rb +2 -1
- data/spec/FitFile_spec.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b1343b7cebb0656f6a7f59aa5b1eb067e684504
|
4
|
+
data.tar.gz: c547627b2a01e6ed635cec1da6618f49e8bc5385
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd337bb5e3d842f4720679ae934cd7c490f92c549569af97a14755e78f8275541faeec5bd44e3b2a858e3a4c647d7c747a54c2d4b2624afbd0e81fe6753cd1c4
|
7
|
+
data.tar.gz: bc9a7c8f43d5f239a768df8f18ebbe1989c835ac5c460f68ca71ef7f35b2390b2756ffd8f954b3de805880ea3c58a17a4d7612c0399d3ac4d6bcdf3c38410ada
|
data/README.md
CHANGED
@@ -8,6 +8,93 @@ This libary is still work in progress and probably not yet ready to be
|
|
8
8
|
used in your application. However, you are welcome to try it and send
|
9
9
|
me comments and patches.
|
10
10
|
|
11
|
+
Usage
|
12
|
+
-----
|
13
|
+
|
14
|
+
You can create an Activity.
|
15
|
+
|
16
|
+
```
|
17
|
+
require 'fit4ruby'
|
18
|
+
|
19
|
+
a = Fit4Ruby::Activity.new
|
20
|
+
a.total_timer_time = 30 * 60
|
21
|
+
a.new_user_profile({ :age => 33, :height => 1.78, :weight => 73.0,
|
22
|
+
:gender => 'male', :activity_class => 4.0,
|
23
|
+
:max_hr => 178 })
|
24
|
+
|
25
|
+
a.new_event({ :event => 'timer', :event_type => 'start_time' })
|
26
|
+
a.new_device_info({ :device_index => 0 })
|
27
|
+
a.new_device_info({ :device_index => 1, :battery_status => 'ok' })
|
28
|
+
ts = Time.now
|
29
|
+
0.upto(a.total_timer_time / 60) do |mins|
|
30
|
+
ts += 60
|
31
|
+
a.new_record({
|
32
|
+
:timestamp => ts,
|
33
|
+
:position_lat => 51.5512 - mins * 0.0008,
|
34
|
+
:position_long => 11.647 + mins * 0.002,
|
35
|
+
:distance => 200.0 * mins,
|
36
|
+
:altitude => 100 + mins * 0.5,
|
37
|
+
:speed => 3.1,
|
38
|
+
:vertical_oscillation => 9 + mins * 0.02,
|
39
|
+
:stance_time => 235.0 * mins * 0.01,
|
40
|
+
:stance_time_percent => 32.0,
|
41
|
+
:heart_rate => 140 + mins,
|
42
|
+
:cadence => 75,
|
43
|
+
:activity_type => 'running',
|
44
|
+
:fractional_cadence => (mins % 2) / 2.0
|
45
|
+
})
|
46
|
+
|
47
|
+
if mins > 0 && mins % 5 == 0
|
48
|
+
a.new_lap({ :timestamp => ts })
|
49
|
+
end
|
50
|
+
end
|
51
|
+
a.new_session({ :timestamp => ts })
|
52
|
+
a.new_event({ :timestamp => ts, :event => 'recovery_time',
|
53
|
+
:event_type => 'marker',
|
54
|
+
:data => 2160 })
|
55
|
+
a.new_event({ :timestamp => ts, :event => 'vo2max',
|
56
|
+
:event_type => 'marker', :data => 52 })
|
57
|
+
a.new_event({ :timestamp => ts, :event => 'timer',
|
58
|
+
:event_type => 'stop_all' })
|
59
|
+
a.new_device_info({ :timestamp => ts, :device_index => 0 })
|
60
|
+
ts += 1
|
61
|
+
a.new_device_info({ :timestamp => ts, :device_index => 1,
|
62
|
+
:battery_status => 'low' })
|
63
|
+
ts += 120
|
64
|
+
a.new_event({ :timestamp => ts, :event => 'recovery_hr',
|
65
|
+
:event_type => 'marker', :data => 132 })
|
66
|
+
```
|
67
|
+
|
68
|
+
Now you can have the accumulated data for laps and sessions computed.
|
69
|
+
|
70
|
+
```
|
71
|
+
a.aggregate
|
72
|
+
```
|
73
|
+
|
74
|
+
Save it to a file.
|
75
|
+
|
76
|
+
```
|
77
|
+
Fit4Ruby.write('TEST.FIT', a)
|
78
|
+
```
|
79
|
+
|
80
|
+
Or read an Activity from a file.
|
81
|
+
|
82
|
+
```
|
83
|
+
a = Fit4Ruby.read('TEST.FIT')
|
84
|
+
```
|
85
|
+
|
86
|
+
Then you can access the data in the file.
|
87
|
+
|
88
|
+
```
|
89
|
+
a.records.each do |r|
|
90
|
+
puts "Latitude: #{r['position_lat']}"
|
91
|
+
puts "Longitude: #{r['position_long']}"
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
Please see lib/fit4ruby/GlobalFitMessages.rb for the data fields that
|
96
|
+
are supported for the various FIT record types.
|
97
|
+
|
11
98
|
License
|
12
99
|
-------
|
13
100
|
|
data/Rakefile
CHANGED
@@ -4,8 +4,11 @@ $:.unshift File.join(File.dirname(__FILE__))
|
|
4
4
|
lib = File.expand_path('../lib', __FILE__)
|
5
5
|
$:.unshift lib unless $:.include?(lib)
|
6
6
|
|
7
|
+
require "bundler/gem_tasks"
|
7
8
|
require "rspec/core/rake_task"
|
8
9
|
require 'rake/clean'
|
10
|
+
require 'yard'
|
11
|
+
YARD::Rake::YardocTask.new
|
9
12
|
|
10
13
|
Dir.glob( 'tasks/*.rake').each do |fn|
|
11
14
|
begin
|
data/lib/fit4ruby/Activity.rb
CHANGED
@@ -23,12 +23,18 @@ require 'fit4ruby/PersonalRecords'
|
|
23
23
|
|
24
24
|
module Fit4Ruby
|
25
25
|
|
26
|
+
# This is the most important class of this library. It holds references to
|
27
|
+
# all other data structures. Each of the objects it references are direct
|
28
|
+
# equivalents of the message record structures used in the FIT file.
|
26
29
|
class Activity < FitDataRecord
|
27
30
|
|
28
31
|
attr_accessor :file_id, :file_creator, :device_infos, :user_profiles,
|
29
32
|
:sessions, :laps, :records, :events, :personal_records
|
30
33
|
|
31
|
-
|
34
|
+
# Create a new Activity object.
|
35
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
36
|
+
# certain fields of the FitDataRecord.
|
37
|
+
def initialize(field_values = {})
|
32
38
|
super('activity')
|
33
39
|
@num_sessions = 0
|
34
40
|
|
@@ -46,8 +52,12 @@ module Fit4Ruby
|
|
46
52
|
@cur_lap_records = []
|
47
53
|
|
48
54
|
@lap_counter = 1
|
55
|
+
|
56
|
+
set_field_values(field_values)
|
49
57
|
end
|
50
58
|
|
59
|
+
# Perform some basic logical checks on the object and all references sub
|
60
|
+
# objects. Any errors will be reported via the Log object.
|
51
61
|
def check
|
52
62
|
unless @timestamp && @timestamp >= Time.parse('1990-01-01')
|
53
63
|
Log.error "Activity has no valid timestamp"
|
@@ -63,22 +73,30 @@ module Fit4Ruby
|
|
63
73
|
@sessions.each { |s| s.check(self) }
|
64
74
|
end
|
65
75
|
|
76
|
+
# Convenience method that aggregates all the distances from the included
|
77
|
+
# sessions.
|
66
78
|
def total_distance
|
67
79
|
d = 0.0
|
68
80
|
@sessions.each { |s| d += s.total_distance }
|
69
81
|
d
|
70
82
|
end
|
71
83
|
|
84
|
+
# Call this method to update the aggregated data fields stored in Lap and
|
85
|
+
# Session objects.
|
72
86
|
def aggregate
|
87
|
+
@laps.each { |l| l.aggregate }
|
73
88
|
@sessions.each { |s| s.aggregate }
|
74
89
|
end
|
75
90
|
|
91
|
+
# Convenience method that averages the speed over all sessions.
|
76
92
|
def avg_speed
|
77
93
|
speed = 0.0
|
78
94
|
@sessions.each { |s| speed += s.avg_speed }
|
79
95
|
speed / @sessions.length
|
80
96
|
end
|
81
97
|
|
98
|
+
# Returns the predicted recovery time needed after this activity.
|
99
|
+
# @return recovery time in seconds.
|
82
100
|
def recovery_time
|
83
101
|
@events.each do |e|
|
84
102
|
return e.data if e.event == 'recovery_time'
|
@@ -87,6 +105,8 @@ module Fit4Ruby
|
|
87
105
|
nil
|
88
106
|
end
|
89
107
|
|
108
|
+
# Returns the computed VO2max value. This value is computed by the device
|
109
|
+
# based on multiple previous activities.
|
90
110
|
def vo2max
|
91
111
|
@events.each do |e|
|
92
112
|
return e.data if e.event == 'vo2max'
|
@@ -95,6 +115,10 @@ module Fit4Ruby
|
|
95
115
|
nil
|
96
116
|
end
|
97
117
|
|
118
|
+
# Write the Activity data to a file.
|
119
|
+
# @param io [IO] File reference
|
120
|
+
# @param id_mapper [FitMessageIdMapper] Maps global FIT record types to
|
121
|
+
# local ones.
|
98
122
|
def write(io, id_mapper)
|
99
123
|
@file_id.write(io, id_mapper)
|
100
124
|
@file_creator.write(io, id_mapper)
|
@@ -106,42 +130,91 @@ module Fit4Ruby
|
|
106
130
|
super
|
107
131
|
end
|
108
132
|
|
133
|
+
# Add a new FileId to the Activity. It will replace any previously added
|
134
|
+
# FileId object.
|
135
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
136
|
+
# certain fields of the FitDataRecord.
|
137
|
+
# @return [FileId]
|
109
138
|
def new_file_id(field_values = {})
|
110
139
|
new_fit_data_record('file_id', field_values)
|
111
140
|
end
|
112
141
|
|
142
|
+
# Add a new FileCreator to the Activity. It will replace any previously
|
143
|
+
# added FileCreator object.
|
144
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
145
|
+
# certain fields of the FitDataRecord.
|
146
|
+
# @return [FileCreator]
|
113
147
|
def new_file_creator(field_values = {})
|
114
148
|
new_fit_data_record('file_creator', field_values)
|
115
149
|
end
|
116
150
|
|
151
|
+
# Add a new DeviceInfo to the Activity.
|
152
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
153
|
+
# certain fields of the FitDataRecord.
|
154
|
+
# @return [DeviceInfo]
|
117
155
|
def new_device_info(field_values = {})
|
118
156
|
new_fit_data_record('device_info', field_values)
|
119
157
|
end
|
120
158
|
|
159
|
+
# Add a new UserProfile to the Activity.
|
160
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
161
|
+
# certain fields of the FitDataRecord.
|
162
|
+
# @return [UserProfile]
|
121
163
|
def new_user_profile(field_values = {})
|
122
164
|
new_fit_data_record('user_profile', field_values)
|
123
165
|
end
|
124
166
|
|
167
|
+
# Add a new Event to the Activity.
|
168
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
169
|
+
# certain fields of the FitDataRecord.
|
170
|
+
# @return [Event]
|
125
171
|
def new_event(field_values = {})
|
126
172
|
new_fit_data_record('event', field_values)
|
127
173
|
end
|
128
174
|
|
175
|
+
# Add a new Session to the Activity. All previously added Lap objects are
|
176
|
+
# associated with this Session unless they have been associated with
|
177
|
+
# another Session before. If there are any Record objects that have not
|
178
|
+
# yet been associated with a Lap, a new lap will be created and the
|
179
|
+
# Record objects will be associated with this Lap. The Lap will be
|
180
|
+
# associated with the newly created Session.
|
181
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
182
|
+
# certain fields of the FitDataRecord.
|
183
|
+
# @return [Session]
|
129
184
|
def new_session(field_values = {})
|
130
185
|
new_fit_data_record('session', field_values)
|
131
186
|
end
|
132
187
|
|
188
|
+
# Add a new Lap to the Activity. All previoulsy added Record objects are
|
189
|
+
# associated with this Lap unless they have been associated with another
|
190
|
+
# Lap before.
|
191
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
192
|
+
# certain fields of the FitDataRecord.
|
193
|
+
# @return [Lap]
|
133
194
|
def new_lap(field_values = {})
|
134
195
|
new_fit_data_record('lap', field_values)
|
135
196
|
end
|
136
197
|
|
198
|
+
# Add a new PersonalRecord to the Activity.
|
199
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
200
|
+
# certain fields of the FitDataRecord.
|
201
|
+
# @return [PersonalRecord]
|
137
202
|
def new_personal_record(field_values = {})
|
138
203
|
new_fit_data_record('personal_record', field_values)
|
139
204
|
end
|
140
205
|
|
206
|
+
# Add a new Record to the Activity.
|
207
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
208
|
+
# certain fields of the FitDataRecord.
|
209
|
+
# @return [Record]
|
141
210
|
def new_record(field_values = {})
|
142
211
|
new_fit_data_record('record', field_values)
|
143
212
|
end
|
144
213
|
|
214
|
+
# Check if the current Activity is equal to the passed Activity.
|
215
|
+
# @param a [Activity] Activity to compare this Activity with.
|
216
|
+
# @return [TrueClass/FalseClass] true if both Activities are equal,
|
217
|
+
# otherwise false.
|
145
218
|
def ==(a)
|
146
219
|
super(a) && @file_id == a.file_id &&
|
147
220
|
@file_creator == a.file_creator &&
|
@@ -150,6 +223,12 @@ module Fit4Ruby
|
|
150
223
|
@sessions == a.sessions && personal_records == a.personal_records
|
151
224
|
end
|
152
225
|
|
226
|
+
# Create a new FitDataRecord.
|
227
|
+
# @param record_type [String] Type that identifies the FitDataRecord
|
228
|
+
# derived class to create.
|
229
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
230
|
+
# certain fields of the FitDataRecord.
|
231
|
+
# @return FitDataRecord
|
153
232
|
def new_fit_data_record(record_type, field_values = {})
|
154
233
|
case record_type
|
155
234
|
when 'file_id'
|
@@ -165,20 +244,14 @@ module Fit4Ruby
|
|
165
244
|
when 'session'
|
166
245
|
unless @cur_lap_records.empty?
|
167
246
|
# Ensure that all previous records have been assigned to a lap.
|
168
|
-
|
169
|
-
@cur_session_laps << (lap = Lap.new(@cur_lap_records, field_values))
|
170
|
-
@laps << lap
|
171
|
-
@cur_lap_records = []
|
247
|
+
record = create_new_lap(field_values)
|
172
248
|
end
|
173
249
|
@num_sessions += 1
|
174
250
|
@sessions << (record = Session.new(@cur_session_laps, @lap_counter,
|
175
251
|
field_values))
|
176
252
|
@cur_session_laps = []
|
177
253
|
when 'lap'
|
178
|
-
|
179
|
-
@cur_session_laps << (record = Lap.new(@cur_lap_records, field_values))
|
180
|
-
@laps << record
|
181
|
-
@cur_lap_records = []
|
254
|
+
record = create_new_lap(field_values)
|
182
255
|
when 'record'
|
183
256
|
@cur_lap_records << (record = Record.new(field_values))
|
184
257
|
@records << record
|
@@ -191,6 +264,18 @@ module Fit4Ruby
|
|
191
264
|
record
|
192
265
|
end
|
193
266
|
|
267
|
+
private
|
268
|
+
|
269
|
+
def create_new_lap(field_values)
|
270
|
+
lap = Lap.new(@cur_lap_records, @laps.last, field_values)
|
271
|
+
@lap_counter += 1
|
272
|
+
@cur_session_laps << lap
|
273
|
+
@laps << lap
|
274
|
+
@cur_lap_records = []
|
275
|
+
|
276
|
+
lap
|
277
|
+
end
|
278
|
+
|
194
279
|
end
|
195
280
|
|
196
281
|
end
|
data/lib/fit4ruby/Converters.rb
CHANGED
@@ -14,9 +14,32 @@ module Fit4Ruby
|
|
14
14
|
|
15
15
|
module Converters
|
16
16
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
17
|
+
def conversion_factor(from_unit, to_unit)
|
18
|
+
factors = {
|
19
|
+
'm' => { 'km' => 0.001, 'in' => 39.3701, 'ft' => 3.28084,
|
20
|
+
'mi' => 0.000621371 },
|
21
|
+
'mm' => { 'cm' => 0.1, 'in' => 0.0393701 },
|
22
|
+
'm/s' => { 'km/h' => 0.277778 },
|
23
|
+
'min/km' => { 'min/mi' => 1.60934 },
|
24
|
+
'kg' => { 'lbs' => 0.453592 }
|
25
|
+
}
|
26
|
+
return 1.0 if from_unit == to_unit
|
27
|
+
unless factors.include?(from_unit)
|
28
|
+
Log.fatal "No conversion factors defined for unit " +
|
29
|
+
"'#{from_unit}' to '#{to_unit}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
factor = factors[from_unit][to_unit]
|
33
|
+
if factor.nil?
|
34
|
+
Log.fatal "No conversion factor from '#{from_unit}' to '#{to_unit}' " +
|
35
|
+
"defined."
|
36
|
+
end
|
37
|
+
factor
|
38
|
+
end
|
39
|
+
|
40
|
+
def speedToPace(speed, distance = 1000.0)
|
41
|
+
if speed && speed > 0.01
|
42
|
+
pace = distance / (speed * 60.0)
|
20
43
|
int, dec = pace.divmod 1
|
21
44
|
"#{int}:#{'%02d' % (dec * 60)}"
|
22
45
|
else
|
@@ -17,14 +17,17 @@ module Fit4Ruby
|
|
17
17
|
|
18
18
|
class FitDataRecord
|
19
19
|
|
20
|
+
include Converters
|
21
|
+
|
20
22
|
def initialize(record_id)
|
21
23
|
@message = GlobalFitMessages.find_by_name(record_id)
|
22
24
|
|
23
25
|
# Create instance variables that correspond to every field of the
|
24
|
-
# corresponding FIT
|
26
|
+
# corresponding FIT data record.
|
25
27
|
@message.fields.each do |field_number, field|
|
26
28
|
create_instance_variable(field.name)
|
27
29
|
end
|
30
|
+
@meta_field_units = {}
|
28
31
|
@timestamp = Time.now
|
29
32
|
end
|
30
33
|
|
@@ -43,6 +46,29 @@ module Fit4Ruby
|
|
43
46
|
instance_variable_set('@' + name, value)
|
44
47
|
end
|
45
48
|
|
49
|
+
def get(name)
|
50
|
+
ivar_name = '@' + name
|
51
|
+
return nil unless instance_variable_defined?(ivar_name)
|
52
|
+
|
53
|
+
instance_variable_get('@' + name)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_as(name, to_unit)
|
57
|
+
value = respond_to?(name) ? send(name) : get(name)
|
58
|
+
return nil if value.nil?
|
59
|
+
|
60
|
+
if @meta_field_units.include?(name)
|
61
|
+
unit = @meta_field_units[name]
|
62
|
+
else
|
63
|
+
field = @message.find_by_name(name)
|
64
|
+
unless (unit = field.opts[:unit])
|
65
|
+
Log.fatal "Field #{name} has no unit"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
value * conversion_factor(unit, to_unit)
|
70
|
+
end
|
71
|
+
|
46
72
|
def ==(fdr)
|
47
73
|
@message.fields.each do |field_number, field|
|
48
74
|
ivar_name = '@' + field.name
|
@@ -114,6 +114,9 @@ module Fit4Ruby
|
|
114
114
|
when 'date_time'
|
115
115
|
value = time_to_fit_time(value)
|
116
116
|
end
|
117
|
+
if value.is_a?(Float) && @opts[:scale].nil?
|
118
|
+
Log.error "Field #{@name} must not be a Float value"
|
119
|
+
end
|
117
120
|
|
118
121
|
value
|
119
122
|
end
|
@@ -141,6 +144,10 @@ module Fit4Ruby
|
|
141
144
|
@fields[number] = Field.new(type, name, opts)
|
142
145
|
end
|
143
146
|
|
147
|
+
def find_by_name(field_name)
|
148
|
+
@fields.values.find { |f| f.name == field_name }
|
149
|
+
end
|
150
|
+
|
144
151
|
def write(io, local_message_type)
|
145
152
|
header = FitRecordHeader.new
|
146
153
|
header.normal = 0
|
@@ -195,7 +202,7 @@ module Fit4Ruby
|
|
195
202
|
end
|
196
203
|
|
197
204
|
def find_by_name(name)
|
198
|
-
@messages.
|
205
|
+
@messages.values.find { |m| m.name == name }
|
199
206
|
end
|
200
207
|
|
201
208
|
def field(number, type, name, opts = {})
|
@@ -43,7 +43,7 @@ module Fit4Ruby
|
|
43
43
|
field 17, 'uint8', 'max_heart_rate', :unit => 'bpm'
|
44
44
|
field 18, 'uint8', 'avg_running_cadence', :unit => 'strides/min'
|
45
45
|
field 19, 'uint8', 'max_running_cadence', :unit => 'strides/min'
|
46
|
-
field 22, 'uint16', '
|
46
|
+
field 22, 'uint16', 'total_ascent', :unit => 'm'
|
47
47
|
field 23, 'uint16', 'total_descent', :unit => 'm'
|
48
48
|
field 24, 'uint8', 'total_training_effect', :scale => 10
|
49
49
|
field 25, 'uint16', 'first_lap_index'
|
@@ -162,6 +162,17 @@ module Fit4Ruby
|
|
162
162
|
field 25, 'enum', 'undocumented_field_25'
|
163
163
|
field 253, 'uint32', 'timestamp', :type => 'date_time'
|
164
164
|
|
165
|
+
message 33, 'totals'
|
166
|
+
field 0, 'uint32', 'timer_time', :unit => 's'
|
167
|
+
field 1, 'uint32', 'distance', :unit => 'm'
|
168
|
+
field 2, 'uint32', 'calories', :unit => 'kcal'
|
169
|
+
field 3, 'enum', 'sport', :dict => 'sport'
|
170
|
+
field 4, 'uint32', 'elapsed_time', :unit => 's'
|
171
|
+
field 5, 'uint16', 'sessions'
|
172
|
+
field 6, 'uint32', 'active_time', :unit => 's'
|
173
|
+
field 253, 'uint32', 'timestamp', :type => 'date_time'
|
174
|
+
field 254, 'uint16', 'message_index'
|
175
|
+
|
165
176
|
message 34, 'activity'
|
166
177
|
field 0, 'uint32', 'total_timer_time', :type => 'duration', :scale => 1000
|
167
178
|
field 1, 'uint16', 'num_sessions'
|
data/lib/fit4ruby/Lap.rb
CHANGED
@@ -11,16 +11,35 @@
|
|
11
11
|
#
|
12
12
|
|
13
13
|
require 'fit4ruby/FitDataRecord'
|
14
|
+
require 'fit4ruby/RecordAggregator'
|
14
15
|
|
15
16
|
module Fit4Ruby
|
16
17
|
|
17
18
|
class Lap < FitDataRecord
|
18
19
|
|
20
|
+
include RecordAggregator
|
21
|
+
|
19
22
|
attr_reader :records
|
20
23
|
|
21
|
-
def initialize(records, field_values)
|
24
|
+
def initialize(records, previous_lap, field_values)
|
22
25
|
super('lap')
|
26
|
+
@meta_field_units['avg_stride_length'] = 'm'
|
23
27
|
@records = records
|
28
|
+
@previous_lap = previous_lap
|
29
|
+
|
30
|
+
if previous_lap && previous_lap.records && previous_lap.records.last
|
31
|
+
# Set the start time of the new lap to the timestamp of the last record
|
32
|
+
# of the previous lap.
|
33
|
+
@start_time = previous_lap.records.last.timestamp
|
34
|
+
elsif records.first
|
35
|
+
# Or to the timestamp of the first record.
|
36
|
+
@start_time = records.first.timestamp
|
37
|
+
end
|
38
|
+
|
39
|
+
if records.last
|
40
|
+
@total_elapsed_time = records.last.timestamp - @start_time
|
41
|
+
end
|
42
|
+
|
24
43
|
set_field_values(field_values)
|
25
44
|
end
|
26
45
|
|
@@ -42,49 +61,11 @@ module Fit4Ruby
|
|
42
61
|
end
|
43
62
|
end
|
44
63
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
r = @records[0]
|
49
|
-
@start_time = r.timestamp
|
50
|
-
@start_position_lat = r.position_lat
|
51
|
-
@start_position_long = r.position_long
|
52
|
-
|
53
|
-
r = @records[-1]
|
54
|
-
@end_position_lat = r.position_lat
|
55
|
-
@end_position_long = r.position_long
|
56
|
-
@total_elapsed_time = r.timestamp - @start_time
|
57
|
-
@total_timer_time = @total_elapsed_time
|
58
|
-
@avg_speed = ((@total_distance.to_f / @total_elapsed_time) * 1000).to_i
|
59
|
-
|
60
|
-
@max_speed = 0
|
61
|
-
first_distance, last_distance = nil, nil
|
62
|
-
@records.each do |r|
|
63
|
-
first_distance = r.distance if first_distance.nil? && r.distance
|
64
|
-
last_distance = r.distance if r.distance
|
65
|
-
@max_speed = r.speed if r.speed && @max_speed < r.speed
|
66
|
-
|
67
|
-
if r.position_lat
|
68
|
-
if (@swc_lat.nil? || r.position_lat < @swc_lat)
|
69
|
-
@swc_lat = r.position_lat
|
70
|
-
end
|
71
|
-
if (@nec_lat.nil? || r.position_lat > @nec_lat)
|
72
|
-
@nec_lat = r.position_lat
|
73
|
-
end
|
74
|
-
end
|
75
|
-
if r.position_long
|
76
|
-
if (@swc_long.nil? || r.position_long < @swc_long)
|
77
|
-
@swc_long = r.position_long
|
78
|
-
end
|
79
|
-
if (@nec_long.nil? || r.position_long > @nec_long)
|
80
|
-
@nec_long = r.position_long
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
64
|
+
# Compute the average stride length for this Session.
|
65
|
+
def avg_stride_length
|
66
|
+
return nil unless @total_strides
|
85
67
|
|
86
|
-
@total_distance
|
87
|
-
last_distance - first_distance : 0
|
68
|
+
@total_distance / (@total_strides * 2.0)
|
88
69
|
end
|
89
70
|
|
90
71
|
end
|
data/lib/fit4ruby/Record.rb
CHANGED
@@ -14,13 +14,32 @@ require 'fit4ruby/FitDataRecord'
|
|
14
14
|
|
15
15
|
module Fit4Ruby
|
16
16
|
|
17
|
+
# The Record corresponds to the record FIT message. A Record is a basic set
|
18
|
+
# of primary measurements that are associated with a certain timestamp.
|
17
19
|
class Record < FitDataRecord
|
18
20
|
|
21
|
+
# Create a new Record object.
|
22
|
+
# @param field_values [Hash] Hash that provides initial values for certain
|
23
|
+
# fields.
|
19
24
|
def initialize(field_values = {})
|
20
25
|
super('record')
|
26
|
+
@meta_field_units['pace'] = 'min/km'
|
27
|
+
@meta_field_units['run_cadence'] = 'spm'
|
21
28
|
set_field_values(field_values)
|
22
29
|
end
|
23
30
|
|
31
|
+
def run_cadence
|
32
|
+
if @cadence && @fractional_cadence
|
33
|
+
(@cadence + @fractional_cadence) * 2
|
34
|
+
elsif @cadence
|
35
|
+
@cadence * 2
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Convert the 'speed' field into a running pace. The pace is measured in
|
42
|
+
# minutes per Kilometer.
|
24
43
|
def pace
|
25
44
|
1000.0 / (@speed * 60.0)
|
26
45
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = RecordAggregator.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
|
+
module RecordAggregator
|
16
|
+
|
17
|
+
def aggregate
|
18
|
+
return if @records.empty?
|
19
|
+
|
20
|
+
# TODO: Add support for pause events.
|
21
|
+
@total_timer_time = @total_elapsed_time
|
22
|
+
|
23
|
+
aggregate_geo_region
|
24
|
+
aggregate_ascent_descent
|
25
|
+
aggregate_speed_distance
|
26
|
+
aggregate_heart_rate
|
27
|
+
aggregate_strides
|
28
|
+
aggregate_vertical_oscillation
|
29
|
+
aggregate_stance_time
|
30
|
+
end
|
31
|
+
|
32
|
+
def aggregate_geo_region
|
33
|
+
r = @records.first
|
34
|
+
@start_position_lat = r.position_lat
|
35
|
+
@start_position_long = r.position_long
|
36
|
+
|
37
|
+
r = @records.last
|
38
|
+
@end_position_lat = r.position_lat
|
39
|
+
@end_position_long = r.position_long
|
40
|
+
|
41
|
+
@records.each do |r|
|
42
|
+
if r.position_lat
|
43
|
+
if (@swc_lat.nil? || r.position_lat < @swc_lat)
|
44
|
+
@swc_lat = r.position_lat
|
45
|
+
end
|
46
|
+
if (@nec_lat.nil? || r.position_lat > @nec_lat)
|
47
|
+
@nec_lat = r.position_lat
|
48
|
+
end
|
49
|
+
end
|
50
|
+
if r.position_long
|
51
|
+
if (@swc_long.nil? || r.position_long < @swc_long)
|
52
|
+
@swc_long = r.position_long
|
53
|
+
end
|
54
|
+
if (@nec_long.nil? || r.position_long > @nec_long)
|
55
|
+
@nec_long = r.position_long
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def aggregate_ascent_descent
|
62
|
+
@total_ascent = @total_descent = 0
|
63
|
+
altitude = nil
|
64
|
+
|
65
|
+
@records.each do |r|
|
66
|
+
if altitude
|
67
|
+
if r.altitude < altitude
|
68
|
+
@total_descent += (altitude - r.altitude)
|
69
|
+
else
|
70
|
+
@total_ascent += (r.altitude - altitude)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
altitude = r.altitude
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def aggregate_speed_distance
|
78
|
+
@max_speed = 0
|
79
|
+
first_distance, last_distance = nil, nil
|
80
|
+
|
81
|
+
@records.each do |r|
|
82
|
+
@max_speed = r.speed if r.speed && @max_speed < r.speed
|
83
|
+
|
84
|
+
first_distance = r.distance if first_distance.nil? && r.distance
|
85
|
+
last_distance = r.distance if r.distance
|
86
|
+
end
|
87
|
+
|
88
|
+
@total_distance = last_distance && first_distance ?
|
89
|
+
last_distance - first_distance : 0
|
90
|
+
@avg_speed = @total_distance.to_f / @total_elapsed_time
|
91
|
+
end
|
92
|
+
|
93
|
+
def aggregate_heart_rate
|
94
|
+
total_heart_beats = 0
|
95
|
+
@max_heart_rate = 0
|
96
|
+
last_timestamp = @start_time
|
97
|
+
|
98
|
+
@records.each do |r|
|
99
|
+
if r.heart_rate
|
100
|
+
delta_t = last_timestamp ? r.timestamp - last_timestamp : nil
|
101
|
+
total_heart_beats += (r.heart_rate / 60.0) * delta_t if delta_t
|
102
|
+
if r.heart_rate > @max_heart_rate
|
103
|
+
@max_heart_rate = r.heart_rate
|
104
|
+
end
|
105
|
+
end
|
106
|
+
last_timestamp = r.timestamp
|
107
|
+
end
|
108
|
+
@avg_heart_rate = (total_heart_beats.to_f / @total_elapsed_time * 60).to_i
|
109
|
+
end
|
110
|
+
|
111
|
+
def aggregate_strides
|
112
|
+
@total_strides = 0
|
113
|
+
@max_running_cadence = 0
|
114
|
+
last_timestamp = @start_time
|
115
|
+
|
116
|
+
@records.each do |r|
|
117
|
+
delta_t = last_timestamp ? r.timestamp - last_timestamp : nil
|
118
|
+
|
119
|
+
if delta_t && (run_cadence = r.run_cadence)
|
120
|
+
@total_strides += (run_cadence / 60.0) * delta_t
|
121
|
+
end
|
122
|
+
if run_cadence && run_cadence > @max_running_cadence
|
123
|
+
@max_running_cadence = run_cadence.to_i
|
124
|
+
end
|
125
|
+
|
126
|
+
last_timestamp = r.timestamp
|
127
|
+
end
|
128
|
+
@avg_running_cadence, @avg_fractional_cadence =
|
129
|
+
(@total_strides.to_f / @total_timer_time * (60 / 2)).divmod(1)
|
130
|
+
@total_strides = @total_strides.to_i
|
131
|
+
end
|
132
|
+
|
133
|
+
def aggregate_vertical_oscillation
|
134
|
+
total_vertical_oscillation = 0.0
|
135
|
+
vertical_oscillation_count = 0
|
136
|
+
|
137
|
+
@records.each do |r|
|
138
|
+
if r.vertical_oscillation
|
139
|
+
total_vertical_oscillation += r.vertical_oscillation
|
140
|
+
vertical_oscillation_count += 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
@avg_vertical_oscillation = total_vertical_oscillation /
|
145
|
+
vertical_oscillation_count
|
146
|
+
end
|
147
|
+
|
148
|
+
def aggregate_stance_time
|
149
|
+
total_stance_time = 0.0
|
150
|
+
total_stance_time_percent = 0.0
|
151
|
+
stance_time_count = 0
|
152
|
+
stance_time_percent_count = 0
|
153
|
+
|
154
|
+
@records.each do |r|
|
155
|
+
if r.stance_time
|
156
|
+
total_stance_time += r.stance_time
|
157
|
+
stance_time_count += 1
|
158
|
+
end
|
159
|
+
if r.stance_time_percent
|
160
|
+
total_stance_time_percent += r.stance_time_percent
|
161
|
+
stance_time_percent_count += 1
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
@avg_stance_time = total_stance_time / stance_time_count
|
166
|
+
@avg_stance_time_percent = total_stance_time_percent /
|
167
|
+
stance_time_percent_count
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
data/lib/fit4ruby/Session.rb
CHANGED
@@ -12,23 +12,45 @@
|
|
12
12
|
|
13
13
|
require 'fit4ruby/Converters'
|
14
14
|
require 'fit4ruby/FitDataRecord'
|
15
|
+
require 'fit4ruby/RecordAggregator'
|
15
16
|
|
16
17
|
module Fit4Ruby
|
17
18
|
|
19
|
+
# The Session objects correspond to the session FIT messages. They hold
|
20
|
+
# accumlated data for a set of Lap objects.
|
18
21
|
class Session < FitDataRecord
|
19
22
|
|
20
|
-
include
|
23
|
+
include RecordAggregator
|
21
24
|
|
22
25
|
attr_reader :laps
|
23
26
|
|
27
|
+
# Create a new Session object.
|
28
|
+
# @param laps [Array of Laps] Laps to associate with the Session.
|
29
|
+
# @param first_lap_index [Fixnum] Index of the first Lap in this Session.
|
30
|
+
# @param field_values [Hash] Hash that provides initial values for certain
|
31
|
+
# fields.
|
24
32
|
def initialize(laps, first_lap_index, field_values)
|
25
33
|
super('session')
|
34
|
+
@meta_field_units['avg_stride_length'] = 'm'
|
26
35
|
@laps = laps
|
36
|
+
@records = []
|
37
|
+
@laps.each { |lap| @records += lap.records }
|
27
38
|
@first_lap_index = first_lap_index
|
28
39
|
@num_laps = @laps.length
|
40
|
+
|
41
|
+
if @records.first
|
42
|
+
# Or to the timestamp of the first record.
|
43
|
+
@start_time = @records.first.timestamp
|
44
|
+
if @records.last
|
45
|
+
@total_elapsed_time = @records.last.timestamp - @start_time
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
29
49
|
set_field_values(field_values)
|
30
50
|
end
|
31
51
|
|
52
|
+
# Perform some basic consistency and logical checks on the object. Errors
|
53
|
+
# are reported via the Log object.
|
32
54
|
def check(activity)
|
33
55
|
unless @first_lap_index
|
34
56
|
Log.error 'first_lap_index is not set'
|
@@ -47,33 +69,16 @@ module Fit4Ruby
|
|
47
69
|
@laps.each { |l| l.check }
|
48
70
|
end
|
49
71
|
|
50
|
-
|
51
|
-
|
52
|
-
@
|
53
|
-
@laps.each do |lap|
|
54
|
-
lap.aggregate
|
55
|
-
@total_distance += lap.total_distance if lap.total_distance
|
56
|
-
@total_elapsed_time += lap.total_elapsed_time if lap.total_elapsed_time
|
57
|
-
end
|
58
|
-
if (l = @laps[0])
|
59
|
-
@start_time = l.start_time
|
60
|
-
@start_position_lat = l.start_position_lat
|
61
|
-
@start_position_long = l.start_position_long
|
62
|
-
end
|
63
|
-
if (l = @laps[-1])
|
64
|
-
@end_position_lat = l.end_position_lat
|
65
|
-
@end_position_long = l.end_position_long
|
66
|
-
end
|
67
|
-
|
68
|
-
if @total_distance && @total_elapsed_time
|
69
|
-
@avg_speed = @total_distance / @total_elapsed_time
|
70
|
-
end
|
72
|
+
# Return true if the session contains geographical location data.
|
73
|
+
def has_geo_data?
|
74
|
+
@swc_long && @swc_lat && @nec_long && nec_lat
|
71
75
|
end
|
72
76
|
|
77
|
+
# Compute the average stride length for this Session.
|
73
78
|
def avg_stride_length
|
74
79
|
return nil unless @total_strides
|
75
80
|
|
76
|
-
@total_distance / @total_strides
|
81
|
+
@total_distance / (@total_strides * 2.0)
|
77
82
|
end
|
78
83
|
|
79
84
|
end
|
data/lib/fit4ruby/UserProfile.rb
CHANGED
@@ -14,8 +14,12 @@ require 'fit4ruby/FitDataRecord'
|
|
14
14
|
|
15
15
|
module Fit4Ruby
|
16
16
|
|
17
|
+
# This class corresponds to the user_profile FIT message.
|
17
18
|
class UserProfile < FitDataRecord
|
18
19
|
|
20
|
+
# Create a new UserProfile object.
|
21
|
+
# @param field_values [Hash] Hash that provides initial values for certain
|
22
|
+
# fields.
|
19
23
|
def initialize(field_values = {})
|
20
24
|
super('user_profile')
|
21
25
|
set_field_values(field_values)
|
data/lib/fit4ruby/version.rb
CHANGED
data/spec/FitFile_spec.rb
CHANGED
@@ -32,7 +32,7 @@ describe Fit4Ruby do
|
|
32
32
|
:position_lat => 51.5512 - mins * 0.0008,
|
33
33
|
:position_long => 11.647 + mins * 0.002,
|
34
34
|
:distance => 200.0 * mins,
|
35
|
-
:altitude => 100 + mins * 0.5,
|
35
|
+
:altitude => (100 + mins * 0.5).to_i,
|
36
36
|
:speed => 3.1,
|
37
37
|
:vertical_oscillation => 9 + mins * 0.02,
|
38
38
|
:stance_time => 235.0 * mins * 0.01,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fit4ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Schlaeger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-08-
|
11
|
+
date: 2014-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bindata
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- lib/fit4ruby/Log.rb
|
107
107
|
- lib/fit4ruby/PersonalRecords.rb
|
108
108
|
- lib/fit4ruby/Record.rb
|
109
|
+
- lib/fit4ruby/RecordAggregator.rb
|
109
110
|
- lib/fit4ruby/Session.rb
|
110
111
|
- lib/fit4ruby/UserProfile.rb
|
111
112
|
- lib/fit4ruby/version.rb
|
@@ -140,3 +141,4 @@ specification_version: 4
|
|
140
141
|
summary: Library to read GARMIN FIT files.
|
141
142
|
test_files:
|
142
143
|
- spec/FitFile_spec.rb
|
144
|
+
has_rdoc:
|