postrunner 0.0.11 → 0.1.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 +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
|
+
|