fitreader 0.2.4 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8836bb10c48ea552d7ede1a7cfc335fd0928a88
4
- data.tar.gz: df87a422072d036bf658a7f0a8fbd8e577b02d44
3
+ metadata.gz: fa987e945eec8a037f1cd52e0d14ef91f26b72bd
4
+ data.tar.gz: c22121460e757cff8b7c20145bf06707af7cba47
5
5
  SHA512:
6
- metadata.gz: 3354ace1a4fd930175ae3d92101db74b596d9ecfd88cee7a89713a20b997676b247071fcb0fdbde5c0adae38d7bd647d93f0037e74b702eaed92aa9ec73ae53c
7
- data.tar.gz: 7d95d89756f8e3b30e2bf3f8177165e361dcf178c513e5f69ea84952a7f14d5d73a3e5eb69123d5a2f383a97323aa8cefc958ad7ae844bd8e41c4efc270be5d5
6
+ metadata.gz: 972d02706d802da6d0965d2379dcae5e0e6326be5206bc55abbb27eb367be4c803faa5cdbc6d22482486baa767e8db69276c3eddbfe1fca89773291c19e24c02
7
+ data.tar.gz: 2857b1d11a428303e190b658be0770a24c0c78bf3a95a344e3cc0ab33e13c79249eab15988afd1109b64262186b102f38a4f41426fb3f43c9ef7439e337a1f7c
data/.gitignore CHANGED
@@ -1,9 +1,9 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
4
3
  /_yardoc/
5
4
  /coverage/
6
5
  /doc/
7
6
  /pkg/
8
7
  /spec/reports/
9
8
  /tmp/
9
+ *.gem
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fitreader (0.3.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.1)
10
+ diff-lcs (1.3)
11
+ method_source (0.8.2)
12
+ pry (0.10.4)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.8.1)
15
+ slop (~> 3.4)
16
+ rake (10.5.0)
17
+ rspec (3.6.0)
18
+ rspec-core (~> 3.6.0)
19
+ rspec-expectations (~> 3.6.0)
20
+ rspec-mocks (~> 3.6.0)
21
+ rspec-core (3.6.0)
22
+ rspec-support (~> 3.6.0)
23
+ rspec-expectations (3.6.0)
24
+ diff-lcs (>= 1.2.0, < 2.0)
25
+ rspec-support (~> 3.6.0)
26
+ rspec-mocks (3.6.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.6.0)
29
+ rspec-support (3.6.0)
30
+ slop (3.6.0)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ bundler (~> 1.12)
37
+ fitreader!
38
+ pry (~> 0.10.4)
39
+ rake (~> 10.0)
40
+ rspec (~> 3.5)
41
+
42
+ BUNDLED WITH
43
+ 1.14.6
data/README.md CHANGED
@@ -35,7 +35,7 @@ require 'fitreader'
35
35
  to the class you wish to call it from. After that, it's a simple matter of calling
36
36
 
37
37
  ```ruby
38
- Fitreader.read(path_to_fit_file)
38
+ fit_file = Fit.new(path_to_fit_file)
39
39
  ```
40
40
 
41
41
  All of the interface and convenience functions can be found in lib/fitreader.rb.
@@ -43,64 +43,53 @@ All of the interface and convenience functions can be found in lib/fitreader.rb.
43
43
  After reading a FIT file, the file header can be inspected by calling
44
44
 
45
45
  ```ruby
46
- Fitreader.header
46
+ fit_file.header
47
47
  ```
48
48
 
49
49
  A digest of the records found in the file is shown with
50
50
 
51
51
  ```ruby
52
- Fitreader.available_records
52
+ fit_file.digest
53
53
  ```
54
54
 
55
- which will return a list similar to the following, showing respectively the global_message_number (as defined in the FIT SDK), the name of the record type, and the number of records parsed
55
+ which will return a list similar to the following, showing respectively the name of the record type (as defined in the FIT SDK), and the number of records parsed
56
56
 
57
57
  ```ruby
58
- [[0, :file_id, 1],
59
- [3, :user_profile, 1],
60
- [7, :zones_target, 1],
61
- [18, :session, 1],
62
- [19, :lap, 1],
63
- [20, :record, 2899],
64
- [21, :event, 73],
65
- [22, :source, 58],
66
- [23, :device_info, 14],
67
- [34, :activity, 1],
68
- [49, :file_creator, 1],
69
- [72, :training_file, 1],
70
- [104, :battery_info, 40],
71
- [147, :sensor_info, 3]]
58
+ {:file_id=>1,
59
+ :file_creator=>1,
60
+ :device_settings=>1,
61
+ :user_profile=>1,
62
+ :sensor_info=>5,
63
+ :sport=>1,
64
+ :zones_target=>1,
65
+ :record=>4988,
66
+ :event=>120,
67
+ :device_info=>30,
68
+ :source=>43,
69
+ :segment_lap=>2,
70
+ :lap=>1,
71
+ :session=>1,
72
+ :activity=>1,
73
+ :battery_info=>20}
72
74
  ```
73
75
 
74
76
  Armed with this information, we can call
75
77
 
76
78
  ```ruby
77
- Fitreader.get_message_type filter
79
+ fit_file.type <name>
78
80
  ```
79
81
 
80
- where filter is either the global_number or the name supplied by the previous command, for example
82
+ where <name> is the name supplied by the previous command, for example
81
83
 
82
84
  ```ruby
83
- Fitreader.get_message_type 18
84
- Fitreader.get_message_type :session
85
+ fit_file.type :session
85
86
  ```
86
87
 
87
- will both fetch the session record(s) in the form of a MessageType object. This object contains three fields: a definition, an array of records and an array of undefined_records.
88
+ will fetch the session record(s) in the form of a Message object. This object contains three fields: a name, a global_num (as defined by the FIT SDK), and an array of records.
88
89
 
89
- The definition contains metadata from a combination of the FIT file itself and the SKD. I will write more detailed information, but basically this information tells us how to interpret the actual data. Of most relevance, it tells us the name and datatype of each field we will find in records of this type.
90
-
91
- The records array contains a list or Record objects. A Record object contains a fields array and an error_fields array. The fields array is a list of the actual data contined within this record, for example timestamp, or coordinated, etc. The particular FieldData object includes the name of the field (also found in the definition), along with the raw_value and the processed value which may differ in the case of, for example, a coordinate.
92
-
93
- The error_fields array contains any fields defined in the FIT file itself that aren't defined within the SDK. Without the SKD we can most likely never know what the field represents or whether the raw value would need further processing to make sense to us. These are included mostly for debugging and future convenience sake.
94
-
95
- The results of this function are probably a lot more data that we generally need, so a convenience function is included that distills the results of the previous call into a more efficient dataset
96
-
97
- ```ruby
98
- Fitreader.record_values 20
99
- Fitreader.record_values :record
100
- ```
101
-
102
- This will return an array of hashes, one hash for each record of the specified type. The hashes contain only the field name and value, for example
90
+ The records array contains a list of hashes, with key-value pairs of field name (also according to the message-type definition) and value, for example timestamp, or coordinates, etc.
103
91
 
92
+ For example:
104
93
  ```ruby
105
94
  {:timestamp=>2016-04-09 11:19:51 UTC,
106
95
  :position_lat=>57.711100755259395,
@@ -116,7 +105,7 @@ This will return an array of hashes, one hash for each record of the specified t
116
105
 
117
106
  Some things to watch out for:
118
107
 
119
- - speed is recorded as m/s in my files, rather than kph as one might expect.
108
+ - speed is recorded as m/s, rather than kph as one might expect.
120
109
 
121
110
  ## Development
122
111
 
data/fitreader.gemspec CHANGED
@@ -31,7 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "bundler", "~> 1.12"
32
32
  spec.add_development_dependency "rake", "~> 10.0"
33
33
  spec.add_development_dependency "rspec", "~> 3.5"
34
- spec.add_development_dependency "pry"
35
-
36
- # spec.add_dependency('bindata')
34
+ spec.add_development_dependency "pry", '~> 0.10.4'
37
35
  end
data/lib/fitreader.rb CHANGED
@@ -1,55 +1,5 @@
1
- require 'fitreader/fitfile'
2
- require 'fitreader/version'
3
- require 'fitreader/static'
4
-
5
1
  module Fitreader
6
- def self.read(path)
7
- @f = FitFile.new(path)
8
- end
9
-
10
- def self.header
11
- @f.header
12
- end
13
-
14
- def self.available_records
15
- @f.messages
16
- .select { |_,v| !v.any? { |z| z.definition.name.nil? } }
17
- .select { |_,v| !v.any? { |z| z.records.length.zero? } }
18
- .collect { |x| [x[0], x[1].first.definition.name, x[1].collect(&:records).flatten.length] }
19
- .sort
20
- end
21
-
22
- def self.get_message_type(filter)
23
- if filter.is_a?(Symbol)
24
- res = @f.messages.find { |_,y| y.any? { |z| z.definition.name == filter } }
25
- res[1] unless res.nil?
26
- elsif filter.is_a?(Integer)
27
- @f.messages[filter]
28
- else
29
- raise ArgumentError, 'needs a string or a symbol'
30
- end
31
- end
32
-
33
- def self.record_values(filter)
34
- message = get_message_type filter
35
- message.collect(&:record_values).flatten
36
- end
37
-
38
- def self.error_messages
39
- @f.messages.select { |_, v| !v.any? { |x| x.undefined_records.empty? } }
40
- end
41
-
42
- def self.error_fields(filter)
43
- message = get_message_type filter
44
- message.collect(&:error_fields).flatten
45
- end
46
-
47
- # def self.filter_by_scope(filter)
48
- # valid = Static.scope.include? filter
49
- # unless valid
50
- # @f.records.select { |x| x.type == filter }
51
- # else
52
- # puts "invalid scope, must be one of #{Static.scope}"
53
- # end
54
- # end
55
2
  end
3
+
4
+ require 'fitreader/fit'
5
+ require 'fitreader/version'
@@ -0,0 +1,38 @@
1
+ class DataField < FitObject
2
+ TYPES = {
3
+ 0 => { size: 1, unpack_type: 'C', endian: 0, invalid: 255 },
4
+ 1 => { size: 1, unpack_type: 'c', endian: 0, invalid: 127 },
5
+ 2 => { size: 1, unpack_type: 'C', endian: 0, invalid: 255 },
6
+ 3 => { size: 2, unpack_type: { big: 's>', little: 's<' }, endian: 1, invalid: 32767 },
7
+ 4 => { size: 2, unpack_type: { big: 'S>', little: 'S<' }, endian: 1, invalid: 65535 },
8
+ 5 => { size: 4, unpack_type: { big: 'l>', little: 'l<' }, endian: 1, invalid: 2147483647 },
9
+ 6 => { size: 4, unpack_type: { big: 'L>', little: 'L<' }, endian: 1, invalid: 4294967295 },
10
+ 7 => { size: 1, unpack_type: 'Z*', endian: 0, invalid: 0 },
11
+ 8 => { size: 4, unpack_type: { big: 'e', little: 'g' }, endian: 1, invalid: 4294967295 },
12
+ 9 => { size: 8, unpack_type: { big: 'E', little: 'G' }, endian: 1, invalid: 18446744073709551615 },
13
+ 10 => { size: 1, unpack_type: 'C', endian: 0, invalid: 0 },
14
+ 11 => { size: 2, unpack_type: { big: 'S>', little: 'S<' }, endian: 1, invalid: 0 },
15
+ 12 => { size: 4, unpack_type: { big: 'L>', little: 'L<' }, endian: 1, invalid: 0 },
16
+ 13 => { size: 1, unpack_type: 'C', endian: 0, invalid: 0xFF },
17
+ 14 => { size: 8, unpack_type: { big: 'q>', little: 'q<' }, endian: 1, invalid: 0x7FFFFFFFFFFFFFFF },
18
+ 15 => { size: 8, unpack_type: { big: 'Q>', little: 'Q<' }, endian: 1, invalid: 0xFFFFFFFFFFFFFFFF },
19
+ 16 => { size: 8, unpack_type: nil, endian: 1, invalid: 0x0000000000000000 }
20
+ }.freeze
21
+
22
+ attr_reader :raw, :valid
23
+
24
+ def initialize(io, d, arch)
25
+ base = TYPES[d.base_num]
26
+ char = d.endianness.zero? ? base[:unpack_type] : base[:unpack_type][arch]
27
+ @raw = read_multiple(io, char, d.size, base[:size])
28
+ @valid = check(@raw, base[:invalid])
29
+ end
30
+
31
+ def check(raw, invalid)
32
+ if raw.is_a? Array
33
+ raw.any? { |e| e != invalid }
34
+ else
35
+ raw != invalid
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ class DataRecord < FitObject
2
+ attr_reader :fields, :global_num
3
+
4
+ def initialize(io, definition)
5
+ @global_num = definition.global_msg_num
6
+ @fields = Hash[definition.field_definitions.map do |f|
7
+ [f.field_def_num, DataField.new(io, f, definition.endian)]
8
+ end]
9
+ end
10
+
11
+ def valid
12
+ @fields.select { |_, v| v.valid }
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'field_definition.rb'
2
+
3
+ class DefinitionRecord < FitObject
4
+ attr_reader :reserved, :architecture, :global_msg_num, :num_fields, :field_definitions, :data_records, :local_num
5
+
6
+ def initialize(io, local_num)
7
+ @local_num = local_num
8
+ @reserved = io.readbyte
9
+ @architecture = io.readbyte
10
+ char = @architecture.zero? ? 'v' : 'n'
11
+ @global_msg_num = readbytes(io, char, 2)
12
+ @num_fields = io.readbyte
13
+ @field_definitions = Array.new(num_fields) { FieldDefinition.new(io) }
14
+ @data_records = []
15
+ end
16
+
17
+ def endian
18
+ @architecture.zero? ? :little : :big
19
+ end
20
+
21
+ def valid
22
+ fd = Sdk.fields(@global_msg_num)
23
+ return if fd.nil?
24
+ @data_records.map do |d|
25
+ d.valid.select { |k, _| fd.keys.include? k }
26
+ end
27
+ end
28
+ end
@@ -1,15 +1,11 @@
1
- module Fitreader
2
- class FieldDefinition
3
- attr_accessor :def_num, :size, :base_num
1
+ class FieldDefinition < FitObject
2
+ attr_reader :field_def_num, :size, :endianness, :base_num
4
3
 
5
- def initialize(msg_num, bytes)
6
- @def_num = bytes[0].unpack('C').first
7
- @size = bytes[1].unpack('C').first
8
- # @single_byte = bytes[2].unpack('C').first & ENDIAN_ABILITY == 0
9
- @base_num = bytes[2].unpack('C').first & BASE_TYPE_NUM
10
- end
11
-
12
- ENDIAN_ABILITY = 128
13
- BASE_TYPE_NUM = 31
4
+ def initialize(io)
5
+ @field_def_num = io.readbyte
6
+ @size = io.readbyte
7
+ byte = io.readbyte
8
+ @endianness = read_bit(byte, 7)
9
+ @base_num = read_bits(byte, 4..0)
14
10
  end
15
11
  end
@@ -1,17 +1,12 @@
1
- class FileHeader
2
- attr_accessor :header_size, :protocol_version,
3
- :profile_version, :num_records, :valid_file, :crc
1
+ class FileHeader < FitObject
2
+ attr_accessor :header_size, :protocol_version, :profile_version, :num_record_bytes, :valid_file, :crc
4
3
 
5
- def initialize(bytes)
6
- @header_size = bytes[0].unpack('C').first
7
- @protocol_version = bytes[1].unpack('C').first
8
- @profile_version = bytes[2..3].unpack('v').first
9
- @num_records = bytes[4..7].unpack('V').first
10
- @valid_file = bytes[8..11] == ".FIT"
11
- @crc = bytes[12..13].unpack('v').first
12
- end
13
-
14
- def to_s
15
- "size: #{header_size}, proto_v: #{protocol_version}, prof_v: #{profile_version}, records: #{num_records}"
4
+ def initialize(io)
5
+ @header_size = io.readbyte
6
+ @protocol_version = io.readbyte
7
+ @profile_version = readbytes(io, 'v', 2)
8
+ @num_record_bytes = readbytes(io, 'V', 4)
9
+ @valid_file = io.read(4) == '.FIT'
10
+ @crc = readbytes(io, 'v', 2)
16
11
  end
17
12
  end
@@ -0,0 +1,49 @@
1
+ require_relative 'unpack.rb'
2
+ require_relative 'fit_object.rb'
3
+ require_relative 'file_header.rb'
4
+ require_relative 'record_header.rb'
5
+ require_relative 'definition_record.rb'
6
+ require_relative 'data_field.rb'
7
+ require_relative 'data_record.rb'
8
+ require_relative 'message.rb'
9
+ require_relative 'sdk/sdk.rb'
10
+
11
+ class Fit
12
+ attr_reader :header, :messages
13
+
14
+ def initialize(io)
15
+ @header = FileHeader.new(io)
16
+ finished = []
17
+ begin
18
+ defs = {}
19
+ until io.pos >= header.num_record_bytes
20
+ h = RecordHeader.new(io)
21
+ if h.definition?
22
+ d = DefinitionRecord.new(io, h.local_message_type)
23
+ finished << defs[d.local_num] if defs.key? d.local_num
24
+ defs[d.local_num] = d
25
+ elsif h.data?
26
+ d = defs[h.local_message_type] if d.local_num != h.local_message_type
27
+ d.data_records << DataRecord.new(io, d)
28
+ else
29
+ # TODO implement timestamps
30
+ end
31
+ end
32
+ finished.push(*defs.values)
33
+ rescue
34
+ puts "error"
35
+ end
36
+ io.close
37
+ @messages = finished.group_by(&:global_msg_num)
38
+ .map { |x| Message.new x }
39
+ .reject { |x| x.data.nil? }
40
+ end
41
+
42
+ def digest
43
+ Hash[@messages.map { |x| [x.name, x.data.count] }]
44
+ end
45
+
46
+ def type(name)
47
+ messages.find { |x| x.name == name }
48
+ end
49
+ end
@@ -0,0 +1,8 @@
1
+ require 'ostruct'
2
+ class FitObject
3
+ include Unpack
4
+
5
+ def to_h
6
+ @data.to_h
7
+ end
8
+ end
@@ -0,0 +1,82 @@
1
+ class Message
2
+ attr_accessor :global_num, :name, :data
3
+
4
+ def initialize(definitions)
5
+ @global_num = definitions[0]
6
+ @name = Sdk.message(@global_num)
7
+ return unless @name
8
+
9
+ fd = Sdk.fields(@global_num)
10
+ @data = definitions[1].map { |x| make_message(x, fd) }.flatten
11
+ end
12
+
13
+ private
14
+
15
+ def make_message(definition, fields)
16
+ return if definition.valid.nil?
17
+ definition.valid.map do |d|
18
+ h = Hash[d.map { |k, v| process_value(fields[k], v.raw) }]
19
+ case @global_num
20
+ when 21
21
+ h = process_event(h)
22
+ when 0, 23
23
+ h = process_deviceinfo(h)
24
+ end
25
+ h
26
+ end
27
+ end
28
+
29
+ def process_value(type, val)
30
+ if type[:type][0..3].to_sym == :enum
31
+ val = Sdk.enum(type[:type])[val]
32
+ elsif type[:type] == :date_time
33
+ t = Time.new(1989, 12, 31, 0, 0, 0, '+00:00').utc.to_i
34
+ Time.at(val + t).utc
35
+ elsif type[:type] == :local_date_time
36
+ t = Time.new(1989, 12, 31, 0, 0, 0, '+02:00').utc.to_i
37
+ val = Time.at(val + t)
38
+ elsif type[:type] == :coordinates
39
+ val *= (180.0 / 2**31)
40
+ end
41
+
42
+ unless type[:scale].zero?
43
+ if val.is_a? Array
44
+ val = val.map { |x| (x * 1.0) / type[:scale] }
45
+ else
46
+ val = (val * 1.0) / type[:scale]
47
+ end
48
+ end
49
+
50
+ unless type[:offset].zero?
51
+ if val.is_a? Array
52
+ val.map { |x| x - type[:offset] }
53
+ else
54
+ val - type[:offset]
55
+ end
56
+ end
57
+ [type[:name], val]
58
+ rescue => e
59
+ puts e
60
+ end
61
+
62
+ def process_event(h)
63
+ case h[:event]
64
+ when :rear_gear_change, :front_gear_change
65
+ h[:data] = h[:data].pack('V*').unpack('C*')
66
+ end
67
+ h
68
+ end
69
+
70
+ def process_deviceinfo(h)
71
+ case h[:source_type]
72
+ when :antplus
73
+ h[:device_type] = Sdk.enum(:antplus_device_type)[h[:value]]
74
+ end
75
+
76
+ case h[:manufacturer]
77
+ when :garmin, :dynastream, :dynastream_oem
78
+ h[:garmin_product] = Sdk.enum(:enum_garmin_product)[h[:garmin_product]]
79
+ end
80
+ h
81
+ end
82
+ end