fit4ruby 3.5.0 → 3.9.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
  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