fit4ruby 3.7.0 → 3.10.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: 1867e2f6e32d331216b372024ba0607837bb5dc3baf7daad107b5a23d6465aa6
4
- data.tar.gz: 25f0423ccf5e68259f48d02f35400fa2a723a9f7209613f77eca2631d056f2d0
3
+ metadata.gz: d40c0fc77ae8184b42aa80643dfb6b0f036b3eab7f37bdb8ee66b76a9feaafc5
4
+ data.tar.gz: 6e8ce1d3472e67a3c7ba58200fdf6b395e009349e68dca0618a92b3176f8ff19
5
5
  SHA512:
6
- metadata.gz: 05641db1523d5e8a9b8ab939d4b65def4e5a50ca6bbcae3fb1c727dc7d641cfeda6d7a5e892b38819b840b0a2222e0d775d20bf4f93ae4af6db2c79b4b6753b3
7
- data.tar.gz: f4b7d619de81444329009c7a188f08bd4684524ecc1654a57b77df147a0e22730658d57247e30729f619a0ab2c6fbd6fce796908df05ff2d409a06a909656298
6
+ metadata.gz: 366a5e5b4f55ee4918c822e26eda16d6381a1e872b8af68504283def72f096e00717dc68f9bd52403d8e6662c4e2aff83d12d26e201d568794bca407513b5bd8
7
+ data.tar.gz: a4eda75c90f65d26b9786570ee0fa07e578a3d627996170385411305b17a7a9697b043de70b335e485f3ca5780cdb0157506d34212ab0c54947c4111fb951fbf
data/README.md CHANGED
@@ -10,7 +10,7 @@ me comments and patches. It was developed to form the back-end of
10
10
 
11
11
  ## Supported Devices
12
12
 
13
- Tested devices: Garmin FR620
13
+ Tested devices: Garmin FR620, Fenix 3, Fenix 5, Fenix 5X, Fenix 6X
14
14
 
15
15
  Other Garmin devices that generate FIT files may work as well. Since I
16
16
  don't have any other devices, I can't add support for them.
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', '~>12.0.0')
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
@@ -30,48 +30,72 @@ require 'fit4ruby/HRV'
30
30
  require 'fit4ruby/HeartRateZones'
31
31
  require 'fit4ruby/Event'
32
32
  require 'fit4ruby/PersonalRecords'
33
+ require 'fit4ruby/Workout'
34
+ require 'fit4ruby/WorkoutStep'
33
35
 
34
36
  module Fit4Ruby
35
37
 
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.
38
+ # Activity files are arguably the most common type of FIT file. The Activity
39
+ # class represents the top-level structure of an activity FIT file.
40
+ # It holds references to all other data structures. Each of the objects it
41
+ # references are direct equivalents of the message record structures used in
42
+ # the FIT file.
39
43
  class Activity < FitDataRecord
40
44
 
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
45
+ # These symbols are a complete list of all the sub-sections that an
46
+ # activity FIT file may contain. This list is used to generate accessors,
47
+ # instance variables and other sections of code. Some are just simple
48
+ # instance variables, but the majority can appear multiple times and hence
49
+ # are stored in an Array.
50
+ FILE_SECTIONS = [
51
+ :file_id,
52
+ :file_creator,
53
+ :events,
54
+ :device_infos,
55
+ :data_sources,
56
+ :epo_data,
57
+ :user_profiles,
58
+ :user_data,
59
+ :workouts,
60
+ :workout_steps,
61
+ :sensor_settings,
62
+ :developer_data_ids,
63
+ :field_descriptions,
64
+ :records,
65
+ :hrv,
66
+ :laps,
67
+ :lengths,
68
+ :heart_rate_zones,
69
+ :physiological_metrics,
70
+ :sessions,
71
+ :personal_records
72
+ ]
73
+
74
+ attr_accessor *FILE_SECTIONS
46
75
 
47
76
  # Create a new Activity object.
48
77
  # @param field_values [Hash] A Hash that provides initial values for
49
78
  # certain fields of the FitDataRecord.
50
79
  def initialize(field_values = {})
51
80
  super('activity')
52
- @meta_field_units['total_gps_distance'] = 'm'
53
- @num_sessions = 0
54
81
 
55
- @file_id = FileId.new
56
- @field_descriptions = []
57
- @developer_data_ids = []
82
+ # The variables hold references to other parts of the FIT file. These
83
+ # can either be direct references to a certain FIT file section or an
84
+ # Array in case the section can appear multiple times in the FIT file.
85
+ @file_id = new_file_id()
86
+ @file_creator = new_file_creator()
58
87
  @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 = []
88
+ # Initialize the remaining variables as empty Array.
89
+ FILE_SECTIONS.each do |fs|
90
+ ivar_name = '@' + fs.to_s
91
+ unless instance_variable_defined?(ivar_name)
92
+ instance_variable_set(ivar_name, [])
93
+ end
94
+ end
74
95
 
96
+ # The following variables hold derived or auxilliary information that
97
+ # are not directly part of the FIT file.
98
+ @meta_field_units['total_gps_distance'] = 'm'
75
99
  @cur_session_laps = []
76
100
 
77
101
  @cur_lap_records = []
@@ -99,11 +123,6 @@ module Fit4Ruby
99
123
  end
100
124
  @device_infos.each.with_index { |d, index| d.check(index) }
101
125
  @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
126
 
108
127
  # Records must have consecutively growing timestamps and distances.
109
128
  ts = Time.parse('1989-12-31')
@@ -308,13 +327,17 @@ module Fit4Ruby
308
327
  def write(io, id_mapper)
309
328
  @file_id.write(io, id_mapper)
310
329
  @file_creator.write(io, id_mapper)
330
+ @epo_data.write(io, id_mapper) if @epo_data
331
+
332
+ ary_ivars = []
333
+ FILE_SECTIONS.each do |fs|
334
+ ivar_name = '@' + fs.to_s
335
+ if (ivar = instance_variable_get(ivar_name)) && ivar.respond_to?(:sort)
336
+ ary_ivars += ivar
337
+ end
338
+ end
311
339
 
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|
340
+ ary_ivars.sort.each do |s|
318
341
  s.write(io, id_mapper)
319
342
  end
320
343
  super
@@ -386,6 +409,22 @@ module Fit4Ruby
386
409
  new_fit_data_record('user_profile', field_values)
387
410
  end
388
411
 
412
+ # Add a new Workout to the Activity.
413
+ # @param field_values [Hash] A Hash that provides initial values for
414
+ # certain fields of the FitDataRecord.
415
+ # @return [UserProfile]
416
+ def new_workout(field_values = {})
417
+ new_fit_data_record('workout', field_values)
418
+ end
419
+
420
+ # Add a new WorkoutSet to the Activity.
421
+ # @param field_values [Hash] A Hash that provides initial values for
422
+ # certain fields of the FitDataRecord.
423
+ # @return [UserProfile]
424
+ def new_workout_set(field_values = {})
425
+ new_fit_data_record('workout_set', field_values)
426
+ end
427
+
389
428
  # Add a new PhysiologicalMetrics to the Activity.
390
429
  # @param field_values [Hash] A Hash that provides initial values for
391
430
  # certain fields of the FitDataRecord.
@@ -464,18 +503,17 @@ module Fit4Ruby
464
503
  # @return [TrueClass/FalseClass] true if both Activities are equal,
465
504
  # otherwise false.
466
505
  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
506
+ return false unless super(a)
507
+
508
+ FILE_SECTIONS.each do |fs|
509
+ ivar_name = '@' + fs.to_s
510
+ ivar = instance_variable_get(ivar_name)
511
+ a_ivar = a.instance_variable_get(ivar_name)
512
+
513
+ return false unless ivar == a_ivar
514
+ end
515
+
516
+ true
479
517
  end
480
518
 
481
519
  # Create a new FitDataRecord.
@@ -504,6 +542,10 @@ module Fit4Ruby
504
542
  @data_sources << (record = DataSources.new(field_values))
505
543
  when 'user_data'
506
544
  @user_data << (record = UserData.new(field_values))
545
+ when 'workout'
546
+ @workouts << (record = Workout.new(field_values))
547
+ when 'workout_step'
548
+ @workout_steps << (record = WorkoutStep.new(field_values))
507
549
  when 'user_profile'
508
550
  @user_profiles << (record = UserProfile.new(field_values))
509
551
  when 'physiological_metrics'
@@ -521,7 +563,6 @@ module Fit4Ruby
521
563
  # Ensure that all previous records have been assigned to a lap.
522
564
  record = create_new_lap(lap_field_values)
523
565
  end
524
- @num_sessions += 1
525
566
  @sessions << (record = Session.new(@cur_session_laps, @lap_counter,
526
567
  field_values))
527
568
  @cur_session_laps = []
@@ -530,7 +571,7 @@ module Fit4Ruby
530
571
  when 'length'
531
572
  record = create_new_length(field_values)
532
573
  when 'record'
533
- record = Record.new(field_values)
574
+ record = Record.new(self, field_values)
534
575
  @cur_lap_records << record
535
576
  @cur_length_records << record
536
577
  @records << record
@@ -547,10 +588,31 @@ module Fit4Ruby
547
588
  record
548
589
  end
549
590
 
591
+ def export
592
+ # Collect all records in a consistent order.
593
+ records = []
594
+ FILE_SECTIONS.each do |fs|
595
+ ivar_name = '@' + fs.to_s
596
+ ivar = instance_variable_get(ivar_name)
597
+
598
+ next unless ivar
599
+
600
+ if ivar.respond_to?(:sort) and ivar.respond_to?(:empty?)
601
+ records += ivar.sort unless ivar.empty?
602
+ else
603
+ records << ivar if ivar
604
+ end
605
+ end
606
+
607
+ records.map do |record|
608
+ record.export
609
+ end
610
+ end
611
+
550
612
  private
551
613
 
552
614
  def create_new_lap(field_values)
553
- lap = Lap.new(@cur_lap_records, @laps.last,
615
+ lap = Lap.new(self, @cur_lap_records, @laps.last,
554
616
  field_values,
555
617
  @length_counter, @cur_lap_lengths)
556
618
  lap.message_index = @lap_counter - 1
@@ -573,6 +635,7 @@ module Fit4Ruby
573
635
 
574
636
  length
575
637
  end
638
+
576
639
  end
577
640
 
578
641
  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
+
@@ -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,12 +53,6 @@ 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
@@ -48,10 +60,10 @@ module Fit4Ruby
48
60
  return
49
61
  end
50
62
 
51
- name = "_#{@developer_data_index}_#{@field_name}"
52
63
  # A fit file may include multiple definitions of the same field. We
53
64
  # ignore all subsequent definitions.
54
- return if msg.has_field?(name)
65
+ return if msg.has_field?(full_field_name(fit_entity.top_level_record.
66
+ developer_data_ids))
55
67
 
56
68
  options = {}
57
69
  options[:scale] = @scale if @scale
@@ -60,7 +72,7 @@ module Fit4Ruby
60
72
  options[:unit] = @units
61
73
  msg.field(@field_definition_number,
62
74
  FIT_TYPE_DEFS[@fit_base_type_id & 0x7F][1],
63
- name, options)
75
+ @full_field_name, options)
64
76
  end
65
77
 
66
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