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.
@@ -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