fit4ruby 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/fit4ruby/Activity.rb +37 -14
- data/lib/fit4ruby/DataSources.rb +35 -0
- data/lib/fit4ruby/DeviceInfo.rb +36 -1
- data/lib/fit4ruby/Event.rb +14 -1
- data/lib/fit4ruby/FileNameCoder.rb +67 -0
- data/lib/fit4ruby/FitDataRecord.rb +10 -2
- data/lib/fit4ruby/FitDefinition.rb +1 -1
- data/lib/fit4ruby/FitFile.rb +4 -4
- data/lib/fit4ruby/GeoMath.rb +55 -0
- data/lib/fit4ruby/GlobalFitDictionaries.rb +1 -1
- data/lib/fit4ruby/GlobalFitMessage.rb +1 -1
- data/lib/fit4ruby/GlobalFitMessages.rb +9 -9
- data/lib/fit4ruby/Lap.rb +8 -5
- data/lib/fit4ruby/Log.rb +25 -11
- data/lib/fit4ruby/Session.rb +4 -5
- data/lib/fit4ruby/version.rb +1 -1
- data/spec/FileNameCoder_spec.rb +61 -0
- data/spec/FitFile_spec.rb +33 -8
- data/spec/GeoMath_spec.rb +34 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f651054b5f16738a2ca5fc4146cdb817c4ef102
|
4
|
+
data.tar.gz: b1af399d6335bcf932dfdeb3b35897191bfc6290
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 773e7c729a6ecae371b03269da9da1312f9ca7da35f3e14e6cc6e2c8429955d5f4ea0f965a975bde69368853d9a7555280f4da9a630dd8e1525f827e090b06d1
|
7
|
+
data.tar.gz: 2e4ea74d55a4496977133a15e0ccb5bf0902b4104c32587e783c3427c175cc123c70a498a8e0b0a56cc28451e3fb95b6c0a509b9b28431ff376194b2a94c0932
|
data/lib/fit4ruby/Activity.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Activity.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
|
@@ -14,6 +14,7 @@ require 'fit4ruby/FitDataRecord'
|
|
14
14
|
require 'fit4ruby/FileId'
|
15
15
|
require 'fit4ruby/FileCreator'
|
16
16
|
require 'fit4ruby/DeviceInfo'
|
17
|
+
require 'fit4ruby/DataSources'
|
17
18
|
require 'fit4ruby/UserProfile'
|
18
19
|
require 'fit4ruby/Session'
|
19
20
|
require 'fit4ruby/Lap'
|
@@ -28,8 +29,9 @@ module Fit4Ruby
|
|
28
29
|
# equivalents of the message record structures used in the FIT file.
|
29
30
|
class Activity < FitDataRecord
|
30
31
|
|
31
|
-
attr_accessor :file_id, :file_creator, :device_infos, :
|
32
|
-
:sessions, :laps, :records,
|
32
|
+
attr_accessor :file_id, :file_creator, :device_infos, :data_sources,
|
33
|
+
:user_profiles, :sessions, :laps, :records,
|
34
|
+
:events, :personal_records
|
33
35
|
|
34
36
|
# Create a new Activity object.
|
35
37
|
# @param field_values [Hash] A Hash that provides initial values for
|
@@ -42,6 +44,7 @@ module Fit4Ruby
|
|
42
44
|
@file_id = FileId.new
|
43
45
|
@file_creator = FileCreator.new
|
44
46
|
@device_infos = []
|
47
|
+
@data_sources = []
|
45
48
|
@user_profiles = []
|
46
49
|
@events = []
|
47
50
|
@sessions = []
|
@@ -61,23 +64,25 @@ module Fit4Ruby
|
|
61
64
|
# objects. Any errors will be reported via the Log object.
|
62
65
|
def check
|
63
66
|
unless @timestamp && @timestamp >= Time.parse('1990-01-01T00:00:00+00:00')
|
64
|
-
Log.
|
67
|
+
Log.fatal "Activity has no valid timestamp"
|
65
68
|
end
|
66
69
|
unless @total_timer_time
|
67
|
-
Log.
|
70
|
+
Log.fatal "Activity has no valid total_timer_time"
|
68
71
|
end
|
72
|
+
unless @device_infos.length > 0
|
73
|
+
Log.fatal "Activity must have at least one device_info section"
|
74
|
+
end
|
75
|
+
@device_infos.each.with_index { |d, index| d.check(index) }
|
69
76
|
unless @num_sessions == @sessions.count
|
70
|
-
Log.
|
77
|
+
Log.fatal "Activity record requires #{@num_sessions}, but "
|
71
78
|
"#{@sessions.length} session records were found in the "
|
72
79
|
"FIT file."
|
73
80
|
end
|
74
|
-
@sessions.each { |s| s.check(self) }
|
75
81
|
# Laps must have a consecutively growing message index.
|
76
82
|
@laps.each.with_index do |lap, index|
|
77
|
-
|
78
|
-
Log.error "Lap #{index} has wrong message_index #{lap.message_index}"
|
79
|
-
end
|
83
|
+
lap.check(index)
|
80
84
|
end
|
85
|
+
@sessions.each { |s| s.check(self) }
|
81
86
|
end
|
82
87
|
|
83
88
|
# Convenience method that aggregates all the distances from the included
|
@@ -196,8 +201,8 @@ module Fit4Ruby
|
|
196
201
|
@file_id.write(io, id_mapper)
|
197
202
|
@file_creator.write(io, id_mapper)
|
198
203
|
|
199
|
-
(@device_infos + @
|
200
|
-
@records + @personal_records).sort.each do |s|
|
204
|
+
(@device_infos + @data_sources + @user_profiles + @events +
|
205
|
+
@sessions + @laps + @records + @personal_records).sort.each do |s|
|
201
206
|
s.write(io, id_mapper)
|
202
207
|
end
|
203
208
|
super
|
@@ -229,6 +234,14 @@ module Fit4Ruby
|
|
229
234
|
new_fit_data_record('device_info', field_values)
|
230
235
|
end
|
231
236
|
|
237
|
+
# Add a new SourceData to the Activity.
|
238
|
+
# @param field_values [Hash] A Hash that provides initial values for
|
239
|
+
# certain fields of the FitDataRecord.
|
240
|
+
# @return [SourceData]
|
241
|
+
def new_data_sources(field_values = {})
|
242
|
+
new_fit_data_record('data_sources', field_values)
|
243
|
+
end
|
244
|
+
|
232
245
|
# Add a new UserProfile to the Activity.
|
233
246
|
# @param field_values [Hash] A Hash that provides initial values for
|
234
247
|
# certain fields of the FitDataRecord.
|
@@ -291,7 +304,9 @@ module Fit4Ruby
|
|
291
304
|
def ==(a)
|
292
305
|
super(a) && @file_id == a.file_id &&
|
293
306
|
@file_creator == a.file_creator &&
|
294
|
-
@device_infos == a.device_infos &&
|
307
|
+
@device_infos == a.device_infos &&
|
308
|
+
@data_sources == a.data_sources &&
|
309
|
+
@user_profiles == a.user_profiles &&
|
295
310
|
@events == a.events &&
|
296
311
|
@sessions == a.sessions && personal_records == a.personal_records
|
297
312
|
end
|
@@ -310,14 +325,21 @@ module Fit4Ruby
|
|
310
325
|
@file_creator = (record = FileCreator.new(field_values))
|
311
326
|
when 'device_info'
|
312
327
|
@device_infos << (record = DeviceInfo.new(field_values))
|
328
|
+
when 'data_sources'
|
329
|
+
@data_sources << (record = DataSources.new(field_values))
|
313
330
|
when 'user_profile'
|
314
331
|
@user_profiles << (record = UserProfile.new(field_values))
|
315
332
|
when 'event'
|
316
333
|
@events << (record = Event.new(field_values))
|
317
334
|
when 'session'
|
318
335
|
unless @cur_lap_records.empty?
|
336
|
+
# Copy selected fields from section to lap.
|
337
|
+
lap_field_values = {}
|
338
|
+
[ :timestamp, :sport ].each do |f|
|
339
|
+
lap_field_values[f] = field_values[f] if field_values.include?(f)
|
340
|
+
end
|
319
341
|
# Ensure that all previous records have been assigned to a lap.
|
320
|
-
record = create_new_lap(
|
342
|
+
record = create_new_lap(lap_field_values)
|
321
343
|
end
|
322
344
|
@num_sessions += 1
|
323
345
|
@sessions << (record = Session.new(@cur_session_laps, @lap_counter,
|
@@ -341,6 +363,7 @@ module Fit4Ruby
|
|
341
363
|
|
342
364
|
def create_new_lap(field_values)
|
343
365
|
lap = Lap.new(@cur_lap_records, @laps.last, field_values)
|
366
|
+
lap.message_index = @lap_counter - 1
|
344
367
|
@lap_counter += 1
|
345
368
|
@cur_session_laps << lap
|
346
369
|
@laps << lap
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = DataSources.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 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/FitDataRecord'
|
14
|
+
|
15
|
+
module Fit4Ruby
|
16
|
+
|
17
|
+
# The DataSources objects are generated by message 22. This message type is
|
18
|
+
# not documented by Garmin and hence the message name and all field names
|
19
|
+
# and their interpretation are guessed. Unless this message gets officially
|
20
|
+
# documented, all names are subject to change. Even minor version changes of
|
21
|
+
# this library can break the API for this message type.
|
22
|
+
class DataSources < FitDataRecord
|
23
|
+
|
24
|
+
def initialize(field_values = {})
|
25
|
+
super('data_sources')
|
26
|
+
set_field_values(field_values)
|
27
|
+
end
|
28
|
+
|
29
|
+
def check(index)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
data/lib/fit4ruby/DeviceInfo.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = DeviceInfo.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
|
@@ -21,6 +21,41 @@ module Fit4Ruby
|
|
21
21
|
set_field_values(field_values)
|
22
22
|
end
|
23
23
|
|
24
|
+
# Ensure that FitDataRecords have a deterministic sequence. Device infos
|
25
|
+
# are sorted by device_index.
|
26
|
+
def <=>(fdr)
|
27
|
+
@timestamp == fdr.timestamp ?
|
28
|
+
@message.name == fdr.message.name ?
|
29
|
+
@device_index <=> fdr.device_index :
|
30
|
+
RecordOrder.index(@message.name) <=>
|
31
|
+
RecordOrder.index(fdr.message.name) :
|
32
|
+
@timestamp <=> fdr.timestamp
|
33
|
+
end
|
34
|
+
|
35
|
+
def check(index)
|
36
|
+
unless @device_index
|
37
|
+
Log.fatal 'device info record must have a device_index'
|
38
|
+
end
|
39
|
+
if @device_index == 0
|
40
|
+
unless @manufacturer
|
41
|
+
Log.fatal 'device info record 0 must have a manufacturer field set'
|
42
|
+
end
|
43
|
+
if @manufacturer == 'garmin'
|
44
|
+
unless @garmin_product
|
45
|
+
Log.fatal 'device info record 0 must have a garman_product ' +
|
46
|
+
'field set'
|
47
|
+
end
|
48
|
+
else
|
49
|
+
unless @product
|
50
|
+
Log.fatal 'device info record 0 must have a product field set'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
if @serial_number.nil?
|
54
|
+
Log.fatal 'device info record 0 must have a serial number set'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
24
59
|
end
|
25
60
|
|
26
61
|
end
|
data/lib/fit4ruby/Event.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Event.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
|
@@ -21,6 +21,19 @@ module Fit4Ruby
|
|
21
21
|
set_field_values(field_values)
|
22
22
|
end
|
23
23
|
|
24
|
+
# Ensure that FitDataRecords have a deterministic sequence. Events are
|
25
|
+
# sorted by event_type and then event.
|
26
|
+
def <=>(fdr)
|
27
|
+
@timestamp == fdr.timestamp ?
|
28
|
+
@message.name == fdr.message.name ?
|
29
|
+
@event_type == fdr.event_type ?
|
30
|
+
@event <=> fdr.event :
|
31
|
+
@event_type <=> fdr.event_type :
|
32
|
+
RecordOrder.index(@message.name) <=>
|
33
|
+
RecordOrder.index(fdr.message.name) :
|
34
|
+
@timestamp <=> fdr.timestamp
|
35
|
+
end
|
36
|
+
|
24
37
|
end
|
25
38
|
|
26
39
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FileNameCoder.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
|
+
# This class provides encoder and decoder for the FIT file names typically
|
16
|
+
# used for activies and monitor data files.
|
17
|
+
class FileNameCoder
|
18
|
+
|
19
|
+
CodeBook = 0.upto(9).map{ |i| (?0.ord + i).chr} +
|
20
|
+
0.upto(25).map{ |i|(?A.ord + i).chr}
|
21
|
+
|
22
|
+
# Convert a Time to a corresponding FIT file name.
|
23
|
+
# @param [Time] time stamp
|
24
|
+
# @return [String] FIT file name with extension '.FIT'
|
25
|
+
def FileNameCoder::encode(time)
|
26
|
+
utc = time.utc
|
27
|
+
if (year = utc.year) < 2010 || year > 2033
|
28
|
+
raise ArgumentError, "Year must be between 2010 and 2033"
|
29
|
+
end
|
30
|
+
year = CodeBook[year - 2010]
|
31
|
+
month = CodeBook[utc.month]
|
32
|
+
day = CodeBook[utc.day]
|
33
|
+
hour = CodeBook[utc.hour]
|
34
|
+
minutes = "%02d" % utc.min
|
35
|
+
seconds = "%02d" % utc.sec
|
36
|
+
|
37
|
+
year + month + day + hour + minutes + seconds + '.FIT'
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert a FIT file name into the corresponding Time value.
|
41
|
+
# @param [String] FIT file name. This can be a full path name but must end
|
42
|
+
# with a '.FIT' extension.
|
43
|
+
# @return [Time] corresponding Time value
|
44
|
+
def FileNameCoder::decode(file_name)
|
45
|
+
base = File.basename(file_name.upcase)
|
46
|
+
unless /\A[0-9A-Z]{4}[0-9]{4}\.FIT\z/ =~ base
|
47
|
+
raise ArgumentError, "#{file_name} is not a valid FIT file name"
|
48
|
+
end
|
49
|
+
|
50
|
+
year = 2010 + CodeBook.index(base[0])
|
51
|
+
month = CodeBook.index(base[1])
|
52
|
+
day = CodeBook.index(base[2])
|
53
|
+
hour = CodeBook.index(base[3])
|
54
|
+
minutes = base[4,2].to_i
|
55
|
+
seconds = base[6,2].to_i
|
56
|
+
if month == 0 || month > 12 || day == 0 || day > 31 ||
|
57
|
+
hour >= 24 || minutes >= 60 || seconds >= 60
|
58
|
+
raise ArgumentError, "#{file_name} is not a valid FIT file name"
|
59
|
+
end
|
60
|
+
|
61
|
+
Time.new(year, month, day, hour, minutes, seconds, "+00:00")
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitDataRecord.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
|
@@ -19,6 +19,11 @@ module Fit4Ruby
|
|
19
19
|
|
20
20
|
include Converters
|
21
21
|
|
22
|
+
RecordOrder = [ 'user_profile', 'device_info', 'data_sources', 'event',
|
23
|
+
'record', 'lap', 'session', 'personal_records' ]
|
24
|
+
|
25
|
+
attr_reader :message
|
26
|
+
|
22
27
|
def initialize(record_id)
|
23
28
|
@message = GlobalFitMessages.find_by_name(record_id)
|
24
29
|
|
@@ -91,7 +96,10 @@ module Fit4Ruby
|
|
91
96
|
end
|
92
97
|
|
93
98
|
def <=>(fdr)
|
94
|
-
@timestamp
|
99
|
+
@timestamp == fdr.timestamp ?
|
100
|
+
RecordOrder.index(@message.name) <=>
|
101
|
+
RecordOrder.index(fdr.message.name) :
|
102
|
+
@timestamp <=> fdr.timestamp
|
95
103
|
end
|
96
104
|
|
97
105
|
def write(io, id_mapper)
|
data/lib/fit4ruby/FitFile.rb
CHANGED
@@ -33,7 +33,7 @@ module Fit4Ruby
|
|
33
33
|
begin
|
34
34
|
io = ::File.open(file_name, 'rb')
|
35
35
|
rescue StandardError => e
|
36
|
-
Log.
|
36
|
+
Log.fatal "Cannot open FIT file '#{file_name}': #{e.message}"
|
37
37
|
end
|
38
38
|
header = FitHeader.read(io)
|
39
39
|
header.check
|
@@ -66,7 +66,7 @@ module Fit4Ruby
|
|
66
66
|
begin
|
67
67
|
io = ::File.open(file_name, 'wb+')
|
68
68
|
rescue StandardError => e
|
69
|
-
Log.
|
69
|
+
Log.fatal "Cannot open FIT file '#{file_name}': #{e.message}"
|
70
70
|
end
|
71
71
|
|
72
72
|
# Create a header object, but don't yet write it into the file.
|
@@ -104,8 +104,8 @@ module Fit4Ruby
|
|
104
104
|
io.seek(start_pos)
|
105
105
|
|
106
106
|
unless crc == crc_ref
|
107
|
-
Log.
|
108
|
-
|
107
|
+
Log.fatal "Checksum error in file '#{@file_name}'. " +
|
108
|
+
"Computed #{"%04X" % crc} instead of #{"%04X" % crc_ref}."
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = Activity.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 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 GeoMath
|
16
|
+
|
17
|
+
# This method uses the ellipsoidal earth projected to a plane formula
|
18
|
+
# prescribed by the FCC in 47 CFR 73.208 for distances not exceeding 475
|
19
|
+
# km /295 miles.
|
20
|
+
# @param p1_lat Latitude of the first point in polar degrees
|
21
|
+
# @param p1_lon Longitude of the first point in polar degrees
|
22
|
+
# @param p2_lat Latitude of the second point in polar degrees
|
23
|
+
# @param p2_lon Longitude of the second point in polar degrees
|
24
|
+
# @return Distance in meters
|
25
|
+
def GeoMath.distance(p1_lat, p1_lon, p2_lat, p2_lon)
|
26
|
+
# Difference in latitude and longitude
|
27
|
+
delta_lat = p2_lat - p1_lat
|
28
|
+
delta_lon = p2_lon - p1_lon
|
29
|
+
|
30
|
+
# Mean latitude
|
31
|
+
mean_lat = (p1_lat + p2_lat) / 2
|
32
|
+
|
33
|
+
# kilometers per degree of latitude difference
|
34
|
+
k1 = 111.13209 - 0.56606 * cos(2 * mean_lat) +
|
35
|
+
0.00120 * cos(4 * mean_lat)
|
36
|
+
# kilometers per degree of longitude difference
|
37
|
+
k2 = 111.41513 * cos(mean_lat) -
|
38
|
+
0.09455 * cos(3 * mean_lat) +
|
39
|
+
0.00012 * cos(5 * mean_lat)
|
40
|
+
|
41
|
+
Math.sqrt(((k1 * delta_lat)) ** 2 + (k2 * delta_lon) ** 2) * 1000.0
|
42
|
+
end
|
43
|
+
|
44
|
+
def GeoMath.cos(deg)
|
45
|
+
Math.cos(deg_to_rad(deg))
|
46
|
+
end
|
47
|
+
|
48
|
+
def GeoMath.deg_to_rad(deg)
|
49
|
+
deg * Math::PI / 180
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = GlobalFitMessage.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
|
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = GlobalFitMessages.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
|
@@ -255,16 +255,16 @@ module Fit4Ruby
|
|
255
255
|
field 253, 'uint32', 'timestamp', :type => 'date_time'
|
256
256
|
|
257
257
|
# Possibly which device is used as metering source.
|
258
|
-
# Not documented in FIT SDK, so
|
259
|
-
message 22, '
|
260
|
-
field 0, 'uint8', '
|
261
|
-
field 1, 'uint8', '
|
258
|
+
# Not documented in FIT SDK, so the field names are all guesses right now.
|
259
|
+
message 22, 'data_sources'
|
260
|
+
field 0, 'uint8', 'distance'
|
261
|
+
field 1, 'uint8', 'speed'
|
262
262
|
field 2, 'uint8', 'cadence'
|
263
|
-
field 3, 'uint8', '
|
263
|
+
field 3, 'uint8', 'elevation'
|
264
264
|
field 4, 'uint8', 'heart_rate'
|
265
|
-
field 5, 'enum', '
|
266
|
-
field 6, 'uint8', '
|
267
|
-
field 14, 'uint8', '
|
265
|
+
field 5, 'enum', 'mode' # 0 or 3 seen, unknown meaning
|
266
|
+
field 6, 'uint8', 'power' # First found in FR920XT
|
267
|
+
field 14, 'uint8', 'calories' # First found in FR920XT
|
268
268
|
field 253, 'uint32', 'timestamp', :type => 'date_time'
|
269
269
|
|
270
270
|
message 23, 'device_info'
|
data/lib/fit4ruby/Lap.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Lap.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
|
@@ -43,17 +43,20 @@ module Fit4Ruby
|
|
43
43
|
set_field_values(field_values)
|
44
44
|
end
|
45
45
|
|
46
|
-
def check
|
46
|
+
def check(index)
|
47
|
+
unless @message_index == index
|
48
|
+
Log.fatal "message_index must be #{index}, not #{@message_index}"
|
49
|
+
end
|
47
50
|
ts = Time.parse('1989-12-31')
|
48
51
|
distance = nil
|
49
52
|
@records.each do |r|
|
50
|
-
Log.
|
53
|
+
Log.fatal "Record has no timestamp" unless r.timestamp
|
51
54
|
if r.timestamp < ts
|
52
|
-
Log.
|
55
|
+
Log.fatal "Record has earlier timestamp than previous record"
|
53
56
|
end
|
54
57
|
if r.distance
|
55
58
|
if distance && r.distance < distance
|
56
|
-
Log.
|
59
|
+
Log.fatal "Record has smaller distance than previous record"
|
57
60
|
end
|
58
61
|
distance = r.distance
|
59
62
|
end
|
data/lib/fit4ruby/Log.rb
CHANGED
@@ -10,7 +10,9 @@
|
|
10
10
|
# published by the Free Software Foundation.
|
11
11
|
#
|
12
12
|
|
13
|
+
require 'monitor'
|
13
14
|
require 'logger'
|
15
|
+
require 'singleton'
|
14
16
|
|
15
17
|
module Fit4Ruby
|
16
18
|
|
@@ -18,24 +20,36 @@ module Fit4Ruby
|
|
18
20
|
# errors.
|
19
21
|
class Error < StandardError ; end
|
20
22
|
|
21
|
-
|
23
|
+
# The ILogger class is a singleton that provides a common logging mechanism
|
24
|
+
# to all objects. It exposes essentially the same interface as the Logger
|
25
|
+
# class, just as a singleton and with some additional methods like 'fatal'
|
26
|
+
# and 'cricital'.
|
27
|
+
class ILogger < Monitor
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
|
29
|
+
include Singleton
|
30
|
+
|
31
|
+
@@logger = Logger.new(STDOUT)
|
32
|
+
|
33
|
+
# Pass all calls to unknown methods to the @@logger object.
|
34
|
+
def method_missing(method, *args, &block)
|
35
|
+
@@logger.send(method, *args, &block)
|
26
36
|
end
|
27
37
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
38
|
+
# Make it properly introspectable.
|
39
|
+
def respond_to?(method, include_private = false)
|
40
|
+
@@logger.respond_to?(method)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Print an error message via the Logger and raise and Fit4Ruby::Error.
|
44
|
+
# code 1.
|
45
|
+
def fatal(msg)
|
46
|
+
@@logger.error(msg)
|
47
|
+
raise Error, msg
|
34
48
|
end
|
35
49
|
|
36
50
|
end
|
37
51
|
|
38
|
-
Log = ILogger.
|
52
|
+
Log = ILogger.instance
|
39
53
|
Log.level = Logger::WARN
|
40
54
|
Log.formatter = proc do |severity, time, progname, msg|
|
41
55
|
msg + "\n"
|
data/lib/fit4ruby/Session.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = Session.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
|
@@ -54,20 +54,19 @@ module Fit4Ruby
|
|
54
54
|
# are reported via the Log object.
|
55
55
|
def check(activity)
|
56
56
|
unless @first_lap_index
|
57
|
-
Log.
|
57
|
+
Log.fatal 'first_lap_index is not set'
|
58
58
|
end
|
59
59
|
unless @num_laps
|
60
|
-
Log.
|
60
|
+
Log.fatal 'num_laps is not set'
|
61
61
|
end
|
62
62
|
@first_lap_index.upto(@first_lap_index - @num_laps) do |i|
|
63
63
|
if (lap = activity.lap[i])
|
64
64
|
@laps << lap
|
65
65
|
else
|
66
|
-
Log.
|
66
|
+
Log.fatal "Session references lap #{i} which is not contained in "
|
67
67
|
"the FIT file."
|
68
68
|
end
|
69
69
|
end
|
70
|
-
@laps.each { |l| l.check }
|
71
70
|
end
|
72
71
|
|
73
72
|
# Return true if the session contains geographical location data.
|
data/lib/fit4ruby/version.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FileNameCoder_spec.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015 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/FileNameCoder'
|
14
|
+
|
15
|
+
describe Fit4Ruby::FileNameCoder do
|
16
|
+
|
17
|
+
before(:all) do
|
18
|
+
@data = [
|
19
|
+
[ '2015-10-23T21:18:00+0200', '5ANJ1800.FIT' ],
|
20
|
+
[ '2015-10-11T15:06:59+0000', '5ABF0659.FIT' ],
|
21
|
+
[ '2014-04-19T07:31:06+0100', '44J63106.FIT' ],
|
22
|
+
[ '2015-05-07T16:16:50+0000', '557G1650.FIT' ]
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should convert a time stamps to a file names' do
|
27
|
+
@data.each do |t|
|
28
|
+
Fit4Ruby::FileNameCoder.encode(Time.parse(t[0])).should == t[1]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should convert file names to time stamps' do
|
33
|
+
@data.each do |t|
|
34
|
+
Fit4Ruby::FileNameCoder.decode(t[1]).should == Time.parse(t[0]).utc
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should fail to encode dates before 2010' do
|
39
|
+
lambda {
|
40
|
+
Fit4Ruby::FileNameCoder.encode(Time.parse('2009-12-31T00:00'))
|
41
|
+
}.should raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should fail to encode dates after 2033' do
|
45
|
+
lambda {
|
46
|
+
Fit4Ruby::FileNameCoder.encode(Time.parse('2034-01-01T00:00+00:00'))
|
47
|
+
}.should raise_error
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should fail to decode illegal file names' do
|
51
|
+
[ 'A.FIT', '0123ABCD', '5ZNJ1800.FIT',
|
52
|
+
'44063106.FIT', '44W63106.FIT',
|
53
|
+
'557O1650.FIT', '557G6050.FIT', '557G1660.FIT' ].each do |name|
|
54
|
+
lambda {
|
55
|
+
Fit4Ruby::FileNameCoder.decode(name)
|
56
|
+
}.should raise_error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
data/spec/FitFile_spec.rb
CHANGED
@@ -15,6 +15,7 @@ require 'fit4ruby'
|
|
15
15
|
describe Fit4Ruby do
|
16
16
|
|
17
17
|
before(:each) do
|
18
|
+
ts = Time.now
|
18
19
|
a = Fit4Ruby::Activity.new
|
19
20
|
a.total_timer_time = 30 * 60.0
|
20
21
|
a.new_user_profile({ :age => 33, :height => 1.78, :weight => 73.0,
|
@@ -22,10 +23,24 @@ describe Fit4Ruby do
|
|
22
23
|
:max_hr => 178 })
|
23
24
|
|
24
25
|
a.new_event({ :event => 'timer', :event_type => 'start_time' })
|
25
|
-
a.new_device_info({ :
|
26
|
-
|
26
|
+
a.new_device_info({ :timestamp => ts,
|
27
|
+
:device_index => 0, :manufacturer => 'garmin',
|
28
|
+
:garmin_product => 'fenix3',
|
29
|
+
:serial_number => 123456789 })
|
30
|
+
a.new_device_info({ :timestamp => ts,
|
31
|
+
:device_index => 1, :manufacturer => 'garmin',
|
32
|
+
:garmin_product => 'fenix3_gps'})
|
33
|
+
a.new_device_info({ :timestamp => ts,
|
34
|
+
:device_index => 2, :manufacturer => 'garmin',
|
35
|
+
:garmin_product => 'hrm_run',
|
27
36
|
:battery_status => 'ok' })
|
28
|
-
|
37
|
+
a.new_device_info({ :timestamp => ts,
|
38
|
+
:device_index => 3, :manufacturer => 'garmin',
|
39
|
+
:garmin_product => 'sdm4',
|
40
|
+
:battery_status => 'ok' })
|
41
|
+
a.new_data_sources({ :timestamp => ts, :distance => 1,
|
42
|
+
:speed => 1, :cadence => 3, :elevation => 1,
|
43
|
+
:heart_rate => 2 })
|
29
44
|
laps = 0
|
30
45
|
0.upto(a.total_timer_time / 60) do |mins|
|
31
46
|
ts += 60
|
@@ -59,11 +74,22 @@ describe Fit4Ruby do
|
|
59
74
|
:event_type => 'marker', :vo2max => 52 })
|
60
75
|
a.new_event({ :timestamp => ts, :event => 'timer',
|
61
76
|
:event_type => 'stop_all' })
|
62
|
-
a.new_device_info({ :timestamp => ts, :device_index => 0,
|
63
|
-
:manufacturer => 'development' })
|
64
77
|
ts += 1
|
65
|
-
a.new_device_info({ :timestamp => ts,
|
66
|
-
:device_index =>
|
78
|
+
a.new_device_info({ :timestamp => ts,
|
79
|
+
:device_index => 0, :manufacturer => 'garmin',
|
80
|
+
:garmin_product => 'fenix3',
|
81
|
+
:serial_number => 123456789 })
|
82
|
+
a.new_device_info({ :timestamp => ts,
|
83
|
+
:device_index => 1, :manufacturer => 'garmin',
|
84
|
+
:garmin_product => 'fenix3_gps'})
|
85
|
+
a.new_device_info({ :timestamp => ts,
|
86
|
+
:device_index => 2, :manufacturer => 'garmin',
|
87
|
+
:garmin_product => 'hrm_run',
|
88
|
+
:battery_status => 'low' })
|
89
|
+
a.new_device_info({ :timestamp => ts,
|
90
|
+
:device_index => 3, :manufacturer => 'garmin',
|
91
|
+
:garmin_product => 'sdm4',
|
92
|
+
:battery_status => 'ok' })
|
67
93
|
ts += 120
|
68
94
|
a.new_event({ :timestamp => ts, :event => 'recovery_hr',
|
69
95
|
:event_type => 'marker', :recovery_hr => 132 })
|
@@ -79,7 +105,6 @@ describe Fit4Ruby do
|
|
79
105
|
File.delete(fit_file) if File.exists?(fit_file)
|
80
106
|
Fit4Ruby.write(fit_file, @activity)
|
81
107
|
File.exists?(fit_file).should be_true
|
82
|
-
puts File.absolute_path(fit_file)
|
83
108
|
|
84
109
|
b = Fit4Ruby.read(fit_file)
|
85
110
|
b.should == @activity
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitFile_spec.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014, 2015 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/GeoMath'
|
14
|
+
|
15
|
+
describe Fit4Ruby::GeoMath do
|
16
|
+
|
17
|
+
it 'should compute a distance between 2 points' do
|
18
|
+
p0_lat = 48.180506536737084
|
19
|
+
p0_lon = 11.611978523433208
|
20
|
+
points = [
|
21
|
+
# latitude, longitude, distance to p0 in meters
|
22
|
+
[ 48.180506536737084, 11.611978523433208, 0.0 ],
|
23
|
+
[ 48.18047543987632, 11.61195664666593, 3.821 ],
|
24
|
+
[ 48.18034409545362, 11.611852794885635, 20.339 ],
|
25
|
+
[ 48.17970883101225, 11.611351054161787, 100.225 ]
|
26
|
+
]
|
27
|
+
points.each do |p|
|
28
|
+
Fit4Ruby::GeoMath.distance(p0_lat, p0_lon,
|
29
|
+
p[0], p[1]).should be_within(0.001).of(p[2])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Schlaeger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bindata
|
@@ -84,10 +84,12 @@ files:
|
|
84
84
|
- lib/fit4ruby.rb
|
85
85
|
- lib/fit4ruby/Activity.rb
|
86
86
|
- lib/fit4ruby/Converters.rb
|
87
|
+
- lib/fit4ruby/DataSources.rb
|
87
88
|
- lib/fit4ruby/DeviceInfo.rb
|
88
89
|
- lib/fit4ruby/Event.rb
|
89
90
|
- lib/fit4ruby/FileCreator.rb
|
90
91
|
- lib/fit4ruby/FileId.rb
|
92
|
+
- lib/fit4ruby/FileNameCoder.rb
|
91
93
|
- lib/fit4ruby/FitDataRecord.rb
|
92
94
|
- lib/fit4ruby/FitDefinition.rb
|
93
95
|
- lib/fit4ruby/FitDefinitionField.rb
|
@@ -99,6 +101,7 @@ files:
|
|
99
101
|
- lib/fit4ruby/FitMessageRecord.rb
|
100
102
|
- lib/fit4ruby/FitRecord.rb
|
101
103
|
- lib/fit4ruby/FitRecordHeader.rb
|
104
|
+
- lib/fit4ruby/GeoMath.rb
|
102
105
|
- lib/fit4ruby/GlobalFitDictList.rb
|
103
106
|
- lib/fit4ruby/GlobalFitDictionaries.rb
|
104
107
|
- lib/fit4ruby/GlobalFitMessage.rb
|
@@ -115,7 +118,9 @@ files:
|
|
115
118
|
- lib/fit4ruby/Software.rb
|
116
119
|
- lib/fit4ruby/UserProfile.rb
|
117
120
|
- lib/fit4ruby/version.rb
|
121
|
+
- spec/FileNameCoder_spec.rb
|
118
122
|
- spec/FitFile_spec.rb
|
123
|
+
- spec/GeoMath_spec.rb
|
119
124
|
- tasks/changelog.rake
|
120
125
|
- tasks/gem.rake
|
121
126
|
- tasks/rdoc.rake
|
@@ -145,5 +150,7 @@ signing_key:
|
|
145
150
|
specification_version: 4
|
146
151
|
summary: Library to read GARMIN FIT files.
|
147
152
|
test_files:
|
153
|
+
- spec/FileNameCoder_spec.rb
|
148
154
|
- spec/FitFile_spec.rb
|
155
|
+
- spec/GeoMath_spec.rb
|
149
156
|
has_rdoc:
|