fit4ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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