fit4ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +280 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +21 -0
- data/README.md +16 -0
- data/Rakefile +19 -0
- data/fit4ruby.gemspec +30 -0
- data/lib/fit4ruby.rb +26 -0
- data/lib/fit4ruby/Activity.rb +119 -0
- data/lib/fit4ruby/Converters.rb +58 -0
- data/lib/fit4ruby/FitDataRecord.rb +79 -0
- data/lib/fit4ruby/FitDefinition.rb +54 -0
- data/lib/fit4ruby/FitDefinitionField.rb +137 -0
- data/lib/fit4ruby/FitFile.rb +154 -0
- data/lib/fit4ruby/FitFileId.rb +31 -0
- data/lib/fit4ruby/FitFilter.rb +26 -0
- data/lib/fit4ruby/FitHeader.rb +55 -0
- data/lib/fit4ruby/FitMessageIdMapper.rb +53 -0
- data/lib/fit4ruby/FitMessageRecord.rb +77 -0
- data/lib/fit4ruby/FitRecord.rb +80 -0
- data/lib/fit4ruby/FitRecordHeader.rb +39 -0
- data/lib/fit4ruby/GlobalFitDictList.rb +68 -0
- data/lib/fit4ruby/GlobalFitDictionaries.rb +302 -0
- data/lib/fit4ruby/GlobalFitMessage.rb +189 -0
- data/lib/fit4ruby/GlobalFitMessages.rb +235 -0
- data/lib/fit4ruby/Lap.rb +44 -0
- data/lib/fit4ruby/Log.rb +45 -0
- data/lib/fit4ruby/Record.rb +56 -0
- data/lib/fit4ruby/Session.rb +110 -0
- data/lib/fit4ruby/version.rb +3 -0
- data/tasks/changelog.rake +169 -0
- data/tasks/gem.rake +52 -0
- data/tasks/rdoc.rake +14 -0
- data/tasks/test.rake +7 -0
- data/test/FitFile_spec.rb +31 -0
- metadata +137 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = Converters.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 Converters
|
16
|
+
|
17
|
+
def speedToPace(speed)
|
18
|
+
if speed > 0.01
|
19
|
+
pace = 1000.0 / (speed * 60.0)
|
20
|
+
int, dec = pace.divmod 1
|
21
|
+
"#{int}:#{'%02d' % (dec * 60)}"
|
22
|
+
else
|
23
|
+
"-:--"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def secsToHMS(secs)
|
28
|
+
secs = secs.to_i
|
29
|
+
s = secs % 60
|
30
|
+
mins = secs / 60
|
31
|
+
m = mins % 60
|
32
|
+
h = mins / 60
|
33
|
+
"#{h}:#{'%02d' % m}:#{'%02d' % s}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def secsToDHMS(secs)
|
37
|
+
secs = secs.to_i
|
38
|
+
s = secs % 60
|
39
|
+
mins = secs / 60
|
40
|
+
m = mins % 60
|
41
|
+
hours = mins / 60
|
42
|
+
h = hours % 24
|
43
|
+
d = hours / 24
|
44
|
+
"#{d} days #{h}:#{'%02d' % m}:#{'%02d' % s}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def time_to_fit_time(t)
|
48
|
+
(t - Time.parse('1989-12-31')).to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def fit_time_to_time(ft)
|
52
|
+
Time.parse('1989-12-31') + ft.to_i
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitDataRecord.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'fit4ruby/FitMessageIdMapper'
|
14
|
+
require 'fit4ruby/GlobalFitMessages.rb'
|
15
|
+
|
16
|
+
module Fit4Ruby
|
17
|
+
|
18
|
+
class FitDataRecord
|
19
|
+
|
20
|
+
def initialize(record_id)
|
21
|
+
@message = GlobalFitMessages.find_by_name(record_id)
|
22
|
+
@renames = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def rename(fit_var, var)
|
26
|
+
@renames[fit_var] = var
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(io, id_mapper)
|
30
|
+
global_message_number = @message.number
|
31
|
+
|
32
|
+
# Map the global message number to the current local message number.
|
33
|
+
unless (local_message_number = id_mapper.get_local(global_message_number))
|
34
|
+
# If the current dictionary does not contain the global message
|
35
|
+
# number, we need to create a new entry for it. The index in the
|
36
|
+
# dictionary is the local message number.
|
37
|
+
local_message_number = id_mapper.add_global(global_message_number)
|
38
|
+
# Write the definition of the global message number to the file.
|
39
|
+
@message.write(io, local_message_number)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Write data record header.
|
43
|
+
header = FitRecordHeader.new
|
44
|
+
header.normal = 0
|
45
|
+
header.message_type = 0
|
46
|
+
header.local_message_type = local_message_number
|
47
|
+
header.write(io)
|
48
|
+
|
49
|
+
# Create a BinData::Struct object to store the data record.
|
50
|
+
fields = []
|
51
|
+
@message.fields.each do |field_number, field|
|
52
|
+
bin_data_type = FitDefinitionField.fit_type_to_bin_data(field.type)
|
53
|
+
fields << [ bin_data_type, field.name ]
|
54
|
+
end
|
55
|
+
bd = BinData::Struct.new(:endian => :little, :fields => fields)
|
56
|
+
|
57
|
+
# Fill the BinData::Struct object with the values from the corresponding
|
58
|
+
# instance variables.
|
59
|
+
@message.fields.each do |field_number, field|
|
60
|
+
iv = "@#{@renames[field.name] || field.name}"
|
61
|
+
if instance_variable_defined?(iv) &&
|
62
|
+
!(iv_value = instance_variable_get(iv)).nil?
|
63
|
+
value = field.native_to_fit(iv_value)
|
64
|
+
else
|
65
|
+
# If we don't have a corresponding variable or the variable is nil
|
66
|
+
# we write the 'undefined' value instead.
|
67
|
+
value = FitDefinitionField.undefined_value(field.type)
|
68
|
+
end
|
69
|
+
bd[field.name] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
# Write the data record to the file.
|
73
|
+
bd.write(io)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitDefinition.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'bindata'
|
14
|
+
require 'fit4ruby/FitDefinitionField'
|
15
|
+
|
16
|
+
module Fit4Ruby
|
17
|
+
|
18
|
+
class FitDefinition < BinData::Record
|
19
|
+
|
20
|
+
hide :reserved
|
21
|
+
|
22
|
+
uint8 :reserved, :initial_value => 0
|
23
|
+
uint8 :architecture, :initial_value => 0
|
24
|
+
choice :global_message_number, :selection => :architecture do
|
25
|
+
uint16le 0
|
26
|
+
uint16be :default
|
27
|
+
end
|
28
|
+
uint8 :field_count
|
29
|
+
array :fields, :type => FitDefinitionField, :initial_length => :field_count
|
30
|
+
|
31
|
+
def endian
|
32
|
+
architecture.snapshot == 0 ? :little : :big
|
33
|
+
end
|
34
|
+
|
35
|
+
def check
|
36
|
+
fields.each { |f| f.check }
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup(fit_message_definition)
|
40
|
+
fit_message_definition.fields.each do |number, f|
|
41
|
+
fdf = FitDefinitionField.new
|
42
|
+
fdf.field_definition_number = number
|
43
|
+
fdf.set_type(f.type)
|
44
|
+
|
45
|
+
fields << fdf
|
46
|
+
end
|
47
|
+
self.field_count = fields.length
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,137 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitDefinitionField.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'bindata'
|
14
|
+
require 'time'
|
15
|
+
require 'fit4ruby/Log'
|
16
|
+
require 'fit4ruby/GlobalFitMessage'
|
17
|
+
|
18
|
+
module Fit4Ruby
|
19
|
+
|
20
|
+
class FitDefinitionField < BinData::Record
|
21
|
+
|
22
|
+
@@TypeDefs = [
|
23
|
+
# FIT Type, BinData type, undefined value, bytes
|
24
|
+
[ 'enum', 'uint8', 0xFF, 1 ],
|
25
|
+
[ 'sint8', 'int8', 0x7F, 1 ],
|
26
|
+
[ 'uint8', 'uint8', 0xFF, 1 ],
|
27
|
+
[ 'sint16', 'int16', 0x7FFF, 2 ],
|
28
|
+
[ 'uint16', 'uint16', 0xFFFF, 2 ],
|
29
|
+
[ 'sint32', 'int32', 0x7FFFFFFF, 4 ],
|
30
|
+
[ 'uint32', 'uint32', 0xFFFFFFFF, 4 ],
|
31
|
+
[ 'string', 'stringz', '', 0 ],
|
32
|
+
[ 'float32', 'float', 0xFFFFFFFF, 4 ],
|
33
|
+
[ 'float63', 'double', 0xFFFFFFFF, 4 ],
|
34
|
+
[ 'uint8z', 'uint8', 0, 1 ],
|
35
|
+
[ 'uint16z', 'uint16', 0, 2 ],
|
36
|
+
[ 'uint32z', 'uint32', 0, 4 ],
|
37
|
+
[ 'byte', 'uint8', 0xFF, 1 ]
|
38
|
+
]
|
39
|
+
|
40
|
+
hide :reserved
|
41
|
+
|
42
|
+
uint8 :field_definition_number
|
43
|
+
uint8 :byte_count
|
44
|
+
bit1 :endian_ability
|
45
|
+
bit2 :reserved
|
46
|
+
bit5 :base_type_number
|
47
|
+
|
48
|
+
def self.fit_type_to_bin_data(fit_type)
|
49
|
+
entry = @@TypeDefs.find { |e| e[0] == fit_type }
|
50
|
+
raise "Unknown fit type #{fit_type}" unless entry
|
51
|
+
entry[1]
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.undefined_value(fit_type)
|
55
|
+
entry = @@TypeDefs.find { |e| e[0] == fit_type }
|
56
|
+
raise "Unknown fit type #{fit_type}" unless entry
|
57
|
+
entry[2]
|
58
|
+
end
|
59
|
+
|
60
|
+
def init
|
61
|
+
@global_message_number = parent.parent.global_message_number.snapshot
|
62
|
+
@global_message_definition = GlobalFitMessages[@global_message_number]
|
63
|
+
field_number = field_definition_number.snapshot
|
64
|
+
if @global_message_definition &&
|
65
|
+
(field = @global_message_definition.fields[field_number])
|
66
|
+
@name = field.name
|
67
|
+
@type = field.type
|
68
|
+
|
69
|
+
if @type && (td = @@TypeDefs[base_type_number]) && td[0] != @type
|
70
|
+
Log.warn "#{@global_message_number}:#{@name} must be of type " +
|
71
|
+
"#{@type}, not #{td[0]}"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
@name = "field#{field_definition_number.snapshot}"
|
75
|
+
@type = nil
|
76
|
+
Log.warn { "Unknown field number #{field_definition_number} " +
|
77
|
+
"in global message #{@global_message_number}" }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def name
|
82
|
+
init unless @global_message_number
|
83
|
+
@name
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_machine(value)
|
87
|
+
init unless @global_message_number
|
88
|
+
value = nil if value == undefined_value
|
89
|
+
|
90
|
+
field_number = field_definition_number.snapshot
|
91
|
+
if @global_message_definition &&
|
92
|
+
(field = @global_message_definition.fields[field_number])
|
93
|
+
field.to_machine(value)
|
94
|
+
else
|
95
|
+
value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s(value)
|
100
|
+
init unless @global_message_number
|
101
|
+
value = nil if value == undefined_value
|
102
|
+
|
103
|
+
field_number = field_definition_number.snapshot
|
104
|
+
if @global_message_definition &&
|
105
|
+
(field = @global_message_definition.fields[field_number])
|
106
|
+
field.to_s(value)
|
107
|
+
else
|
108
|
+
"[#{value.to_s}]"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def set_type(fit_type)
|
113
|
+
idx = @@TypeDefs.index { |x| x[0] == fit_type }
|
114
|
+
raise "Unknown type #{fit_type}" unless idx
|
115
|
+
self.base_type_number = idx
|
116
|
+
self.byte_count = @@TypeDefs[idx][3]
|
117
|
+
end
|
118
|
+
|
119
|
+
def type(fit_type = false)
|
120
|
+
if @@TypeDefs.length <= base_type_number.snapshot
|
121
|
+
Log.fatal "Unknown FIT Base type #{base_type_number.snapshot}"
|
122
|
+
end
|
123
|
+
|
124
|
+
@@TypeDefs[base_type_number.snapshot][fit_type ? 0 : 1]
|
125
|
+
end
|
126
|
+
|
127
|
+
def undefined_value
|
128
|
+
if @@TypeDefs.length <= base_type_number.snapshot
|
129
|
+
Log.fatal "Unknown FIT Base type #{base_type_number.snapshot}"
|
130
|
+
end
|
131
|
+
|
132
|
+
@@TypeDefs[base_type_number.snapshot][2]
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitFile.rb -- Fit4Ruby - FIT file processing library for Ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
|
7
|
+
#
|
8
|
+
# This program is free software; you can redistribute it and/or modify
|
9
|
+
# it under the terms of version 2 of the GNU General Public License as
|
10
|
+
# published by the Free Software Foundation.
|
11
|
+
#
|
12
|
+
|
13
|
+
require 'fit4ruby/Log'
|
14
|
+
require 'fit4ruby/FitHeader'
|
15
|
+
require 'fit4ruby/FitRecord'
|
16
|
+
require 'fit4ruby/FitFilter'
|
17
|
+
require 'fit4ruby/FitMessageIdMapper'
|
18
|
+
require 'fit4ruby/FitFileId'
|
19
|
+
require 'fit4ruby/GlobalFitMessages'
|
20
|
+
require 'fit4ruby/GlobalFitDictionaries'
|
21
|
+
|
22
|
+
module Fit4Ruby
|
23
|
+
|
24
|
+
class FitFile
|
25
|
+
|
26
|
+
def initialize()
|
27
|
+
@header = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def read(file_name, filter = nil)
|
31
|
+
@file_name = file_name
|
32
|
+
definitions = {}
|
33
|
+
begin
|
34
|
+
io = ::File.open(file_name, 'rb')
|
35
|
+
rescue RuntimeError => e
|
36
|
+
Log.critical("Cannot open FIT file '#{file_name}'", e)
|
37
|
+
end
|
38
|
+
header = FitHeader.read(io)
|
39
|
+
header.check
|
40
|
+
|
41
|
+
check_crc(io, header.end_pos)
|
42
|
+
|
43
|
+
activity = Activity.new
|
44
|
+
# This Array holds the raw data of the records that may be needed to
|
45
|
+
# dump a human readable form of the FIT file.
|
46
|
+
records = []
|
47
|
+
# This hash will hold a counter for each record type. The counter is
|
48
|
+
# incremented each time the corresponding record type is found.
|
49
|
+
record_counters = Hash.new { 0 }
|
50
|
+
while io.pos < header.end_pos
|
51
|
+
record = FitRecord.new(definitions)
|
52
|
+
record.read(io, activity, filter, record_counters)
|
53
|
+
records << record if filter
|
54
|
+
end
|
55
|
+
|
56
|
+
io.close
|
57
|
+
|
58
|
+
header.dump if filter && filter.record_numbers.nil?
|
59
|
+
dump_records(records) if filter
|
60
|
+
|
61
|
+
activity.check
|
62
|
+
activity
|
63
|
+
end
|
64
|
+
|
65
|
+
def write(file_name, activity)
|
66
|
+
begin
|
67
|
+
io = ::File.open(file_name, 'wb+')
|
68
|
+
rescue StandardError => e
|
69
|
+
Log.critical("Cannot open FIT file '#{file_name}'", e)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a header object, but don't yet write it into the file.
|
73
|
+
header = FitHeader.new
|
74
|
+
start_pos = header.header_size
|
75
|
+
# Move the pointer behind the header section.
|
76
|
+
io.seek(start_pos)
|
77
|
+
id_mapper = FitMessageIdMapper.new
|
78
|
+
FitFileId.new.write(io, id_mapper)
|
79
|
+
activity.write(io, id_mapper)
|
80
|
+
end_pos = io.pos
|
81
|
+
|
82
|
+
crc = write_crc(io, start_pos, end_pos)
|
83
|
+
|
84
|
+
# Complete the data of the header section and write it at the start of
|
85
|
+
# the file.
|
86
|
+
header.data_size = end_pos - start_pos
|
87
|
+
header.crc = crc
|
88
|
+
io.seek(0)
|
89
|
+
header.write(io)
|
90
|
+
|
91
|
+
io.close
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def check_crc(io, end_pos)
|
97
|
+
# Save the current file IO position
|
98
|
+
start_pos = io.pos
|
99
|
+
|
100
|
+
crc = compute_crc(io, start_pos, end_pos)
|
101
|
+
|
102
|
+
# Read the 2 CRC bytes from the end of the file
|
103
|
+
io.seek(-2, IO::SEEK_END)
|
104
|
+
crc_ref = io.readbyte.to_i | (io.readbyte.to_i << 8)
|
105
|
+
io.seek(start_pos)
|
106
|
+
|
107
|
+
unless crc == crc_ref
|
108
|
+
Log.critical "Checksum error in file '#{@file_name}'. " +
|
109
|
+
"Computed #{"%04X" % crc} instead of #{"%04X" % crc_ref}."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def write_crc(io, start_pos, end_pos)
|
114
|
+
# Compute the checksum over the data section of the file and append it
|
115
|
+
# to the file. Ideally, we should compute the CRC from data in memory
|
116
|
+
# instead of the file data.
|
117
|
+
crc = compute_crc(io, start_pos, end_pos)
|
118
|
+
io.seek(end_pos)
|
119
|
+
BinData::Uint16le.new(crc).write(io)
|
120
|
+
|
121
|
+
crc
|
122
|
+
end
|
123
|
+
|
124
|
+
def compute_crc(io, start_pos, end_pos)
|
125
|
+
crc_table = [
|
126
|
+
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
|
127
|
+
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400
|
128
|
+
]
|
129
|
+
|
130
|
+
io.seek(start_pos)
|
131
|
+
|
132
|
+
crc = 0
|
133
|
+
while io.pos < end_pos
|
134
|
+
byte = io.readbyte
|
135
|
+
|
136
|
+
0.upto(1) do |i|
|
137
|
+
tmp = crc_table[crc & 0xF]
|
138
|
+
crc = (crc >> 4) & 0x0FFF
|
139
|
+
crc = crc ^ tmp ^ crc_table[(byte >> (4 * i)) & 0xF]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
crc
|
144
|
+
end
|
145
|
+
|
146
|
+
def dump_records(records)
|
147
|
+
records.each do |record|
|
148
|
+
record.dump
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|