-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathpolar_training2sml
executable file
·233 lines (219 loc) · 8.92 KB
/
polar_training2sml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#!/usr/bin/env ruby
# Converts RAW Polar training session data files in Suunto SML file format.
require 'nokogiri'
require "#{File.dirname(__FILE__)}/lib/polar_data_parser"
def usage
puts "Usage:"
puts " #{__FILE__} <directory> [<sml file>]"
end
dir = ARGV[0]
unless dir
usage
exit -2
end
output_file = ARGV[1] || File.join(dir, 'output.sml')
def output_sml(parsed)
sport = parsed[:sport]
training_session = parsed[:training_session]
sensors = parsed[:sensors]
samples = parsed[:samples]
exercise = parsed[:exercise]
laps = parsed[:exercise_laps]
exercise_stats = parsed[:exercise_stats]
route_samples = parsed[:route_samples]
start = DateTime.new(training_session.start.date.year, training_session.start.date.month, training_session.start.date.day, training_session.start.time.hour, training_session.start.time.minute, training_session.start.time.seconds, "%+i" % (training_session.start.time_zone_offset / 60)).to_time.utc
recording_interval = samples.recording_interval.hours * 3600 + samples.recording_interval.minutes * 60 + samples.recording_interval.seconds + (samples.recording_interval.millis.to_f / 1000)
samples_count = samples.speed_samples.count
laps_count = laps ? laps.laps.count : 0
route_samples_count = route_samples.latitude.count
first_gps_fix = route_samples_count > 0 ? DateTime.new(route_samples.first_location_time.date.year, route_samples.first_location_time.date.month, route_samples.first_location_time.date.day, route_samples.first_location_time.time.hour, route_samples.first_location_time.time.minute, route_samples.first_location_time.time.seconds, '+0').to_time.utc : nil
time_to_first_gps_fix = first_gps_fix.to_i - start.to_i
altitude_delta = 0
altitude_calibration_samples = samples.altitude_calibration.each do |s|
if s.operation == :SUM
altitude_delta = s.value
else
STDERR.puts "Warning: Altitude calibration data of unsupported operation type ignored"
end
end if samples.altitude_calibration
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.sml('SdkVersion' => "2.4.89", 'Modified' => "2012-09-22T10:39:51", 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance", 'xmlns:xsd' => "http://www.w3.org/2001/XMLSchema", 'xmlns' => "http://www.suunto.com/schemas/sml") {
xml.DeviceLog {
xml.Header {
xml.Duration training_session.duration.hours.to_f * 3600 + training_session.duration.minutes * 60 + training_session.duration.seconds + training_session.duration.millis / 1000
xml.Ascent exercise.ascent.round
xml.Descent exercise.descent.round
#FIXME AscentTime
#FIXME DescentTime
#FIXME RecoveryTime
xml.Speed {
xml.Avg min_per_km_2_m_per_s(exercise_stats.speed.average)
max = 0
max_at = 0
samples.speed_samples.each_with_index do |s, i|
if s > max
max = s
max_at = i
end
end
xml.Max min_per_km_2_m_per_s(max)
xml.MaxTime max_at * recording_interval
}
xml.Cadence {
xml.Avg exercise_stats.cadence.average.to_f / 60
max = 0
max_at = 0
samples.cadence_samples.each_with_index do |s, i|
if s > max
max = s
max_at = i
end
end
xml.Max max.to_f / 60
xml.MaxTime max_at * recording_interval
}
xml.Altitude {
xml.Avg exercise_stats.altitude.average.to_f / 60
min = 99999999
min_at = 0
max = -99999999
max_at = 0
samples.altitude_samples.each_with_index do |s, i|
if s < min
min = s
min_at = i
end
if s > max
max = s
max_at = i
end
end
xml.Max max
xml.Min min
xml.MaxTime max_at * recording_interval
xml.MinTime min_at * recording_interval
}
xml.HR {
xml.Avg exercise_stats.heart_rate.average / 60
min = 999
min_at = 0
max = 0
max_at = 0
samples.heart_rate_samples.each_with_index do |s, i|
if s < min
min = s
min_at = i
end
if s > max
max = s
max_at = i
end
end
xml.Max max.to_f / 60
xml.Min min.to_f / 60
xml.MaxTime max_at * recording_interval
xml.MinTime min_at * recording_interval
}
#FIXME PeakTrainingEffect
xml.ActivityType case sport.identifier.value
when 1 # Running
3
when 6 # XC-skiing
22
when 9 # Nordic Walking
84
when 11 # Hiking
96
when 15 # Strength tr.
23
when 27 # Trail running
82
when 59 # Roller ski skating -> roller ski
88
when 66 # Stretching
79
when 95 # Kayaking
14
when 126 # Core -> Gymastics
67
when 127 # Mobility / static -> stretching
79
else
0 # FIXME: convert Polar->Suunto IDs
end
xml.Activity training_session.session_name.text
xml.Distance training_session.distance
xml.LogItemCount samples_count + laps_count + route_samples_count
xml.Energy kcal2joules(training_session.calories)
xml.TimeToFirstFix time_to_first_gps_fix if first_gps_fix
xml.BatteryChargeAtStart 0
xml.BatteryCharge 0
xml.DistanceBeforeCalibrationChange 0
xml.DateTime start.strftime("%Y-%m-%dT%H:%M:%S")
}
xml.Device {
xml.Name training_session.model_name
xml.SerialNumber training_session.device_id
}
xml.Samples {
datetime = start
timestamp = 0.0
for i in 0..samples_count-1
datetime += recording_interval
timestamp += recording_interval
xml.Sample {
#FIXME VerticalSpeed
xml.Cadence samples.cadence_samples[i].to_f / 60
xml.HR samples.heart_rate_samples[i].to_f / 60
#FIXME EnergyConsumption
xml.Altitude samples.altitude_samples[i] + altitude_delta if samples.altitude_samples[i]
xml.Speed min_per_km_2_m_per_s(samples.speed_samples[i])
xml.Time timestamp
xml.SampleType "periodic"
xml.UTC datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
}
end
if laps_count > 0
for i in 0..laps_count-1
lap = laps.laps[i]
xml.Sample {
lap_split_time = (lap.header.split_time.hours * 3600 + lap.header.split_time.minutes * 60 + lap.header.split_time.seconds).to_f + lap.header.split_time.millis.to_f / 1000
xml.UTC (start + lap_split_time).strftime("%Y-%m-%dT%H:%M:%SZ")
xml.Time lap_split_time
xml.Events {
xml.Lap {
xml.Type 'Manual'
xml.Duration (lap.header.duration.hours * 3600 + lap.header.duration.minutes * 60 + lap.header.duration.seconds).to_f + lap.header.duration.millis.to_f / 1000
xml.Distance lap.header.distance
}
}
}
end
end
for i in 0..route_samples_count-1
xml.Sample {
xml.GPSAltitude route_samples.gps_altitude[i]
xml.NumberOfSatellites route_samples.satellite_amount[i]
xml.Latitude degree2radian(route_samples.latitude[i])
xml.Longitude degree2radian(route_samples.longitude[i])
xml.Time (time_to_first_gps_fix + route_samples.duration[i].to_f / 1000)
xml.UTC (first_gps_fix + (route_samples.duration[i].to_f / 1000)).strftime("%Y-%m-%dT%H:%M:%SZ")
xml.SampleType "gps-base"
}
end
}
}
}
end
builder.to_xml
end
puts "Converting Polar training session in '#{dir}' to Suunto SML format as '#{output_file}'..."
parsed = PolarDataParser.parse_training_session(dir)
if parsed.key?(:training_session)
File.open(output_file, 'w') do |f|
f << output_sml(parsed)
end
puts "Done"
else
puts "Error: couldn't find training session"
end