fit4ruby 0.0.4 → 0.0.5
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 +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
|