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