fit4ruby 3.5.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3c3c8eeb82d8dcde4114e43cc7e99882c61eef8cbf204bff47abe8658523dbe
4
- data.tar.gz: 02aa11ffebeabab221d493199e402a51ffe3bcbf88d023ca2734c05b22e64362
3
+ metadata.gz: 1f0a74c755c07cbbcfd9b17b9efeb16a62696fd9076c9ece178e00e51b3ce075
4
+ data.tar.gz: 81ee48cea068828a6ad08c5f4f274270ec96179159e585ecc9068eccbf1be3a4
5
5
  SHA512:
6
- metadata.gz: 209b75663bb89a9a52185c632971039651cf71a0ee9758a56b4accfa1559f360123beaa4f055b0d50f064adc43ca0c951050806aeed38d21b7db5a014b597288
7
- data.tar.gz: ac995d33edeb4c6c35bb808896933f5f76795efaeb500e73b95d9b270afcc68ff3eca04c1ddd95ae7367c27b6f531576c65d9407271277cadbc732cf5a457aa8
6
+ metadata.gz: 0f140497b1f269075e912da5988238db3a279ef5baf20e4ce29f3b960307c3b87a4ada116283f17f5965a7e0e25e9b3e5bcdce7306b14ae49cb16d837692b239
7
+ data.tar.gz: 829a3ff7cee37f26218915d8370a0dd44fe6c24152c634f073fb0d25a543c3e50783188fe81f6d5cbbb00af42974eba1aed51f59ee25a108718773b7a65f8ad8
data/Gemfile.lock CHANGED
@@ -37,7 +37,7 @@ DEPENDENCIES
37
37
  bundler (>= 1.6.4)
38
38
  fit4ruby!
39
39
  pry (>= 0.12)
40
- rake (~> 0.9.6)
40
+ rake (~> 12.0.0)
41
41
  rspec (>= 3.8)
42
42
  yard (~> 0.9.20)
43
43
 
data/fit4ruby.gemspec CHANGED
@@ -24,9 +24,9 @@ EOT
24
24
  spec.require_paths = ["lib"]
25
25
  spec.required_ruby_version = '>=2.0'
26
26
 
27
- spec.add_dependency('bindata', '=2.3.0')
27
+ spec.add_dependency('bindata', '~>2.4.8')
28
28
  spec.add_development_dependency('yard', '~>0.9.20')
29
- spec.add_development_dependency('rake', '~>0.9.6')
29
+ spec.add_development_dependency('rake', '~>13.0.3')
30
30
  spec.add_development_dependency('bundler', '>=1.6.4')
31
31
  spec.add_development_dependency('rspec', '>=3.8')
32
32
  spec.add_development_dependency('pry', '>=0.12')
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = Activity.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014, 2015 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015, 2020 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
@@ -33,45 +33,65 @@ require 'fit4ruby/PersonalRecords'
33
33
 
34
34
  module Fit4Ruby
35
35
 
36
- # This is the most important class of this library. It holds references to
37
- # all other data structures. Each of the objects it references are direct
38
- # equivalents of the message record structures used in the FIT file.
36
+ # Activity files are arguably the most common type of FIT file. The Activity
37
+ # class represents the top-level structure of an activity FIT file.
38
+ # It holds references to all other data structures. Each of the objects it
39
+ # references are direct equivalents of the message record structures used in
40
+ # the FIT file.
39
41
  class Activity < FitDataRecord
40
42
 
41
- attr_accessor :file_id, :field_descriptions, :developer_data_ids, :epo_data,
42
- :file_creator, :device_infos, :sensor_settings, :data_sources,
43
- :user_data, :user_profiles, :physiological_metrics,
44
- :sessions, :laps, :records, :lengths, :hrv,
45
- :heart_rate_zones, :events, :personal_records
43
+ # These symbols are a complete list of all the sub-sections that an
44
+ # activity FIT file may contain. This list is used to generate accessors,
45
+ # instance variables and other sections of code. Some are just simple
46
+ # instance variables, but the majority can appear multiple times and hence
47
+ # are stored in an Array.
48
+ FILE_SECTIONS = [
49
+ :file_id,
50
+ :file_creator,
51
+ :events,
52
+ :device_infos,
53
+ :data_sources,
54
+ :epo_data,
55
+ :user_profiles,
56
+ :user_data,
57
+ :sensor_settings,
58
+ :developer_data_ids,
59
+ :field_descriptions,
60
+ :records,
61
+ :hrv,
62
+ :laps,
63
+ :lengths,
64
+ :heart_rate_zones,
65
+ :physiological_metrics,
66
+ :sessions,
67
+ :personal_records
68
+ ]
69
+
70
+ attr_accessor *FILE_SECTIONS
46
71
 
47
72
  # Create a new Activity object.
48
73
  # @param field_values [Hash] A Hash that provides initial values for
49
74
  # certain fields of the FitDataRecord.
50
75
  def initialize(field_values = {})
51
76
  super('activity')
52
- @meta_field_units['total_gps_distance'] = 'm'
53
- @num_sessions = 0
54
77
 
55
- @file_id = FileId.new
56
- @field_descriptions = []
57
- @developer_data_ids = []
78
+ # The variables hold references to other parts of the FIT file. These
79
+ # can either be direct references to a certain FIT file section or an
80
+ # Array in case the section can appear multiple times in the FIT file.
81
+ @file_id = new_file_id()
82
+ @file_creator = new_file_creator()
58
83
  @epo_data = nil
59
- @file_creator = FileCreator.new
60
- @device_infos = []
61
- @sensor_settings = []
62
- @data_sources = []
63
- @user_data = []
64
- @user_profiles = []
65
- @physiological_metrics = []
66
- @events = []
67
- @sessions = []
68
- @laps = []
69
- @lengths = []
70
- @records = []
71
- @hrv = []
72
- @heart_rate_zones = []
73
- @personal_records = []
84
+ # Initialize the remaining variables as empty Array.
85
+ FILE_SECTIONS.each do |fs|
86
+ ivar_name = '@' + fs.to_s
87
+ unless instance_variable_defined?(ivar_name)
88
+ instance_variable_set(ivar_name, [])
89
+ end
90
+ end
74
91
 
92
+ # The following variables hold derived or auxilliary information that
93
+ # are not directly part of the FIT file.
94
+ @meta_field_units['total_gps_distance'] = 'm'
75
95
  @cur_session_laps = []
76
96
 
77
97
  @cur_lap_records = []
@@ -99,11 +119,6 @@ module Fit4Ruby
99
119
  end
100
120
  @device_infos.each.with_index { |d, index| d.check(index) }
101
121
  @sensor_settings.each.with_index { |s, index| s.check(index) }
102
- unless @num_sessions == @sessions.count
103
- Log.fatal "Activity record requires #{@num_sessions}, but "
104
- "#{@sessions.length} session records were found in the "
105
- "FIT file."
106
- end
107
122
 
108
123
  # Records must have consecutively growing timestamps and distances.
109
124
  ts = Time.parse('1989-12-31')
@@ -308,13 +323,17 @@ module Fit4Ruby
308
323
  def write(io, id_mapper)
309
324
  @file_id.write(io, id_mapper)
310
325
  @file_creator.write(io, id_mapper)
326
+ @epo_data.write(io, id_mapper) if @epo_data
311
327
 
312
- (@field_descriptions + @developer_data_ids +
313
- @device_infos + @sensor_settings +
314
- @data_sources + @user_profiles +
315
- @physiological_metrics + @events +
316
- @sessions + @laps + @records + @lengths +
317
- @heart_rate_zones + @personal_records).sort.each do |s|
328
+ ary_ivars = []
329
+ FILE_SECTIONS.each do |fs|
330
+ ivar_name = '@' + fs.to_s
331
+ if (ivar = instance_variable_get(ivar_name)) && ivar.respond_to?(:sort)
332
+ ary_ivars += ivar
333
+ end
334
+ end
335
+
336
+ ary_ivars.sort.each do |s|
318
337
  s.write(io, id_mapper)
319
338
  end
320
339
  super
@@ -464,18 +483,17 @@ module Fit4Ruby
464
483
  # @return [TrueClass/FalseClass] true if both Activities are equal,
465
484
  # otherwise false.
466
485
  def ==(a)
467
- super(a) && @file_id == a.file_id &&
468
- @file_creator == a.file_creator &&
469
- @field_descriptions == a.field_descriptions &&
470
- @developer_data_ids == a.developer_data_ids &&
471
- @device_infos == a.device_infos &&
472
- @sensor_settings == a.sensor_settings &&
473
- @data_sources == a.data_sources &&
474
- @physiological_metrics == a.physiological_metrics &&
475
- @user_profiles == a.user_profiles &&
476
- @heart_rate_zones == a.heart_rate_zones &&
477
- @events == a.events &&
478
- @sessions == a.sessions && personal_records == a.personal_records
486
+ return false unless super(a)
487
+
488
+ FILE_SECTIONS.each do |fs|
489
+ ivar_name = '@' + fs.to_s
490
+ ivar = instance_variable_get(ivar_name)
491
+ a_ivar = a.instance_variable_get(ivar_name)
492
+
493
+ return false unless ivar == a_ivar
494
+ end
495
+
496
+ true
479
497
  end
480
498
 
481
499
  # Create a new FitDataRecord.
@@ -521,7 +539,6 @@ module Fit4Ruby
521
539
  # Ensure that all previous records have been assigned to a lap.
522
540
  record = create_new_lap(lap_field_values)
523
541
  end
524
- @num_sessions += 1
525
542
  @sessions << (record = Session.new(@cur_session_laps, @lap_counter,
526
543
  field_values))
527
544
  @cur_session_laps = []
@@ -530,7 +547,7 @@ module Fit4Ruby
530
547
  when 'length'
531
548
  record = create_new_length(field_values)
532
549
  when 'record'
533
- record = Record.new(field_values)
550
+ record = Record.new(self, field_values)
534
551
  @cur_lap_records << record
535
552
  @cur_length_records << record
536
553
  @records << record
@@ -547,10 +564,31 @@ module Fit4Ruby
547
564
  record
548
565
  end
549
566
 
567
+ def export
568
+ # Collect all records in a consistent order.
569
+ records = []
570
+ FILE_SECTIONS.each do |fs|
571
+ ivar_name = '@' + fs.to_s
572
+ ivar = instance_variable_get(ivar_name)
573
+
574
+ next unless ivar
575
+
576
+ if ivar.respond_to?(:sort) and ivar.respond_to?(:empty?)
577
+ records += ivar.sort unless ivar.empty?
578
+ else
579
+ records << ivar if ivar
580
+ end
581
+ end
582
+
583
+ records.map do |record|
584
+ record.export
585
+ end
586
+ end
587
+
550
588
  private
551
589
 
552
590
  def create_new_lap(field_values)
553
- lap = Lap.new(@cur_lap_records, @laps.last,
591
+ lap = Lap.new(self, @cur_lap_records, @laps.last,
554
592
  field_values,
555
593
  @length_counter, @cur_lap_lengths)
556
594
  lap.message_index = @lap_counter - 1
@@ -573,6 +611,7 @@ module Fit4Ruby
573
611
 
574
612
  length
575
613
  end
614
+
576
615
  end
577
616
 
578
617
  end
@@ -0,0 +1,36 @@
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) 2020 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
+ # Some FIT message field names conflict with BinData reserved names. We use
16
+ # this translation method to map the conflicting names to BinData compatible
17
+ # names.
18
+ module BDFieldNameTranslator
19
+
20
+ BD_DICT = {
21
+ 'array' => '_array',
22
+ 'type' => '_type'
23
+ }
24
+
25
+ def to_bd_field_name(name)
26
+ if (bd_name = BD_DICT[name])
27
+ return bd_name
28
+ end
29
+
30
+ name
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
@@ -32,6 +32,43 @@ module Fit4Ruby
32
32
  @timestamp <=> fdr.timestamp
33
33
  end
34
34
 
35
+ def numeric_manufacturer
36
+ if @manufacturer && @manufacturer.is_a?(String)
37
+ if @manufacturer[0..17] == 'Undocumented value'
38
+ return @manufacturer[18..-1].to_i
39
+ else
40
+ return GlobalFitDictionaries['manufacturer'].
41
+ value_by_name(@manufacturer)
42
+ end
43
+ end
44
+
45
+ Log.fatal "Unexpected @manufacturer (#{@manufacturer}) value"
46
+ end
47
+
48
+ def numeric_product
49
+ # The numeric product ID must be an integer or nil. In case the
50
+ # dictionary did not contain an entry for the numeric ID in the fit file
51
+ # the @garmin_product or @product variables contain a String starting
52
+ # with 'Undocumented value ' followed by the ID.
53
+ if @garmin_product && @garmin_product.is_a?(String)
54
+ if @garmin_product[0..17] == 'Undocumented value'
55
+ return @garmin_product[18..-1].to_i
56
+ else
57
+ return GlobalFitDictionaries['garmin_product'].
58
+ value_by_name(@garmin_product)
59
+ end
60
+ elsif @product && @product.is_a?(String)
61
+ if @product[0..17] == 'Undocumented value'
62
+ return @product[18..-1].to_i
63
+ else
64
+ return GlobalFitDictionaries['product'].value_by_name(@product)
65
+ end
66
+ end
67
+
68
+ Log.fatal "Unexpected @product (#{@product}) or " +
69
+ "@garmin_product (#{@garmin_product}) values"
70
+ end
71
+
35
72
  def check(index)
36
73
  unless @device_index
37
74
  Log.fatal 'device info record must have a device_index'
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby -w
2
+ # encoding: UTF-8
3
+ #
4
+ # = FDR_DevField_Extension.rb -- Fit4Ruby - FIT file processing library for Ruby
5
+ #
6
+ # Copyright (c) 2020 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
+ # This module extends FitDataRecord derived classes in case they have
16
+ # developer fields.
17
+ module FDR_DevField_Extension
18
+
19
+ def create_dev_field_instance_variables
20
+ # Create instance variables for developer fields
21
+ @dev_field_descriptions = {}
22
+ @top_level_record.field_descriptions.each do |field_description|
23
+ # Only create instance variables if the FieldDescription is for this
24
+ # message number.
25
+ if field_description.native_mesg_num == @message.number
26
+ name = field_description.full_field_name(@top_level_record.
27
+ developer_data_ids)
28
+ create_instance_variable(name)
29
+ @dev_field_descriptions[name] = field_description
30
+ end
31
+ end
32
+ end
33
+
34
+ def each_developer_field
35
+ @top_level_record.field_descriptions.each do |field_description|
36
+ # Only create instance variables if the FieldDescription is for this
37
+ # message number.
38
+ if field_description.native_mesg_num == @message.number
39
+ name = field_description.full_field_name(@top_level_record.
40
+ developer_data_ids)
41
+ yield(field_description, instance_variable_get('@' + name))
42
+ end
43
+ end
44
+ end
45
+
46
+ def get_unit_by_name(name)
47
+ if /[A-Za-z_]+_[A-F0-9]{16}/ =~ name
48
+ @dev_field_descriptions[name].units
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def export
55
+ message = super
56
+
57
+ each_developer_field do |field_description, ivar|
58
+ field = field_description.message.fields_by_number[
59
+ field_description.native_field_num]
60
+ fit_value = field.native_to_fit(ivar)
61
+ unless field.is_undefined?(fit_value)
62
+ fld = {
63
+ 'number' => field_description.native_field_num | 128,
64
+ 'value' => field.fit_to_native(fit_value),
65
+ #'human' => field.to_human(fit_value),
66
+ 'type' => field.type
67
+ }
68
+ fld['unit'] = field.opts[:unit] if field.opts[:unit]
69
+ fld['scale'] = field.opts[:scale] if field.opts[:scale]
70
+
71
+ message['fields'][field.name] = fld
72
+ end
73
+ end
74
+
75
+ message
76
+ end
77
+
78
+ end
79
+
80
+ end
81
+
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = FieldDescription.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2017, 2018, 2019 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2017, 2018, 2019, 2020 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
@@ -25,6 +25,24 @@ module Fit4Ruby
25
25
  def initialize(field_values = {})
26
26
  super('field_description')
27
27
  set_field_values(field_values)
28
+
29
+ @full_field_name = nil
30
+ end
31
+
32
+ def full_field_name(developer_data_ids)
33
+ return @full_field_name if @full_field_name
34
+
35
+ if @developer_data_index >=
36
+ developer_data_ids.size
37
+ Log.error "Developer data index #{@developer_data_index} is too large"
38
+ return
39
+ end
40
+
41
+ app_id = developer_data_ids[@developer_data_index].application_id
42
+ # Convert the byte array with the app ID into a 16 character hex string.
43
+ app_id_str = app_id.map { |i| '%02X' % i }.join('')
44
+ @full_field_name =
45
+ "#{@field_name.gsub(/[^A-Za-z0-9_]/, '_')}_#{app_id_str}"
28
46
  end
29
47
 
30
48
  def create_global_definition(fit_entity)
@@ -35,18 +53,18 @@ module Fit4Ruby
35
53
  return
36
54
  end
37
55
 
38
- if @developer_data_index >=
39
- fit_entity.top_level_record.developer_data_ids.size
40
- Log.error "Developer data index #{@developer_data_index} is too large"
41
- return
42
- end
43
-
44
56
  msg = messages[@native_mesg_num] ||
45
57
  messages.message(@native_mesg_num, gfm.name)
46
58
  unless (@fit_base_type_id & 0x7F) < FIT_TYPE_DEFS.size
47
59
  Log.error "fit_base_type_id #{@fit_base_type_id} is too large"
48
60
  return
49
61
  end
62
+
63
+ # A fit file may include multiple definitions of the same field. We
64
+ # ignore all subsequent definitions.
65
+ return if msg.has_field?(full_field_name(fit_entity.top_level_record.
66
+ developer_data_ids))
67
+
50
68
  options = {}
51
69
  options[:scale] = @scale if @scale
52
70
  options[:offset] = @offset if @offset
@@ -54,7 +72,7 @@ module Fit4Ruby
54
72
  options[:unit] = @units
55
73
  msg.field(@field_definition_number,
56
74
  FIT_TYPE_DEFS[@fit_base_type_id & 0x7F][1],
57
- "_#{@developer_data_index}_#{@field_name}", options)
75
+ @full_field_name, options)
58
76
  end
59
77
 
60
78
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = FileId.rb -- Fit4Ruby - FIT file processing library for Ruby
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2020 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
@@ -23,6 +23,7 @@ module Fit4Ruby
23
23
  @time_created = Time.at(Time.now.to_i)
24
24
  @manufacturer = 'development'
25
25
  @type = 'activity'
26
+ @product = 0
26
27
 
27
28
  set_field_values(field_values)
28
29
  end