fitreader 0.1.1 → 0.2.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: 8e70e256d1754b9d603ebd7c0c0a3e312b14337f
4
- data.tar.gz: 6d972f23def9ffb2bce1738bbc892a06a203506d
3
+ metadata.gz: 6dc32742ab1e01f369d27408f7cab25fcde47e24
4
+ data.tar.gz: 41f1381a24b1985958e115f3f1033866ad9a005b
5
5
  SHA512:
6
- metadata.gz: b7c416df560642d9e80b0cab03ff5fc78f74679a7bcb6a778417c2df80a058836534493435e92a2409a645c05bfba23a5c3d44cf19581b3ddd36c3fa73e43558
7
- data.tar.gz: 5164ac137fcf5d36abe42520a9c38d6776e609a5294c04262792b0c6758d680da8b27018b95ca1339aae59f740227df0d75f552674704b031e6d2c9b7ba58017
6
+ metadata.gz: 6b89c5a123826b9a1ff2ae79b5f7be047f9bb35d44af1db1664de2289bde313465635a145b555228559c01d2f1de4f8f051933bd1e39b7e8758019c514fbe535
7
+ data.tar.gz: 2b9af2f2aa0b6502f8b7465acebed731ee819b207640898b659b16c0514d73e04caacebd04afc5375daa5da2b66fa84bd4e652f0edfc29831c35312204c628b4
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Fitreader
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/fitreader`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Fitreader is a small gem used for read and decoding FIT files generated by various sports devices. Currently it has only been designed specifically to read files produced by a Garmin Edge 1000. It will probably mostly work out of the box with other file sources, but will probably not work 100% perfectly unless it's a Garmin cycling computer.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,20 +20,115 @@ Or install it yourself as:
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ To use this gem in a ruby or rails application, simply add
24
+
25
+ ```ruby
26
+ gem 'fitreader'
27
+ ```
28
+
29
+ to the Gemfile and then
30
+
31
+ ```ruby
32
+ require 'fitreader'
33
+ ```
34
+
35
+ to the class you wish to call it from. After that, it's a simple matter of calling
36
+
37
+ ```ruby
38
+ Fitreader.read(path_to_fit_file)
39
+ ```
40
+
41
+ All of the interface and convenience functions can be found in lib/fitreader.rb.
42
+
43
+ After reading a FIT file, the file header can be inspected by calling
44
+
45
+ ```ruby
46
+ Fitreader.header
47
+ ```
48
+
49
+ A digest of the records found in the file is shown with
50
+
51
+ ```ruby
52
+ Fitreader.available_records
53
+ ```
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
56
+
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]]
72
+ ```
73
+
74
+ Armed with this information, we can call
75
+
76
+ ```ruby
77
+ Fitreader.get_message_type filter
78
+ ```
79
+
80
+ where filter is either the global_number or the name supplied by the previous command, for example
81
+
82
+ ```ruby
83
+ Fitreader.get_message_type 18
84
+ Fitreader.get_message_type :session
85
+ ```
86
+
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
+
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
103
+
104
+ ```ruby
105
+ {:timestamp=>2016-04-09 11:19:51 UTC,
106
+ :position_lat=>57.711100755259395,
107
+ :position_long=>11.992837116122246,
108
+ :distance=>78.94,
109
+ :altitude=>14.200000000000045,
110
+ :speed=>3.835,
111
+ :heart_rate=>103,
112
+ :cadence=>0,
113
+ :temperature=>12,
114
+ :fractional_cadence=>0.0},
115
+ ```
116
+
117
+ Some things to watch out for:
118
+
119
+ - speed is recorded as m/s in my files, rather than kph as one might expect.
26
120
 
27
121
  ## Development
28
122
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
123
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rspec spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
124
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
125
+ To install this gem onto your local machine from a clone of the repo, run `bundle exec rake install`.
32
126
 
33
127
  ## Contributing
34
128
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/fitreader.
129
+ Bug reports and pull requests are welcome on GitHub at https://github.com/samsari/fitreader.
36
130
 
37
131
 
38
132
  ## License
39
133
 
40
134
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
-
data/fitreader.gemspec CHANGED
@@ -30,9 +30,8 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_development_dependency "bundler", "~> 1.12"
32
32
  spec.add_development_dependency "rake", "~> 10.0"
33
- # spec.add_development_dependency "minitest", "~> 5.0"
34
33
  spec.add_development_dependency "rspec", "~> 3.5"
35
34
  spec.add_development_dependency "pry"
36
35
 
37
- spec.add_dependency('bindata')
36
+ # spec.add_dependency('bindata')
38
37
  end
@@ -3,13 +3,12 @@ require 'fitreader/errors'
3
3
 
4
4
  module Fitreader
5
5
  class DegradedRecord
6
- attr_reader :definition, :fields
6
+ attr_reader :fields
7
7
  def initialize(definition, bytes)
8
- @definition = definition
9
8
  @fields = {}
10
9
 
11
10
  start = 0
12
- @definition.field_definitions.each do |f|
11
+ definition.field_definitions.each do |f|
13
12
  raw = bytes[start...start+=f.size]
14
13
  b = Static.base[f.base_num]
15
14
  data = unpack_data(f, b, raw)
@@ -10,9 +10,9 @@ module Fitreader
10
10
  end
11
11
 
12
12
  class UnknownMessageTypeError < RuntimeError
13
- attr :definition
14
- def initialize(definition)
15
- @definition = definition
16
- end
13
+ # attr :definition
14
+ # def initialize(definition)
15
+ # @definition = definition
16
+ # end
17
17
  end
18
18
  end
@@ -3,23 +3,23 @@ require 'fitreader/record_header'
3
3
  require 'fitreader/definition'
4
4
  require 'fitreader/record'
5
5
  require 'fitreader/degraded_record'
6
+ require 'fitreader/message_type'
6
7
  require 'fitreader/static'
7
8
 
8
9
  module Fitreader
9
10
  class FitFile
10
- attr_reader :header, :records, :defs, :file, :error_records
11
+ attr_reader :header, :messages, :file
11
12
 
12
13
  def initialize(path)
13
14
  @file = File.open(path, 'rb')
14
15
  @header = FileHeader.new(@file.read(14))
15
16
  if valid?
16
17
  @defs = {}
17
- @records = []
18
- @error_records = []
18
+ @messages = {}
19
19
  while @file.pos < @header.num_records do
20
20
  process_next_record
21
21
  end
22
- puts "number of bad records found: #{@error_records.length}"
22
+ # puts "number of bad records found: #{@error_records.length}"
23
23
  end
24
24
  end
25
25
 
@@ -35,17 +35,18 @@ module Fitreader
35
35
  dr = Definition.new(h.local_num, @file.read(5))
36
36
  dr.add_fields(@file.read(dr.num_fields*3))
37
37
  @defs[h.local_num] = dr
38
+
39
+ m = MessageType.new(dr)
40
+ @messages[dr.global_num] = m
38
41
  elsif h.header_type == :data
39
42
  unless dr.nil?
40
43
  begin
41
44
  data = @file.read(dr.content_length)
42
45
  datr = Record.new(dr, data)
43
- @records.push(datr)
44
- rescue UnknownMessageTypeError => error
45
- # @error_records.push(error.definition)
46
- puts error
46
+ @messages[dr.global_num].records.push datr
47
+ rescue UnknownMessageTypeError
47
48
  degraded = DegradedRecord.new(dr, data)
48
- @error_records.push degraded
49
+ @messages[dr.global_num].undefined_records.push degraded
49
50
  end
50
51
  else
51
52
  msg = "no record def found! #{h.local_msg_num}"
@@ -0,0 +1,12 @@
1
+ module Fitreader
2
+ class MessageType
3
+ attr_reader :definition, :records
4
+ attr_accessor :undefined_records
5
+
6
+ def initialize(definition)
7
+ @definition = definition
8
+ @records = []
9
+ @undefined_records = []
10
+ end
11
+ end
12
+ end
@@ -5,32 +5,32 @@ module Fitreader
5
5
  class Record
6
6
  attr_reader :definition, :fields, :error_fields
7
7
  def initialize(definition, bytes)
8
- @definition = definition
8
+ # definition = definition
9
9
  @fields = {}
10
10
  @error_fields = {}
11
11
 
12
- unless @definition.fit_msg.nil?
12
+ unless definition.fit_msg.nil?
13
13
  start = 0
14
- @definition.field_definitions.each do |f|
14
+ definition.field_definitions.each do |f|
15
15
  raw = bytes[start...start+=f.size]
16
16
  b = Static.base[f.base_num]
17
17
  data = unpack_data(f, b, raw)
18
18
  begin
19
- process_data(f.def_num, b[:invalid], data)
19
+ process_data(f.def_num, b[:invalid], data, definition)
20
20
  rescue UnknownFieldTypeError => error
21
21
  push_error error.reason, error.field, error.data
22
22
  # puts error unless error.reason == :invalid
23
23
  end
24
24
  end
25
25
 
26
- if @definition.global_num == 21
26
+ if definition.global_num == 21
27
27
  process_event
28
- elsif @definition.global_num == 23
28
+ elsif definition.global_num == 23
29
29
  process_deviceinfo
30
30
  end
31
31
  else
32
- msg = "no known message type: #{@definition.global_num}"
33
- raise UnknownMessageTypeError.new(definition), msg, caller
32
+ msg = "no known message type: #{definition.global_num}"
33
+ raise UnknownMessageTypeError.new, msg, caller
34
34
  end
35
35
  end
36
36
 
@@ -52,8 +52,8 @@ module Fitreader
52
52
  end
53
53
  end
54
54
 
55
- def process_data(fieldDefNum, invalid, data)
56
- field_def = @definition.fit_msg[fieldDefNum]
55
+ def process_data(fieldDefNum, invalid, data, definition)
56
+ field_def = definition.fit_msg[fieldDefNum]
57
57
  unless field_def.nil?
58
58
  # populate invalid
59
59
  if data.is_a?(Array)
@@ -66,12 +66,12 @@ module Fitreader
66
66
  unless invalid
67
67
  @fields[fieldDefNum] = FieldData.new(fieldDefNum, data, field_def)
68
68
  else
69
- msg = "invalid field data (#{data}) processed for field number [#{fieldDefNum}::#{field_def[:name]}] in message (#{@definition.global_num}::#{@definition.name})"
70
- raise UnknownFieldTypeError.new(@definition, fieldDefNum, data, :invalid), msg, caller
69
+ msg = "invalid field data (#{data}) processed for field number [#{fieldDefNum}::#{field_def[:name]}] in message (#{definition.global_num}::#{definition.name})"
70
+ raise UnknownFieldTypeError.new(definition, fieldDefNum, data, :invalid), msg, caller
71
71
  end
72
72
  else
73
- msg = "invalid field [#{fieldDefNum}] encountered for message [#{@definition.global_num}::#{@definition.name}] with data [#{data}]"
74
- raise UnknownFieldTypeError.new(@definition, fieldDefNum, data, :unknown), msg, caller
73
+ msg = "invalid field [#{fieldDefNum}] encountered for message [#{definition.global_num}::#{definition.name}] with data [#{data}]"
74
+ raise UnknownFieldTypeError.new(definition, fieldDefNum, data, :unknown), msg, caller
75
75
  end
76
76
  end
77
77
 
@@ -98,7 +98,7 @@ module Fitreader
98
98
  # :battery_info 40
99
99
  # :sensor_info 3
100
100
  def type
101
- case @definition.name
101
+ case definition.name
102
102
  when :file_id, :user_profile, :zones_target, :session
103
103
  :ride
104
104
  when condition
@@ -1,3 +1,3 @@
1
1
  module Fitreader
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/fitreader.rb CHANGED
@@ -13,30 +13,38 @@ module Fitreader
13
13
  end
14
14
 
15
15
  def self.available_records
16
- @f.records.collect { |x| [x.definition.global_num, x.definition.name] }
17
- .group_by { |i| i }
16
+ @f.messages
17
+ .select { |x,y| !y.definition.name.nil? }
18
+ .select { |x,y| !y.records.length.zero? }
19
+ .collect { |x| [x[0], x[1].definition.name, x[1].records.length] }
18
20
  .sort
19
- .collect { |k, v| k << v.length }
20
21
  end
21
22
 
22
- def self.filter_by_record(filter)
23
+ def self.get_message_type(filter)
23
24
  if filter.is_a?(Symbol)
24
- @f.records.select { |x| x.definition.name == filter }
25
+ res = @f.messages.find { |_,y| y.definition.name == filter}
26
+ res[1] unless res.nil?
27
+ binding.pry
25
28
  elsif filter.is_a?(Integer)
26
- @f.records.select { |x| x.definition.global_num == filter }
29
+ @f.messages[filter]
27
30
  else
28
31
  raise ArgumentError, "needs a string or a symbol"
29
32
  end
30
33
  end
31
34
 
32
35
  def self.record_values(filter)
33
- records = filter_by_record filter
34
- records.collect {|x| x.fields.values.collect{|z| [z.name, z.value]}}.collect{|y| y.to_h}
36
+ message = get_message_type filter
37
+ message.records.collect {|x| x.fields.values.collect{|z| [z.name, z.value]}}.collect{|y| y.to_h}
35
38
  end
36
39
 
37
- # def self.error_fields(filter=nil)
38
- # @f.records.
39
- # end
40
+ def self.error_messages
41
+ @f.messages.select { |_,v| !v.undefined_records.empty? }
42
+ end
43
+
44
+ def self.error_fields(filter)
45
+ message = get_message_type filter
46
+ message.records.select{|x| !x.error_fields.empty?}.collect{|y| y.error_fields}
47
+ end
40
48
 
41
49
  # def self.filter_by_scope(filter)
42
50
  # valid = Static.scope.include? filter
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+ require 'fitreader'
3
+
4
+ describe Fitreader do
5
+ describe 'has functioning interfaces' do
6
+ before do
7
+ @path = File.join(File.dirname(__FILE__), '2016-04-09-13-19-18.fit')
8
+ Fitreader.read(@path)
9
+ end
10
+
11
+ it 'has a valid header' do
12
+ expect(Fitreader.header).not_to be_nil
13
+ expect(Fitreader.header.num_records).to be(89139)
14
+ end
15
+
16
+ it 'has valid records' do
17
+ expect(Fitreader.available_records).not_to be_nil
18
+ expect(Fitreader.available_records.length).to be(14)
19
+ end
20
+
21
+ it 'can fetch an existing message by number' do
22
+ expect(Fitreader.get_message_type 18).to be_a(Fitreader::MessageType)
23
+ end
24
+ it 'can fetch an existing message by name' do
25
+ expect(Fitreader.get_message_type :session).to be_a(Fitreader::MessageType)
26
+ end
27
+ it 'cannot fetch a non existing message by number' do
28
+ expect(Fitreader.get_message_type 1234).to be(nil)
29
+ end
30
+ it 'cannot fetch a non existing message by name' do
31
+ expect(Fitreader.get_message_type :non_existant).to be(nil)
32
+ end
33
+
34
+ it 'can return name-value data' do
35
+ expect(Fitreader.record_values :session).to be_a(Array)
36
+ end
37
+ it 'can return error-fields' do
38
+ expect(Fitreader.error_fields :record).to be_a(Array)
39
+ end
40
+
41
+ it 'can return error-messages' do
42
+ expect(Fitreader.error_messages).to be_a(Hash)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ require 'fitreader'
2
+
3
+ describe Fitreader do
4
+ describe 'read fit file' do
5
+ before do
6
+ @path = File.join(File.dirname(__FILE__), '2016-04-09-13-19-18.fit')
7
+ end
8
+
9
+ it 'test file exists' do
10
+ expect(File.exist?(@path)).to eql(true)
11
+ end
12
+
13
+ it 'parses file without exceptions' do
14
+ expect { Fitreader.read(@path) }.not_to raise_error
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+ require 'fitreader'
3
+
4
+ describe Fitreader::Static do
5
+ it 'const is loaded' do
6
+ expect(Fitreader::Static.enums.class).to eql(Hash)
7
+ end
8
+
9
+ it 'const has data' do
10
+ expect(Fitreader::Static.enums[:enum_file][1]).to eql(:device)
11
+ end
12
+
13
+ it 'scope is readable' do
14
+ expect(Fitreader::Static.scope).to include(:ride)
15
+ expect(Fitreader::Static.scope).not_to include(:bike)
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fitreader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Brodie
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-25 00:00:00.000000000 Z
11
+ date: 2016-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: bindata
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
69
  description:
84
70
  email:
85
71
  - richard@samsari.org
@@ -105,6 +91,7 @@ files:
105
91
  - lib/fitreader/field_definition.rb
106
92
  - lib/fitreader/file_header.rb
107
93
  - lib/fitreader/fitfile.rb
94
+ - lib/fitreader/message_type.rb
108
95
  - lib/fitreader/record.rb
109
96
  - lib/fitreader/record_header.rb
110
97
  - lib/fitreader/sdk/constants.yml
@@ -113,7 +100,9 @@ files:
113
100
  - lib/fitreader/version.rb
114
101
  - schema
115
102
  - spec/2016-04-09-13-19-18.fit
116
- - spec/fitreader_spec.rb
103
+ - spec/fitreader_interfaces_spec.rb
104
+ - spec/fitreader_readfile_spec.rb
105
+ - spec/fitreader_static_spec.rb
117
106
  - test/fitreader_test.rb
118
107
  - test/test_helper.rb
119
108
  homepage: https://github.com/samsari/fitreader
@@ -142,6 +131,8 @@ specification_version: 4
142
131
  summary: Library for reading FIT files generated by Garmin devices.
143
132
  test_files:
144
133
  - spec/2016-04-09-13-19-18.fit
145
- - spec/fitreader_spec.rb
134
+ - spec/fitreader_interfaces_spec.rb
135
+ - spec/fitreader_readfile_spec.rb
136
+ - spec/fitreader_static_spec.rb
146
137
  - test/fitreader_test.rb
147
138
  - test/test_helper.rb
@@ -1,55 +0,0 @@
1
- # encoding: utf-8
2
- require 'fitreader'
3
-
4
- describe Fitreader::Static do
5
- it 'const is loaded' do
6
- expect(Fitreader::Static.enums.class).to eql(Hash)
7
- end
8
-
9
- it 'const has data' do
10
- expect(Fitreader::Static.enums[:enum_file][1]).to eql(:device)
11
- end
12
-
13
- it 'scope is readable' do
14
- expect(Fitreader::Static.scope).to include(:ride)
15
- expect(Fitreader::Static.scope).not_to include(:bike)
16
- end
17
- end
18
-
19
- describe Fitreader do
20
- describe 'read fit file' do
21
- before do
22
- @path = File.join(File.dirname(__FILE__), '2016-04-09-13-19-18.fit')
23
- end
24
-
25
- it 'test file exists' do
26
- expect(File.exist?(@path)).to eql(true)
27
- end
28
-
29
- it 'parses file without exceptions' do
30
- expect { Fitreader.read(@path) }.not_to raise_error
31
- end
32
- end
33
-
34
- describe 'has functioning interfaces' do
35
- before do
36
- @path = File.join(File.dirname(__FILE__), '2016-04-09-13-19-18.fit')
37
- Fitreader.read(@path)
38
- end
39
-
40
- it 'has a valid header' do
41
- expect(Fitreader.header).not_to be_nil
42
- expect(Fitreader.header.num_records).to be(89139)
43
- end
44
-
45
- it 'has valid records' do
46
- expect(Fitreader.available_records).not_to be_nil
47
- expect(Fitreader.available_records.length).to be(14)
48
- binding.pry
49
- end
50
-
51
- it 'can return name-value data' do
52
- expect(Fitreader.record_values :session).to be(Array)
53
- end
54
- end
55
- end