fit4ruby 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|