postrunner 0.0.11 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -2
- data/Rakefile +18 -2
- data/lib/postrunner/ActivitiesDB.rb +1 -22
- data/lib/postrunner/Activity.rb +21 -0
- data/lib/postrunner/ActivityLink.rb +1 -1
- data/lib/postrunner/ActivityListView.rb +10 -9
- data/lib/postrunner/ActivitySummary.rb +42 -15
- data/lib/postrunner/ActivityView.rb +10 -8
- data/lib/postrunner/ChartView.rb +238 -80
- data/lib/postrunner/DataSources.rb +1 -14
- data/lib/postrunner/DeviceList.rb +18 -10
- data/lib/postrunner/DirUtils.rb +33 -0
- data/lib/postrunner/EventList.rb +149 -0
- data/lib/postrunner/FFS_Activity.rb +297 -0
- data/lib/postrunner/FFS_Device.rb +129 -0
- data/lib/postrunner/FitFileStore.rb +372 -0
- data/lib/postrunner/HRV_Analyzer.rb +178 -0
- data/lib/postrunner/LinearPredictor.rb +46 -0
- data/lib/postrunner/Main.rb +135 -33
- data/lib/postrunner/Percentiles.rb +45 -0
- data/lib/postrunner/PersonalRecords.rb +203 -114
- data/lib/postrunner/RecordListPageView.rb +6 -6
- data/lib/postrunner/UserProfileView.rb +4 -0
- data/lib/postrunner/version.rb +1 -1
- data/misc/postrunner/trackview.js +99 -0
- data/postrunner.gemspec +5 -5
- data/spec/ActivitySummary_spec.rb +15 -4
- data/spec/FitFileStore_spec.rb +133 -0
- data/spec/FlexiTable_spec.rb +1 -1
- data/spec/PersonalRecords_spec.rb +206 -0
- data/spec/PostRunner_spec.rb +64 -60
- data/spec/View_spec.rb +1 -1
- data/spec/spec_helper.rb +76 -2
- metadata +42 -28
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FFS_Device.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2015, 2016 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
|
+
require 'perobs'
|
14
|
+
require 'postrunner/FFS_Activity'
|
15
|
+
|
16
|
+
module PostRunner
|
17
|
+
|
18
|
+
# Objects of this class can store the activities and monitoring data of a
|
19
|
+
# specific device. The device gets a random number assigned as a unique but
|
20
|
+
# anonymous ID. It also gets a long ID assigned that is a String of the
|
21
|
+
# manufacturer, the product name and the serial number concatenated by
|
22
|
+
# dashes. All objects are transparently stored in the PEROBS::Store.
|
23
|
+
class FFS_Device < PEROBS::Object
|
24
|
+
|
25
|
+
po_attr :activities, :monitorings, :short_uid, :long_uid
|
26
|
+
|
27
|
+
# Create a new FFS_Device object.
|
28
|
+
# @param p [PEROBS::Handle] p
|
29
|
+
# @param short_uid [Fixnum] A random number used a unique ID
|
30
|
+
# @param long_uid [String] A string consisting of the manufacturer and
|
31
|
+
# product name and the serial number.
|
32
|
+
def initialize(p, short_uid, long_uid)
|
33
|
+
super(p)
|
34
|
+
self.short_uid = short_uid
|
35
|
+
self.long_uid = long_uid
|
36
|
+
restore
|
37
|
+
end
|
38
|
+
|
39
|
+
# Handle initialization of persistent attributes.
|
40
|
+
def restore
|
41
|
+
attr_init(:activities) { @store.new(PEROBS::Array) }
|
42
|
+
attr_init(:monitorings) { @store.new(PEROBS::Array) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a new FIT file for this device.
|
46
|
+
# @param fit_file_name [String] The full path to the FIT file
|
47
|
+
# @param fit_entity [Fit4Ruby::FitEntity] The content of the FIT file
|
48
|
+
# @param overwrite [Boolean] A flag to indicate if an existing file should
|
49
|
+
# be replaced with the new one.
|
50
|
+
# @return [FFS_Activity or FFS_Monitoring] Corresponding entry in the
|
51
|
+
# FitFileStore or nil if file could not be added.
|
52
|
+
def add_fit_file(fit_file_name, fit_entity, overwrite)
|
53
|
+
case fit_entity.class
|
54
|
+
when Fit4Ruby::Activity.class
|
55
|
+
entity = activity_by_file_name(File.basename(fit_file_name))
|
56
|
+
entities = @activities
|
57
|
+
type = 'activity'
|
58
|
+
new_entity_class = FFS_Activity
|
59
|
+
when Fit4Ruby::Monitoring.class
|
60
|
+
entity = monitoring_by_file_name(File.basename(fit_file_name))
|
61
|
+
entities = @monitorings
|
62
|
+
type = 'monitoring'
|
63
|
+
new_entity_class = FFS_Monitoring
|
64
|
+
else
|
65
|
+
Log.fatal "Unsupported FIT entity #{fit_entity.class}"
|
66
|
+
end
|
67
|
+
|
68
|
+
if entity
|
69
|
+
if overwrite
|
70
|
+
# Replace the old file. All meta-information will be lost.
|
71
|
+
entities.delete_if { |e| e.fit_file_name == fit_file_name }
|
72
|
+
entity = @store.new(new_entity_class, myself, fit_file_name,
|
73
|
+
fit_entity)
|
74
|
+
else
|
75
|
+
Log.debug "FIT file #{fit_file_name} has already been imported"
|
76
|
+
# Refuse to replace the file.
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
else
|
80
|
+
# Don't add the entity if has deleted before and overwrite isn't true.
|
81
|
+
path = @store['file_store'].fit_file_dir(File.basename(fit_file_name),
|
82
|
+
long_uid, type)
|
83
|
+
fq_fit_file_name = File.join(path, File.basename(fit_file_name))
|
84
|
+
if File.exists?(fq_fit_file_name) && !overwrite
|
85
|
+
Log.debug "FIT file #{fq_fit_file_name} has already been imported " +
|
86
|
+
"and deleted"
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
# Add the new file to the list.
|
90
|
+
entity = @store.new(new_entity_class, myself, fit_file_name, fit_entity)
|
91
|
+
end
|
92
|
+
entity.store_fit_file(fit_file_name)
|
93
|
+
entities << entity
|
94
|
+
entities.sort!
|
95
|
+
|
96
|
+
# Scan the activity for any potential new personal records and register
|
97
|
+
# them.
|
98
|
+
if entity.is_a?(FFS_Activity)
|
99
|
+
records = @store['records']
|
100
|
+
records.scan_activity_for_records(entity, true)
|
101
|
+
end
|
102
|
+
|
103
|
+
entity
|
104
|
+
end
|
105
|
+
|
106
|
+
# Delete the given activity from the activity list.
|
107
|
+
# @param activity [FFS_Activity] activity to delete
|
108
|
+
def delete_activity(activity)
|
109
|
+
@activities.delete(activity)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return the activity with the given file name.
|
113
|
+
# @param file_name [String] Base name of the fit file.
|
114
|
+
# @return [FFS_Activity] Corresponding FFS_Activity or nil.
|
115
|
+
def activity_by_file_name(file_name)
|
116
|
+
@activities.find { |a| a.fit_file_name == file_name }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return the monitoring with the given file name.
|
120
|
+
# @param file_name [String] Base name of the fit file.
|
121
|
+
# @return [FFS_Activity] Corresponding FFS_Monitoring or nil.
|
122
|
+
def monitoring_by_file_name(file_name)
|
123
|
+
@monitorings.find { |a| a.fit_file_name == file_name }
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
@@ -0,0 +1,372 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
# encoding: UTF-8
|
3
|
+
#
|
4
|
+
# = FitFileStore.rb -- PostRunner - Manage the data from your Garmin sport devices.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2014, 2015, 2016 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
|
+
require 'fit4ruby'
|
14
|
+
require 'perobs'
|
15
|
+
|
16
|
+
require 'postrunner/Log'
|
17
|
+
require 'postrunner/DirUtils'
|
18
|
+
require 'postrunner/FFS_Device'
|
19
|
+
require 'postrunner/ActivityListView'
|
20
|
+
require 'postrunner/ViewButtons'
|
21
|
+
|
22
|
+
module PostRunner
|
23
|
+
|
24
|
+
# The FitFileStore stores all FIT file and provides access to the contained
|
25
|
+
# data.
|
26
|
+
class FitFileStore < PEROBS::Object
|
27
|
+
|
28
|
+
include DirUtils
|
29
|
+
|
30
|
+
po_attr :devices
|
31
|
+
|
32
|
+
attr_reader :store, :views
|
33
|
+
|
34
|
+
# Create a new FIT file store.
|
35
|
+
# @param p [PEROBS::Handle] PEROBS handle
|
36
|
+
def initialize(p)
|
37
|
+
super(p)
|
38
|
+
restore
|
39
|
+
end
|
40
|
+
|
41
|
+
# Setup non-persistent variables.
|
42
|
+
def restore
|
43
|
+
@data_dir = @store['config']['data_dir']
|
44
|
+
# Ensure that we have an Array in the store to hold all known devices.
|
45
|
+
@store['devices'] = @store.new(PEROBS::Hash) unless @store['devices']
|
46
|
+
|
47
|
+
@devices_dir = File.join(@data_dir, 'devices')
|
48
|
+
# It's generally not a good idea to store absolute file names in the
|
49
|
+
# database. We'll make an exception here as this is the only way to
|
50
|
+
# propagate this path to FFS_Activity or FFS_Monitoring objects. The
|
51
|
+
# store entry is updated on each program run, so the DB can be moved
|
52
|
+
# safely to another directory.
|
53
|
+
@store['config']['devices_dir'] = @devices_dir
|
54
|
+
create_directory(@devices_dir, 'devices')
|
55
|
+
|
56
|
+
# Define which View objects the HTML output will consist of. This
|
57
|
+
# doesn't really belong in this class but for now it's the best place
|
58
|
+
# to put it.
|
59
|
+
@views = ViewButtons.new([
|
60
|
+
NavButtonDef.new('activities.png', 'index.html'),
|
61
|
+
NavButtonDef.new('record.png', "records-0.html")
|
62
|
+
])
|
63
|
+
end
|
64
|
+
|
65
|
+
# Version upgrade logic.
|
66
|
+
def handle_version_update
|
67
|
+
# Nothing here so far.
|
68
|
+
end
|
69
|
+
|
70
|
+
# Add a file to the store.
|
71
|
+
# @param fit_file_name [String] Name of the FIT file
|
72
|
+
# @param overwrite [TrueClass, FalseClass] If true, an existing file will
|
73
|
+
# be replaced.
|
74
|
+
# @return [FFS_Activity or FFS_Monitoring] Corresponding entry in the
|
75
|
+
# FitFileStore or nil if file could not be added.
|
76
|
+
def add_fit_file(fit_file_name, fit_entity = nil, overwrite = false)
|
77
|
+
# If we the file hasn't been read yet, read it in as a
|
78
|
+
# Fit4Ruby::Activity or Fit4Ruby::Monitoring entity.
|
79
|
+
unless fit_entity
|
80
|
+
return nil unless (fit_entity = read_fit_file(fit_file_name))
|
81
|
+
end
|
82
|
+
|
83
|
+
unless [ Fit4Ruby::Activity,
|
84
|
+
Fit4Ruby::Monitoring ].include?(fit_entity.class)
|
85
|
+
Log.critical "Unsupported FIT file type #{fit_entity.class}"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Generate a String that uniquely identifies the device that generated
|
89
|
+
# the FIT file.
|
90
|
+
id = extract_fit_file_id(fit_entity)
|
91
|
+
long_uid = "#{id[:manufacturer]}-#{id[:product]}-#{id[:serial_number]}"
|
92
|
+
|
93
|
+
# Make sure the device that created the FIT file is properly registered.
|
94
|
+
device = register_device(long_uid)
|
95
|
+
# Store the FIT entity with the device.
|
96
|
+
entity = device.add_fit_file(fit_file_name, fit_entity, overwrite)
|
97
|
+
|
98
|
+
# The FIT file might be already stored or invalid. In that case we
|
99
|
+
# abort this method.
|
100
|
+
return nil unless entity
|
101
|
+
|
102
|
+
if fit_entity.is_a?(Fit4Ruby::Activity)
|
103
|
+
@store['records'].scan_activity_for_records(entity)
|
104
|
+
|
105
|
+
# Generate HTML file for this activity.
|
106
|
+
entity.generate_html_report
|
107
|
+
|
108
|
+
# The HTML activity views contain links to their predecessors and
|
109
|
+
# successors. After inserting a new activity, we need to re-generate
|
110
|
+
# these views as well.
|
111
|
+
if (pred = predecessor(entity))
|
112
|
+
pred.generate_html_report
|
113
|
+
end
|
114
|
+
if (succ = successor(entity))
|
115
|
+
succ.generate_html_report
|
116
|
+
end
|
117
|
+
# And update the index pages
|
118
|
+
generate_html_index_pages
|
119
|
+
end
|
120
|
+
|
121
|
+
Log.info "#{File.basename(fit_file_name)} " +
|
122
|
+
'has been successfully added to archive'
|
123
|
+
|
124
|
+
entity
|
125
|
+
end
|
126
|
+
|
127
|
+
# Delete an activity from the database. It will only delete the entry in
|
128
|
+
# the database. The original activity file will not be deleted from the
|
129
|
+
# file system.
|
130
|
+
# @param activity [FFS_Activity] Activity to delete
|
131
|
+
def delete_activity(activity)
|
132
|
+
pred = predecessor(activity)
|
133
|
+
succ = successor(activity)
|
134
|
+
|
135
|
+
activity.device.delete_activity(activity)
|
136
|
+
|
137
|
+
# The HTML activity views contain links to their predecessors and
|
138
|
+
# successors. After deleting an activity, we need to re-generate these
|
139
|
+
# views.
|
140
|
+
pred.generate_html_report if pred
|
141
|
+
succ.generate_html_report if succ
|
142
|
+
|
143
|
+
generate_html_index_pages
|
144
|
+
end
|
145
|
+
|
146
|
+
# Rename the specified activity and update all HTML pages that contain the
|
147
|
+
# name.
|
148
|
+
# @param activity [FFS_Activity] Activity to rename
|
149
|
+
# @param name [String] New name
|
150
|
+
def rename_activity(activity, name)
|
151
|
+
activity.set('name', name)
|
152
|
+
generate_html_index_pages
|
153
|
+
end
|
154
|
+
|
155
|
+
# Set the specified attribute of the given activity to a new value.
|
156
|
+
# @param activity [FFS_Activity] Activity to rename
|
157
|
+
# @param attribute [String] name of the attribute to change
|
158
|
+
# @param value [any] new value of the attribute
|
159
|
+
def set_activity_attribute(activity, attribute, value)
|
160
|
+
activity.set(attribute, value)
|
161
|
+
case attribute
|
162
|
+
when 'norecord', 'type'
|
163
|
+
# If we have changed a norecord setting or an activity type, we need
|
164
|
+
# to regenerate all reports and re-collect the record list since we
|
165
|
+
# don't know which Activity needs to replace the changed one.
|
166
|
+
check
|
167
|
+
end
|
168
|
+
generate_html_index_pages
|
169
|
+
end
|
170
|
+
|
171
|
+
# Perform the necessary report updates after the unit system has been
|
172
|
+
# changed.
|
173
|
+
def change_unit_system
|
174
|
+
# If we have changed the unit system we need to re-generate all HTML
|
175
|
+
# reports.
|
176
|
+
activities.each do |activity|
|
177
|
+
activity.generate_html_report
|
178
|
+
end
|
179
|
+
@store['records'].generate_html_reports
|
180
|
+
generate_html_index_pages
|
181
|
+
end
|
182
|
+
# Determine the right directory for the given FIT file. The resulting path
|
183
|
+
# looks something like /home/user/.postrunner/devices/garmin-fenix3-1234/
|
184
|
+
# activity/5A.
|
185
|
+
# @param fit_file_base_name [String] The base name of the fit file
|
186
|
+
# @param long_uid [String] the long UID of the device
|
187
|
+
# @param type [String] 'activity' or 'monitoring'
|
188
|
+
# @return [String] the full path name of the archived FIT file
|
189
|
+
def fit_file_dir(fit_file_base_name, long_uid, type)
|
190
|
+
# The first letter of the FIT file specifies the creation year.
|
191
|
+
# The second letter of the FIT file specifies the creation month.
|
192
|
+
File.join(@store['config']['devices_dir'],
|
193
|
+
long_uid, type, fit_file_base_name[0..1])
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
|
198
|
+
# @return [Array of FFS_Device] List of registered devices.
|
199
|
+
def devices
|
200
|
+
@store['devices']
|
201
|
+
end
|
202
|
+
|
203
|
+
# @return [Array of FFS_Activity] List of stored activities.
|
204
|
+
def activities
|
205
|
+
list = []
|
206
|
+
@store['devices'].each do |id, device|
|
207
|
+
list += device.activities
|
208
|
+
end
|
209
|
+
list.sort
|
210
|
+
end
|
211
|
+
|
212
|
+
# Return the reference index of the given FFS_Activity.
|
213
|
+
# @param activity [FFS_Activity]
|
214
|
+
# @return [Fixnum] Reference index as used in the UI
|
215
|
+
def ref_by_activity(activity)
|
216
|
+
return nil unless (idx = activities.index(activity))
|
217
|
+
|
218
|
+
idx + 1
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return the next Activity after the provided activity. Note that this has
|
222
|
+
# a lower index. If none is found, return nil.
|
223
|
+
def successor(activity)
|
224
|
+
all_activities = activities
|
225
|
+
idx = all_activities.index(activity)
|
226
|
+
return nil if idx.nil? || idx == 0
|
227
|
+
all_activities[idx - 1]
|
228
|
+
end
|
229
|
+
|
230
|
+
# Return the previous Activity before the provided activity.
|
231
|
+
# If none is found, return nil.
|
232
|
+
def predecessor(activity)
|
233
|
+
all_activities = activities
|
234
|
+
idx = all_activities.index(activity)
|
235
|
+
return nil if idx.nil?
|
236
|
+
# Activities indexes are reversed. The predecessor has a higher index.
|
237
|
+
all_activities[idx + 1]
|
238
|
+
end
|
239
|
+
|
240
|
+
# Find a specific subset of the activities based on their index.
|
241
|
+
# @param query [String]
|
242
|
+
def find(query)
|
243
|
+
case query
|
244
|
+
when /\A-?\d+$\z/
|
245
|
+
index = query.to_i
|
246
|
+
# The UI counts the activities from 1 to N. Ruby counts from 0 -
|
247
|
+
# (N-1).
|
248
|
+
if index <= 0
|
249
|
+
Log.error 'Index must be larger than 0'
|
250
|
+
return []
|
251
|
+
end
|
252
|
+
# The UI counts the activities from 1 to N. Ruby counts from 0 -
|
253
|
+
# (N-1).
|
254
|
+
if (a = activities[index - 1])
|
255
|
+
return [ a ]
|
256
|
+
end
|
257
|
+
when /\A-?\d+--?\d+\z/
|
258
|
+
idxs = query.match(/(?<sidx>-?\d+)-(?<eidx>-?[0-9]+)/)
|
259
|
+
if (sidx = idxs['sidx'].to_i) <= 0
|
260
|
+
Log.error 'Start index must be larger than 0'
|
261
|
+
return []
|
262
|
+
end
|
263
|
+
if (eidx = idxs['eidx'].to_i) <= 0
|
264
|
+
Log.error 'End index must be larger than 0'
|
265
|
+
return []
|
266
|
+
end
|
267
|
+
if eidx < sidx
|
268
|
+
Log.error 'Start index must be smaller than end index'
|
269
|
+
return []
|
270
|
+
end
|
271
|
+
# The UI counts the activities from 1 to N. Ruby counts from 0 -
|
272
|
+
# (N-1).
|
273
|
+
unless (as = activities[(sidx - 1)..(eidx - 1)]).empty?
|
274
|
+
return as
|
275
|
+
end
|
276
|
+
else
|
277
|
+
Log.error "Invalid activity query: #{query}"
|
278
|
+
end
|
279
|
+
|
280
|
+
[]
|
281
|
+
end
|
282
|
+
|
283
|
+
# This methods checks all stored FIT files for correctness, updates all
|
284
|
+
# indexes and re-generates all HTML reports.
|
285
|
+
def check
|
286
|
+
records = @store['records']
|
287
|
+
records.delete_all_records
|
288
|
+
activities.sort do |a1, a2|
|
289
|
+
a1.timestamp <=> a2.timestamp
|
290
|
+
end.each do |a|
|
291
|
+
a.check
|
292
|
+
records.scan_activity_for_records(a)
|
293
|
+
end
|
294
|
+
records.generate_html_reports
|
295
|
+
generate_html_index_pages
|
296
|
+
end
|
297
|
+
|
298
|
+
# Show the activity list in a web browser.
|
299
|
+
def show_list_in_browser
|
300
|
+
generate_html_index_pages
|
301
|
+
@store['records'].generate_html_reports
|
302
|
+
show_in_browser(File.join(@store['config']['html_dir'], 'index.html'))
|
303
|
+
end
|
304
|
+
|
305
|
+
def list_activities
|
306
|
+
puts ActivityListView.new(self).to_s
|
307
|
+
end
|
308
|
+
|
309
|
+
# Launch a web browser and show an HTML file.
|
310
|
+
# @param html_file [String] file name of the HTML file to show
|
311
|
+
def show_in_browser(html_file)
|
312
|
+
cmd = "#{ENV['BROWSER'] || 'firefox'} \"#{html_file}\" &"
|
313
|
+
|
314
|
+
unless system(cmd)
|
315
|
+
Log.fatal "Failed to execute the following shell command: #{$cmd}\n" +
|
316
|
+
"#{$!}"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
private
|
321
|
+
|
322
|
+
def read_fit_file(fit_file_name)
|
323
|
+
begin
|
324
|
+
return Fit4Ruby.read(fit_file_name)
|
325
|
+
rescue Fit4Ruby::Error
|
326
|
+
Log.error $!
|
327
|
+
return nil
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def extract_fit_file_id(fit_entity)
|
332
|
+
fit_entity.device_infos.each do |di|
|
333
|
+
if di.device_index == 0
|
334
|
+
return {
|
335
|
+
:manufacturer => di.manufacturer,
|
336
|
+
:product => di.garmin_product || di.product,
|
337
|
+
:serial_number => di.serial_number
|
338
|
+
}
|
339
|
+
end
|
340
|
+
end
|
341
|
+
Log.fatal "Fit entity has no device info for 0"
|
342
|
+
end
|
343
|
+
|
344
|
+
def register_device(long_uid)
|
345
|
+
unless @store['devices'].include?(long_uid)
|
346
|
+
Log.info "New device registered: #{long_uid}"
|
347
|
+
|
348
|
+
# Generate a unique ID for the device that does not allow any insight
|
349
|
+
# on the number of and type of managed devices.
|
350
|
+
begin
|
351
|
+
short_uid = rand(2**32)
|
352
|
+
end while @store['devices'].find { |luid, d| d.short_uid == short_uid }
|
353
|
+
|
354
|
+
@store['devices'][long_uid] =
|
355
|
+
@store.new(FFS_Device, short_uid, long_uid)
|
356
|
+
|
357
|
+
# Create the directory to store the FIT files of this device.
|
358
|
+
create_directory(File.join(@devices_dir, long_uid), long_uid)
|
359
|
+
end
|
360
|
+
|
361
|
+
@store['devices'][long_uid]
|
362
|
+
end
|
363
|
+
|
364
|
+
def generate_html_index_pages
|
365
|
+
# Ensure that HTML index is up-to-date.
|
366
|
+
ActivityListView.new(myself).update_index_pages
|
367
|
+
end
|
368
|
+
|
369
|
+
end
|
370
|
+
|
371
|
+
end
|
372
|
+
|