summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Schlaeger <chris@linux.com>2015-10-26 19:15:47 +0100
committerChris Schlaeger <chris@linux.com>2015-10-26 19:15:47 +0100
commit5590faccc2c96f5038463685407ea4e468c056c8 (patch)
tree3b1a43c9d99300463aeaf13f5f4523a10a5b190a
parent23f180afce6c6089b9c671974611130b999a817f (diff)
downloadpostrunner-5590faccc2c96f5038463685407ea4e468c056c8.zip
Add data sources table to activity view.
-rw-r--r--lib/postrunner/Activity.rb8
-rw-r--r--lib/postrunner/ActivityView.rb12
-rw-r--r--lib/postrunner/DataSources.rb100
-rw-r--r--lib/postrunner/DeviceList.rb57
-rw-r--r--lib/postrunner/Log.rb25
-rw-r--r--lib/postrunner/Main.rb17
-rw-r--r--lib/postrunner/RuntimeConfig.rb1
-rw-r--r--spec/PostRunner_spec.rb45
-rw-r--r--spec/spec_helper.rb38
9 files changed, 248 insertions, 55 deletions
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 <cs@taskjuggler.org>
+#
+# 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 <cs@taskjuggler.org>
+#
+# 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 [ <ref> ]
Show the referenced FIT activity in a web browser. If no reference
is provided show the list of activities in the database.
+sources [ <ref> ]
+ Show the data sources for the various measurements and how they
+ changed during the course of the activity.
+
summary <ref>
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 = "<a href=\"%s.html\"><img src=\"icons/%s.png\" " +
"class=\"active_button\">"
@@ -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',