From 5590faccc2c96f5038463685407ea4e468c056c8 Mon Sep 17 00:00:00 2001 From: Chris Schlaeger Date: Mon, 26 Oct 2015 19:15:47 +0100 Subject: Add data sources table to activity view. --- lib/postrunner/Activity.rb | 8 +++- lib/postrunner/ActivityView.rb | 12 ++++- lib/postrunner/DataSources.rb | 100 ++++++++++++++++++++++++++++++++++++++++ lib/postrunner/DeviceList.rb | 57 ++++++++++++++++------- lib/postrunner/Log.rb | 25 ++++++++++ lib/postrunner/Main.rb | 17 +++---- lib/postrunner/RuntimeConfig.rb | 1 + spec/PostRunner_spec.rb | 45 +++++++++--------- spec/spec_helper.rb | 38 +++++++++++++-- 9 files changed, 248 insertions(+), 55 deletions(-) create mode 100644 lib/postrunner/DataSources.rb create mode 100644 lib/postrunner/Log.rb diff --git a/lib/postrunner/Activity.rb b/lib/postrunner/Activity.rb index e0dbc89..13f2cd9 100644 --- a/lib/postrunner/Activity.rb +++ b/lib/postrunner/Activity.rb @@ -13,6 +13,7 @@ require 'fit4ruby' require 'postrunner/ActivitySummary' +require 'postrunner/DataSources' require 'postrunner/ActivityView' require 'postrunner/Schema' require 'postrunner/QueryResult' @@ -201,6 +202,11 @@ module PostRunner @db.show_in_browser(@html_file) end + def sources + @fit_activity = load_fit_file unless @fit_activity + puts DataSources.new(self, @db.cfg[:unit_system]).to_s + end + def summary @fit_activity = load_fit_file unless @fit_activity puts ActivitySummary.new(self, @db.cfg[:unit_system], @@ -375,7 +381,7 @@ module PostRunner begin fit_activity = Fit4Ruby.read(fit_file, filter) rescue Fit4Ruby::Error - Log.fatal $! + Log.fatal "#{@fit_file} corrupted: #{$!}" end unless fit_activity diff --git a/lib/postrunner/ActivityView.rb b/lib/postrunner/ActivityView.rb index b36c5c2..a1bbc3b 100644 --- a/lib/postrunner/ActivityView.rb +++ b/lib/postrunner/ActivityView.rb @@ -67,14 +67,17 @@ module PostRunner :sub_type => @activity.activity_sub_type }).to_html(doc) TrackView.new(@activity).to_html(doc) - DeviceList.new(@activity.fit_activity).to_html(doc) UserProfileView.new(@activity.fit_activity, @unit_system). to_html(doc) + DeviceList.new(@activity.fit_activity).to_html(doc) } doc.div({ :class => 'right_col' }) { ChartView.new(@activity, @unit_system).to_html(doc) } } + doc.div({ :class => 'two_col' }) { + DataSources.new(@activity, @unit_system).to_html(doc) + } } } end @@ -91,12 +94,17 @@ body { } .left_col { float: left; - width: 400px; + width: 600px; } .right_col { float: right; width: 600px; } +.two_col { + margin: 0 auto; + clear: both; + width: 1210px; +} EOT end diff --git a/lib/postrunner/DataSources.rb b/lib/postrunner/DataSources.rb new file mode 100644 index 0000000..01ed9a8 --- /dev/null +++ b/lib/postrunner/DataSources.rb @@ -0,0 +1,100 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = DataSources.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2015 by Chris Schlaeger +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# + +require 'fit4ruby' + +require 'postrunner/FlexiTable' +require 'postrunner/ViewFrame' +require 'postrunner/DeviceList' + +module PostRunner + + # The DataSources objects can generate a table that lists all the data + # sources in chronological order that were in use during a workout. + class DataSources + + include Fit4Ruby::Converters + + # Create a DataSources object. + # @param activity [Activity] The activity to analyze. + # @param unit_system [Symbol] The unit system to use (:metric or + # :imperial ) + def initialize(activity, unit_system) + @activity = activity + @fit_activity = activity.fit_activity + @unit_system = unit_system + end + + def to_s + data_sources.to_s + end + + def to_html(doc) + ViewFrame.new("Data Sources", 1210, data_sources).to_html(doc) + end + + private + + def data_sources + session = @fit_activity.sessions[0] + + t = FlexiTable.new + t.enable_frame(false) + t.body + t.row([ 'Time', 'Distance', 'Mode', 'Distance', 'Speed', + 'Cadence', 'Elevation', 'Heart Rate', 'Power', 'Calories' ]) + start_time = session.start_time + @fit_activity.data_sources.each do |source| + t.cell(secsToHMS(source.timestamp - start_time)) + t.cell(distance(source.timestamp)) + t.cell(source.mode) + t.cell(device_name(source.distance)) + t.cell(device_name(source.speed)) + t.cell(device_name(source.cadence)) + t.cell(device_name(source.elevation)) + t.cell(device_name(source.heart_rate)) + t.cell(device_name(source.power)) + t.cell(device_name(source.calories)) + t.new_row + end + + t + end + + def device_name(index) + @fit_activity.device_infos.each do |device| + if device.device_index == index + return (DeviceList::DeviceTypeNames[device.device_type] || + device.device_type) + " [#{device.device_index}]" + end + end + + '' + end + + def distance(timestamp) + @fit_activity.records.each do |record| + if record.timestamp >= timestamp + unit = { :metric => 'km', :statute => 'mi'}[@unit_system] + value = record.get_as('distance', unit) + return '-' unless value + return "#{'%.2f %s' % [value, unit]}" + end + end + + '-' + end + + end + +end + diff --git a/lib/postrunner/DeviceList.rb b/lib/postrunner/DeviceList.rb index 813501f..7e379e2 100644 --- a/lib/postrunner/DeviceList.rb +++ b/lib/postrunner/DeviceList.rb @@ -20,6 +20,25 @@ module PostRunner include Fit4Ruby::Converters + DeviceTypeNames = { + 'acceleration' => 'Accelerometer', + 'antfs' => 'Main Unit', + 'barometric_pressure' => 'Barometer', + 'bike_cadence' => 'Bike Cadence', + 'bike_power' => 'Bike Power Meter', + 'bike_speed' => 'Bike Speed', + 'bike_speed_cadence' => 'Bike Speed + Cadence', + 'environment_sensor_legacy' => 'GPS', + 'gps' => 'GPS', + 'heart_rate' => 'Heart Rate Sensor', + 'running_dynamics' => 'Running Dynamics', + 'stride_speed_distance' => 'Footpod' + } + ProductNames = { + 'hrm_run_single_byte_product_id' => 'HRM Run', + 'hrm_run' => 'HRM Run' + } + def initialize(fit_activity) @fit_activity = fit_activity end @@ -38,63 +57,67 @@ module PostRunner tables = [] seen_indexes = [] @fit_activity.device_infos.reverse_each do |device| - next if seen_indexes.include?(device.device_index) || - device.manufacturer.nil? || - device.manufacturer == 'Undocumented value 0' || - device.device_type == 'Undocumented value 0' + next if seen_indexes.include?(device.device_index) tables << (t = FlexiTable.new) t.set_html_attrs(:style, 'margin-bottom: 15px') if tables.length != 1 t.body - t.cell('Manufacturer:', { :width => '40%' }) - t.cell(device.manufacturer.upcase, { :width => '60%' }) + t.cell('Index:', { :width => '40%' }) + t.cell(device.device_index.to_s, { :width => '60%' }) t.new_row + if (manufacturer = device.manufacturer) + t.cell('Manufacturer:', { :width => '40%' }) + t.cell(manufacturer.upcase, { :width => '60%' }) + t.new_row + end + if (product = %w( garmin dynastream dynastream_oem ).include?( - device.manufacturer) ? - device.garmin_product : device.product) + device.manufacturer) ? device.garmin_product : device.product) && + product != 0xFFFF # For unknown products the numerical ID will be returned. product = product.to_s unless product.is_a?(String) t.cell('Product:') # Beautify some product names. The others will just be upcased. - rename = { 'hrm_run_single_byte_product_id' => 'HRM Run', - 'hrm_run' => 'HRM Run' } - product = rename.include?(product) ? rename[product] : product.upcase + product = ProductNames.include?(product) ? + ProductNames[product] : product.upcase t.cell(product) t.new_row end + if (type = device.device_type) - rename = { 'heart_rate' => 'Heart Rate Sensor', - 'barometric_pressure' => 'Barometer', - 'position' => 'GPS', - 'stride_speed_distance' => 'Footpod', - 'running_dynamics' => 'Running Dynamics' } - type = rename[type] if rename.include?(type) + # Beautify some device type names. + type = DeviceTypeNames[type] if DeviceTypeNames.include?(type) t.cell('Device Type:') t.cell(type) t.new_row end + if device.serial_number t.cell('Serial Number:') t.cell(device.serial_number) t.new_row end + if device.software_version t.cell('Software Version:') t.cell(device.software_version) t.new_row end + if (rx_ok = device.rx_packets_ok) && (rx_err = device.rx_packets_err) t.cell('Packet Errors:') t.cell('%d%%' % ((rx_err.to_f / (rx_ok + rx_err)) * 100).to_i) t.new_row end + if device.battery_status t.cell('Battery Status:') t.cell(device.battery_status) t.new_row end + if device.cum_operating_time t.cell('Cumulated Operating Time:') t.cell(secsToDHMS(device.cum_operating_time)) diff --git a/lib/postrunner/Log.rb b/lib/postrunner/Log.rb new file mode 100644 index 0000000..be63e77 --- /dev/null +++ b/lib/postrunner/Log.rb @@ -0,0 +1,25 @@ +#!/usr/bin/env ruby -w +# encoding: UTF-8 +# +# = Log.rb -- PostRunner - Manage the data from your Garmin sport devices. +# +# Copyright (c) 2014, 2015 by Chris Schlaeger +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# + +require 'fit4ruby' + +module PostRunner + + # Use the Logger provided by Fit4Ruby for all console output. + Log = Fit4Ruby::ILogger.instance + Log.formatter = proc { |severity, datetime, progname, msg| + "#{severity == Logger::INFO ? '' : "#{severity}:"} #{msg}\n" + } + Log.level = Logger::INFO + +end + diff --git a/lib/postrunner/Main.rb b/lib/postrunner/Main.rb index 2605fe4..7dbb6be 100644 --- a/lib/postrunner/Main.rb +++ b/lib/postrunner/Main.rb @@ -11,11 +11,11 @@ # require 'optparse' -require 'logger' require 'fit4ruby' require 'perobs' require 'postrunner/version' +require 'postrunner/Log' require 'postrunner/RuntimeConfig' require 'postrunner/ActivitiesDB' require 'postrunner/MonitoringDB' @@ -23,13 +23,6 @@ require 'postrunner/EPO_Downloader' module PostRunner - # Use the Logger provided by Fit4Ruby for all console output. - Log = Fit4Ruby::ILogger.new(STDOUT) - Log.formatter = proc { |severity, datetime, progname, msg| - "#{severity == Logger::INFO ? '' : "#{severity}:"} #{msg}\n" - } - Log.level = Logger::INFO - class Main def initialize(args) @@ -159,6 +152,10 @@ show [ ] Show the referenced FIT activity in a web browser. If no reference is provided show the list of activities in the database. +sources [ ] + Show the data sources for the various measurements and how they + changed during the course of the activity. + summary Display the summary information for the FIT file. @@ -244,6 +241,8 @@ EOT else process_activities(args, :show) end + when 'sources' + process_activities(args, :sources) when 'summary' process_activities(args, :summary) when 'units' @@ -358,6 +357,8 @@ EOT @activities.set(activity, @attribute, @value) when :show activity.show + when :sources + activity.sources when :summary activity.summary else diff --git a/lib/postrunner/RuntimeConfig.rb b/lib/postrunner/RuntimeConfig.rb index 16115aa..9e91c15 100644 --- a/lib/postrunner/RuntimeConfig.rb +++ b/lib/postrunner/RuntimeConfig.rb @@ -28,6 +28,7 @@ module PostRunner :version => '0.0.0', :unit_system => :metric, :import_dir => nil, + :data_dir => dir, :html_dir => File.join(dir, 'html') } @config_file = File.join(dir, 'config.yml') diff --git a/spec/PostRunner_spec.rb b/spec/PostRunner_spec.rb index 4ca5a4a..59c5d0e 100644 --- a/spec/PostRunner_spec.rb +++ b/spec/PostRunner_spec.rb @@ -27,26 +27,25 @@ describe PostRunner::Main do end before(:all) do - @db_dir = File.join(File.dirname(__FILE__), '.postrunner') - FileUtils.rm_rf(@db_dir) - FileUtils.rm_rf('FILE1.FIT') - create_fit_file('FILE1.FIT', '2014-07-01-8:00') - create_fit_file('FILE2.FIT', '2014-07-02-8:00') + @work_dir = tmp_dir_name(__FILE__) + Dir.mkdir(@work_dir) + @db_dir = File.join(@work_dir, '.postrunner') + @file1 = File.join(@work_dir, 'FILE1.FIT') + @file2 = File.join(@work_dir, 'FILE2.FIT') + create_fit_file(@file1, '2014-07-01-8:00') + create_fit_file(@file2, '2014-07-02-8:00') end after(:all) do - FileUtils.rm_rf(@db_dir) - FileUtils.rm_rf('FILE1.FIT') - FileUtils.rm_rf('FILE2.FIT') - FileUtils::rm_rf('icons') + FileUtils.rm_rf(@work_dir) end it 'should abort without arguments' do - lambda { postrunner([]) }.should raise_error SystemExit + lambda { postrunner([]) }.should raise_error Fit4Ruby::Error end it 'should abort with bad command' do - lambda { postrunner(%w( foobar)) }.should raise_error SystemExit + lambda { postrunner(%w( foobar)) }.should raise_error Fit4Ruby::Error end it 'should support the -v option' do @@ -54,7 +53,7 @@ describe PostRunner::Main do end it 'should check a FIT file' do - postrunner(%w( check FILE1.FIT )) + postrunner([ 'check', @file1 ]) end it 'should list and empty archive' do @@ -62,7 +61,7 @@ describe PostRunner::Main do end it 'should import a FIT file' do - postrunner(%w( import FILE1.FIT )) + postrunner([ 'import', @file1 ]) end it 'should check the imported file' do @@ -70,20 +69,20 @@ describe PostRunner::Main do end it 'should check a FIT file' do - postrunner(%w( check FILE2.FIT )) + postrunner([ 'check', @file2 ]) end it 'should list the imported file' do - postrunner(%w( list )).index('FILE1.FIT').should be_a(Fixnum) + postrunner(%w( list )).index('FILE1').should be_a(Fixnum) end it 'should import the other FIT file' do - postrunner([ 'import', '.' ]) + postrunner([ 'import', @work_dir ]) list = postrunner(%w( list )) list.index('FILE1.FIT').should be_a(Fixnum) list.index('FILE2.FIT').should be_a(Fixnum) rc = YAML::load_file(File.join(@db_dir, 'config.yml')) - rc[:import_dir].should == '.' + rc[:import_dir].should == @work_dir template = "" @@ -110,18 +109,18 @@ describe PostRunner::Main do it 'should rename FILE2.FIT activity' do postrunner(%w( rename foobar :1 )) list = postrunner(%w( list )) - list.index('FILE2.FIT').should be_nil + list.index(@file2).should be_nil list.index('foobar').should be_a(Fixnum) end it 'should fail when setting bad attribute' do - lambda { postrunner(%w( set foo bar :1)) }.should raise_error SystemExit + lambda { postrunner(%w( set foo bar :1)) }.should raise_error Fit4Ruby::Error end it 'should set name for FILE2.FIT activity' do postrunner(%w( set name foobar :1 )) list = postrunner(%w( list )) - list.index('FILE2.FIT').should be_nil + list.index(@file2).should be_nil list.index('foobar').should be_a(Fixnum) end @@ -133,7 +132,7 @@ describe PostRunner::Main do end it 'should fail when setting bad activity type' do - lambda { postrunner(%w( set type foobar :1)) }.should raise_error SystemExit + lambda { postrunner(%w( set type foobar :1)) }.should raise_error Fit4Ruby::Error end it 'should set activity subtype for FILE2.FIT activity' do @@ -144,7 +143,7 @@ describe PostRunner::Main do end it 'should fail when setting bad activity subtype' do - lambda { postrunner(%w( set subtype foobar :1)) }.should raise_error SystemExit + lambda { postrunner(%w( set subtype foobar :1)) }.should raise_error Fit4Ruby::Error end it 'should dump an activity from the archive' do @@ -152,7 +151,7 @@ describe PostRunner::Main do end it 'should dump a FIT file' do - postrunner(%w( dump FILE1.FIT )) + postrunner([ 'dump', @file1 ]) end it 'should switch to statute units' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e91cffd..dbb1e0e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,20 +10,44 @@ # published by the Free Software Foundation. # +require 'tmpdir' +require 'fileutils' + # Some dependencies may not be installed as Ruby Gems but as local sources. # Add them and the postrunner dir to the LOAD_PATH. %w( postrunner fit4ruby perobs ).each do |lib_dir| $:.unshift(File.join(File.dirname(__FILE__), '..', '..', lib_dir, 'lib')) end +def tmp_dir_name(caller_file) + begin + dir_name = File.join(Dir.tmpdir, + "#{File.basename(caller_file)}.#{rand(2**32)}") + end while File.exists?(dir_name) + + dir_name +end + def create_fit_file(name, date, duration_minutes = 30) - Fit4Ruby.write(name, create_fit_activity(date, duration_minutes)) + Fit4Ruby.write(name, create_fit_activity( + { :t => date, :duration => duration_minutes })) +end + +def create_fit_activity_file(dir, config) + activity = create_fit_activity(config) + end_time = activity.sessions[-1].start_time + + activity.sessions[-1].total_elapsed_time + fit_file_name = File.join(dir, Fit4Ruby::FileNameCoder.encode(end_time)) + Fit4Ruby.write(fit_file_name, activity) + + fit_file_name end -def create_fit_activity(date, duration_minutes) - ts = Time.parse(date) +def create_fit_activity(config) + ts = Time.parse(config[:t]) + serial = config[:serial] || 12345890 a = Fit4Ruby::Activity.new({ :timestamp => ts }) - a.total_timer_time = duration_minutes * 60 + a.total_timer_time = (config[:duration] || 10) * 60 a.new_user_profile({ :timestamp => ts, :age => 33, :height => 1.78, :weight => 73.0, :gender => 'male', :activity_class => 7.0, @@ -32,8 +56,11 @@ def create_fit_activity(date, duration_minutes) a.new_event({ :timestamp => ts, :event => 'timer', :event_type => 'start_time' }) a.new_device_info({ :timestamp => ts, :manufacturer => 'garmin', + :garmin_product => 'fenix3', + :serial_number => serial, :device_index => 0 }) a.new_device_info({ :timestamp => ts, :manufacturer => 'garmin', + :garmin_product => 'sdm4', :device_index => 1, :battery_status => 'ok' }) laps = 0 0.upto((a.total_timer_time / 60) - 1) do |mins| @@ -69,9 +96,12 @@ def create_fit_activity(date, duration_minutes) a.new_event({ :timestamp => ts, :event => 'timer', :event_type => 'stop_all' }) a.new_device_info({ :timestamp => ts, :manufacturer => 'garmin', + :garmin_product => 'fenix3', + :serial_number => serial, :device_index => 0 }) ts += 1 a.new_device_info({ :timestamp => ts, :manufacturer => 'garmin', + :garmin_product => 'sdm4', :device_index => 1, :battery_status => 'low' }) ts += 120 a.new_event({ :timestamp => ts, :event => 'recovery_hr', -- cgit v1.2.3