fit4ruby 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/fit4ruby.rb +2 -2
- data/lib/fit4ruby/Activity.rb +7 -2
- data/lib/fit4ruby/FitDataRecord.rb +26 -16
- data/lib/fit4ruby/FitDefinition.rb +11 -1
- data/lib/fit4ruby/FitDefinitionField.rb +60 -18
- data/lib/fit4ruby/FitFile.rb +8 -7
- data/lib/fit4ruby/FitFileEntity.rb +80 -0
- data/lib/fit4ruby/FitMessageIdMapper.rb +16 -5
- data/lib/fit4ruby/FitMessageRecord.rb +89 -9
- data/lib/fit4ruby/FitRecord.rb +22 -11
- data/lib/fit4ruby/FitRecordHeader.rb +2 -2
- data/lib/fit4ruby/GlobalFitDictionaries.rb +50 -12
- data/lib/fit4ruby/GlobalFitMessage.rb +125 -8
- data/lib/fit4ruby/GlobalFitMessages.rb +135 -18
- data/lib/fit4ruby/version.rb +1 -1
- data/spec/FitFile_spec.rb +16 -12
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b2445f82a3a5c423df2e01cf21fdddd2c77322e
|
4
|
+
data.tar.gz: 5e51fe986b7b3fa1435a5b6f0dd8b475d6091c47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f11797d8a21e49791b04b5299d76e5ae7a14f110f38a7841aaffd5fc0b4e5d3471143c75b1b4f8d8f7e03051759e97bf96b8b370bedcfecee168f7240bad2cd0
|
7
|
+
data.tar.gz: 695da88503d5a491f5a4b50d6ed0334f18cf53fbdeaeba0a6a6c7f4ed1de501dea491745ffb8e86415fd858691f2cd02612f1abd778765657dbde47632d69fae
|
data/lib/fit4ruby.rb
CHANGED
data/lib/fit4ruby/Activity.rb
CHANGED
@@ -99,7 +99,7 @@ module Fit4Ruby
|
|
99
99
|
# @return recovery time in seconds.
|
100
100
|
def recovery_time
|
101
101
|
@events.each do |e|
|
102
|
-
return e.
|
102
|
+
return e.recovery_time if e.event == 'recovery_time'
|
103
103
|
end
|
104
104
|
|
105
105
|
nil
|
@@ -109,7 +109,7 @@ module Fit4Ruby
|
|
109
109
|
# based on multiple previous activities.
|
110
110
|
def vo2max
|
111
111
|
@events.each do |e|
|
112
|
-
return e.
|
112
|
+
return e.vo2max if e.event == 'vo2max'
|
113
113
|
end
|
114
114
|
|
115
115
|
nil
|
@@ -120,6 +120,11 @@ module Fit4Ruby
|
|
120
120
|
@sessions[0].sport
|
121
121
|
end
|
122
122
|
|
123
|
+
# Returns the sport subtype of this activity.
|
124
|
+
def sub_sport
|
125
|
+
@sessions[0].sub_sport
|
126
|
+
end
|
127
|
+
|
123
128
|
# Write the Activity data to a file.
|
124
129
|
# @param io [IO] File reference
|
125
130
|
# @param id_mapper [FitMessageIdMapper] Maps global FIT record types to
|
@@ -24,9 +24,12 @@ module Fit4Ruby
|
|
24
24
|
|
25
25
|
# Create instance variables that correspond to every field of the
|
26
26
|
# corresponding FIT data record.
|
27
|
-
@message.
|
28
|
-
create_instance_variable(
|
27
|
+
@message.fields_by_name.each do |name, field|
|
28
|
+
create_instance_variable(name)
|
29
29
|
end
|
30
|
+
# Meta fields are additional fields that are not part of the FIT
|
31
|
+
# specification but are convenient to have. These are typcially
|
32
|
+
# aggregated or converted values of regular fields.
|
30
33
|
@meta_field_units = {}
|
31
34
|
@timestamp = Time.now
|
32
35
|
end
|
@@ -40,17 +43,18 @@ module Fit4Ruby
|
|
40
43
|
def set(name, value)
|
41
44
|
ivar_name = '@' + name
|
42
45
|
unless instance_variable_defined?(ivar_name)
|
43
|
-
Log.warn("Unknown FIT record field '#{
|
46
|
+
Log.warn("Unknown FIT record field '#{name}' in global message " +
|
47
|
+
"#{@message.name} (#{@message.number}).")
|
44
48
|
return
|
45
49
|
end
|
46
|
-
instance_variable_set(
|
50
|
+
instance_variable_set(ivar_name, value)
|
47
51
|
end
|
48
52
|
|
49
53
|
def get(name)
|
50
54
|
ivar_name = '@' + name
|
51
55
|
return nil unless instance_variable_defined?(ivar_name)
|
52
56
|
|
53
|
-
instance_variable_get(
|
57
|
+
instance_variable_get(ivar_name)
|
54
58
|
end
|
55
59
|
|
56
60
|
def get_as(name, to_unit)
|
@@ -60,7 +64,7 @@ module Fit4Ruby
|
|
60
64
|
if @meta_field_units.include?(name)
|
61
65
|
unit = @meta_field_units[name]
|
62
66
|
else
|
63
|
-
field = @message.
|
67
|
+
field = @message.fields_by_name[name]
|
64
68
|
unless (unit = field.opts[:unit])
|
65
69
|
Log.fatal "Field #{name} has no unit"
|
66
70
|
end
|
@@ -70,15 +74,15 @@ module Fit4Ruby
|
|
70
74
|
end
|
71
75
|
|
72
76
|
def ==(fdr)
|
73
|
-
@message.
|
74
|
-
ivar_name = '@' +
|
77
|
+
@message.fields_by_name.each do |name, field|
|
78
|
+
ivar_name = '@' + name
|
75
79
|
v1 = field.fit_to_native(field.native_to_fit(
|
76
80
|
instance_variable_get(ivar_name)))
|
77
81
|
v2 = field.fit_to_native(field.native_to_fit(
|
78
82
|
fdr.instance_variable_get(ivar_name)))
|
79
83
|
|
80
84
|
unless v1 == v2
|
81
|
-
Log.error "#{
|
85
|
+
Log.error "#{name}: #{v1} != #{v2}"
|
82
86
|
return false
|
83
87
|
end
|
84
88
|
end
|
@@ -91,16 +95,22 @@ module Fit4Ruby
|
|
91
95
|
end
|
92
96
|
|
93
97
|
def write(io, id_mapper)
|
94
|
-
|
98
|
+
# Construct a GlobalFitMessage object that matches exactly the provided
|
99
|
+
# set of fields. It does not contain any AltField objects.
|
100
|
+
fields = {}
|
101
|
+
@message.fields_by_name.each_key do |name|
|
102
|
+
fields[name] = instance_variable_get('@' + name)
|
103
|
+
end
|
104
|
+
global_fit_message = @message.construct(fields)
|
95
105
|
|
96
106
|
# Map the global message number to the current local message number.
|
97
|
-
unless (local_message_number = id_mapper.get_local(
|
107
|
+
unless (local_message_number = id_mapper.get_local(global_fit_message))
|
98
108
|
# If the current dictionary does not contain the global message
|
99
109
|
# number, we need to create a new entry for it. The index in the
|
100
110
|
# dictionary is the local message number.
|
101
|
-
local_message_number = id_mapper.add_global(
|
111
|
+
local_message_number = id_mapper.add_global(global_fit_message)
|
102
112
|
# Write the definition of the global message number to the file.
|
103
|
-
|
113
|
+
global_fit_message.write(io, local_message_number)
|
104
114
|
end
|
105
115
|
|
106
116
|
# Write data record header.
|
@@ -112,7 +122,7 @@ module Fit4Ruby
|
|
112
122
|
|
113
123
|
# Create a BinData::Struct object to store the data record.
|
114
124
|
fields = []
|
115
|
-
|
125
|
+
global_fit_message.fields_by_number.each do |field_number, field|
|
116
126
|
bin_data_type = FitDefinitionField.fit_type_to_bin_data(field.type)
|
117
127
|
fields << [ bin_data_type, field.name ]
|
118
128
|
end
|
@@ -120,7 +130,7 @@ module Fit4Ruby
|
|
120
130
|
|
121
131
|
# Fill the BinData::Struct object with the values from the corresponding
|
122
132
|
# instance variables.
|
123
|
-
|
133
|
+
global_fit_message.fields_by_number.each do |field_number, field|
|
124
134
|
iv = "@#{field.name}"
|
125
135
|
if instance_variable_defined?(iv) &&
|
126
136
|
!(iv_value = instance_variable_get(iv)).nil?
|
@@ -139,7 +149,7 @@ module Fit4Ruby
|
|
139
149
|
|
140
150
|
def inspect
|
141
151
|
fields = {}
|
142
|
-
@message.
|
152
|
+
@message.fields_by_name.each do |name, field|
|
143
153
|
ivar_name = '@' + field.name
|
144
154
|
fields[field.name] = instance_variable_get(ivar_name)
|
145
155
|
end
|
@@ -15,6 +15,13 @@ require 'fit4ruby/FitDefinitionField'
|
|
15
15
|
|
16
16
|
module Fit4Ruby
|
17
17
|
|
18
|
+
# The FitDefinition contains the blueprints for FitMessageRecord segments of
|
19
|
+
# FIT files. Before a message record can occur in a FIT file, its definition
|
20
|
+
# must be included in the FIT file. The definition holds enough information
|
21
|
+
# about the message record to define its size. It also contains some basic
|
22
|
+
# information how to interpret the data in the record. To fully understand
|
23
|
+
# the message record data the full definition in the GlobalFitMessage is
|
24
|
+
# required.
|
18
25
|
class FitDefinition < BinData::Record
|
19
26
|
|
20
27
|
hide :reserved
|
@@ -33,11 +40,14 @@ module Fit4Ruby
|
|
33
40
|
end
|
34
41
|
|
35
42
|
def check
|
43
|
+
if architecture.snapshot > 1
|
44
|
+
Log.error "Illegal architecture value #{architecture.snapshot}"
|
45
|
+
end
|
36
46
|
fields.each { |f| f.check }
|
37
47
|
end
|
38
48
|
|
39
49
|
def setup(fit_message_definition)
|
40
|
-
fit_message_definition.
|
50
|
+
fit_message_definition.fields_by_number.each do |number, f|
|
41
51
|
fdf = FitDefinitionField.new
|
42
52
|
fdf.field_definition_number = number
|
43
53
|
fdf.set_type(f.type)
|
@@ -17,6 +17,11 @@ require 'fit4ruby/GlobalFitMessage'
|
|
17
17
|
|
18
18
|
module Fit4Ruby
|
19
19
|
|
20
|
+
# The FitDefinitionField models the part of the FIT file that contains the
|
21
|
+
# template definition for a field of a FitMessageRecord. It should match the
|
22
|
+
# corresponding definition in GlobalFitMessages. In case we don't have a
|
23
|
+
# known entry in GlobalFitMessages we can still read the file since we know
|
24
|
+
# the exact size of the binary records.
|
20
25
|
class FitDefinitionField < BinData::Record
|
21
26
|
|
22
27
|
@@TypeDefs = [
|
@@ -28,7 +33,7 @@ module Fit4Ruby
|
|
28
33
|
[ 'uint16', 'uint16', 0xFFFF, 2 ],
|
29
34
|
[ 'sint32', 'int32', 0x7FFFFFFF, 4 ],
|
30
35
|
[ 'uint32', 'uint32', 0xFFFFFFFF, 4 ],
|
31
|
-
[ 'string', '
|
36
|
+
[ 'string', 'string', '', 0 ],
|
32
37
|
[ 'float32', 'float', 0xFFFFFFFF, 4 ],
|
33
38
|
[ 'float63', 'double', 0xFFFFFFFF, 4 ],
|
34
39
|
[ 'uint8z', 'uint8', 0, 1 ],
|
@@ -62,9 +67,10 @@ module Fit4Ruby
|
|
62
67
|
@global_message_definition = GlobalFitMessages[@global_message_number]
|
63
68
|
field_number = field_definition_number.snapshot
|
64
69
|
if @global_message_definition &&
|
65
|
-
(field = @global_message_definition.
|
66
|
-
@name = field.name
|
67
|
-
|
70
|
+
(field = @global_message_definition.fields_by_number[field_number])
|
71
|
+
@name = field.respond_to?('name') ? field.name :
|
72
|
+
"choice_#{field_number}"
|
73
|
+
@type = field.respond_to?('type') ? field.type : nil
|
68
74
|
|
69
75
|
if @type && (td = @@TypeDefs[base_type_number]) && td[0] != @type
|
70
76
|
Log.warn "#{@global_message_number}:#{@name} must be of type " +
|
@@ -88,11 +94,19 @@ module Fit4Ruby
|
|
88
94
|
value = nil if value == undefined_value
|
89
95
|
|
90
96
|
field_number = field_definition_number.snapshot
|
91
|
-
if
|
92
|
-
|
93
|
-
|
97
|
+
if value.kind_of?(Array)
|
98
|
+
ary = []
|
99
|
+
value.each { |v| ary << to_machine(v) }
|
100
|
+
ary
|
94
101
|
else
|
95
|
-
|
102
|
+
if @global_message_definition &&
|
103
|
+
(field = @global_message_definition.
|
104
|
+
fields_by_number[field_number]) &&
|
105
|
+
field.respond_to?('to_machine')
|
106
|
+
field.to_machine(value)
|
107
|
+
else
|
108
|
+
value
|
109
|
+
end
|
96
110
|
end
|
97
111
|
end
|
98
112
|
|
@@ -100,12 +114,18 @@ module Fit4Ruby
|
|
100
114
|
init unless @global_message_number
|
101
115
|
value = nil if value == undefined_value
|
102
116
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
117
|
+
if value.kind_of?(Array)
|
118
|
+
s = '[ '
|
119
|
+
value.each { |v| s << to_s(v) + ' ' }
|
120
|
+
s + ']'
|
107
121
|
else
|
108
|
-
|
122
|
+
field_number = field_definition_number.snapshot
|
123
|
+
if @global_message_definition &&
|
124
|
+
(field = @global_message_definition.fields_by_number[field_number])
|
125
|
+
field.to_s(value)
|
126
|
+
else
|
127
|
+
"[#{value.to_s}]"
|
128
|
+
end
|
109
129
|
end
|
110
130
|
end
|
111
131
|
|
@@ -117,19 +137,41 @@ module Fit4Ruby
|
|
117
137
|
end
|
118
138
|
|
119
139
|
def type(fit_type = false)
|
120
|
-
|
121
|
-
|
140
|
+
check_fit_base_type
|
141
|
+
@@TypeDefs[base_type_number.snapshot][fit_type ? 0 : 1]
|
142
|
+
end
|
143
|
+
|
144
|
+
def is_array?
|
145
|
+
if total_bytes > base_type_bytes
|
146
|
+
if total_bytes % base_type_bytes != 0
|
147
|
+
Log.fatal "Total bytes (#{total_bytes}) must be multiple of " +
|
148
|
+
"base type bytes (#{base_type_bytes})."
|
149
|
+
end
|
150
|
+
return true
|
122
151
|
end
|
152
|
+
false
|
153
|
+
end
|
123
154
|
|
124
|
-
|
155
|
+
def total_bytes
|
156
|
+
self.byte_count.snapshot
|
157
|
+
end
|
158
|
+
|
159
|
+
def base_type_bytes
|
160
|
+
check_fit_base_type
|
161
|
+
@@TypeDefs[base_type_number.snapshot][3]
|
125
162
|
end
|
126
163
|
|
127
164
|
def undefined_value
|
165
|
+
check_fit_base_type
|
166
|
+
@@TypeDefs[base_type_number.snapshot][2]
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def check_fit_base_type
|
128
172
|
if @@TypeDefs.length <= base_type_number.snapshot
|
129
173
|
Log.fatal "Unknown FIT Base type #{base_type_number.snapshot}"
|
130
174
|
end
|
131
|
-
|
132
|
-
@@TypeDefs[base_type_number.snapshot][2]
|
133
175
|
end
|
134
176
|
|
135
177
|
end
|
data/lib/fit4ruby/FitFile.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
#
|
4
4
|
# = FitFile.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
|
@@ -12,6 +12,7 @@
|
|
12
12
|
|
13
13
|
require 'fit4ruby/Log'
|
14
14
|
require 'fit4ruby/FitHeader'
|
15
|
+
require 'fit4ruby/FitFileEntity'
|
15
16
|
require 'fit4ruby/FitRecord'
|
16
17
|
require 'fit4ruby/FitFilter'
|
17
18
|
require 'fit4ruby/FitMessageIdMapper'
|
@@ -39,7 +40,7 @@ module Fit4Ruby
|
|
39
40
|
|
40
41
|
check_crc(io, header.end_pos)
|
41
42
|
|
42
|
-
|
43
|
+
entity = FitFileEntity.new
|
43
44
|
# This Array holds the raw data of the records that may be needed to
|
44
45
|
# dump a human readable form of the FIT file.
|
45
46
|
records = []
|
@@ -48,7 +49,7 @@ module Fit4Ruby
|
|
48
49
|
record_counters = Hash.new { 0 }
|
49
50
|
while io.pos < header.end_pos
|
50
51
|
record = FitRecord.new(definitions)
|
51
|
-
record.read(io,
|
52
|
+
record.read(io, entity, filter, record_counters)
|
52
53
|
records << record if filter
|
53
54
|
end
|
54
55
|
|
@@ -57,11 +58,11 @@ module Fit4Ruby
|
|
57
58
|
header.dump if filter && filter.record_numbers.nil?
|
58
59
|
dump_records(records) if filter
|
59
60
|
|
60
|
-
|
61
|
-
|
61
|
+
entity.check
|
62
|
+
entity.top_level_record
|
62
63
|
end
|
63
64
|
|
64
|
-
def write(file_name,
|
65
|
+
def write(file_name, top_level_record)
|
65
66
|
begin
|
66
67
|
io = ::File.open(file_name, 'wb+')
|
67
68
|
rescue StandardError => e
|
@@ -74,7 +75,7 @@ module Fit4Ruby
|
|
74
75
|
# Move the pointer behind the header section.
|
75
76
|
io.seek(start_pos)
|
76
77
|
id_mapper = FitMessageIdMapper.new
|
77
|
-
|
78
|
+
top_level_record.write(io, id_mapper)
|
78
79
|
end_pos = io.pos
|
79
80
|
|
80
81
|
crc = write_crc(io, start_pos, end_pos)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitMessageRecord.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/Activity'
|
14
|
+
|
15
|
+
module Fit4Ruby
|
16
|
+
|
17
|
+
# The FIT file is a generic container for all kinds of data. This could be
|
18
|
+
# activity data, config files, workout definitions, etc. All data is stored
|
19
|
+
# in FIT message records. Also the information what kind of FIT file this is
|
20
|
+
# is stored in such a record. When we start reading the file, we actually
|
21
|
+
# don't know what kind of file it is until we find the right record to tell
|
22
|
+
# us. Since we already need to have gathered some information at this point,
|
23
|
+
# we use this utility class to store the read data until we know what Ruby
|
24
|
+
# objec we need to use to store it for later consumption.
|
25
|
+
class FitFileEntity
|
26
|
+
|
27
|
+
attr_reader :top_level_record
|
28
|
+
|
29
|
+
# Create a FitFileEntity.
|
30
|
+
def initialize
|
31
|
+
@top_level_record = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set what kind of FIT file we are dealing with.
|
35
|
+
# @return The Ruby object that will hold the content of the FIT file. It's
|
36
|
+
# a derivative of FitDataRecord.
|
37
|
+
def set_type(type)
|
38
|
+
if @top_level_record
|
39
|
+
Log.fatal "FIT file type has already been set to " +
|
40
|
+
"#{@top_level_record.class}"
|
41
|
+
end
|
42
|
+
case type
|
43
|
+
when 4, 'activity'
|
44
|
+
@top_level_record = Activity.new
|
45
|
+
@type = 'activity'
|
46
|
+
else
|
47
|
+
Log.error "Unsupported FIT file type #{type}"
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
|
51
|
+
@top_level_record
|
52
|
+
end
|
53
|
+
|
54
|
+
# Add a new data record to the top-level object.
|
55
|
+
def new_fit_data_record(type)
|
56
|
+
return nil unless @top_level_record
|
57
|
+
|
58
|
+
# We already have a record for the top-level type. Just return it.
|
59
|
+
return @top_level_record if type == @type
|
60
|
+
|
61
|
+
# For all other types, we need to create a new record inside the
|
62
|
+
# top-level record.
|
63
|
+
@top_level_record.new_fit_data_record(type)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check the consistency of the top-level object.
|
67
|
+
def check
|
68
|
+
return false unless @top_level_record
|
69
|
+
@top_level_record.check
|
70
|
+
end
|
71
|
+
|
72
|
+
# Write the top-level object into a IO stream.
|
73
|
+
def write(io, id_mapper)
|
74
|
+
return unless @top_level_record
|
75
|
+
@top_level_record.write(io, id_mapper)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|