1#![allow(dead_code)]
22
23pub mod fit_file;
24
25#[cfg(test)]
26mod activity_tests {
27 use std::collections::HashMap;
28 extern crate csv;
29
30 fn callback(timestamp: u32, global_message_num: u16, local_msg_type: u8, _message_index: u16, fields: Vec<crate::fit_file::FitFieldValue>, data: &mut Context) {
32 if global_message_num == crate::fit_file::GLOBAL_MSG_NUM_SESSION {
33 let msg = crate::fit_file::FitSessionMsg::new(fields);
34 let sport_names = crate::fit_file::init_sport_name_map();
35 let sport_id = msg.sport.unwrap();
36
37 println!("[Sport Message] {}", sport_names.get(&sport_id).unwrap());
38 }
39 else if global_message_num == crate::fit_file::GLOBAL_MSG_NUM_RECORD {
40 let msg = crate::fit_file::FitRecordMsg::new(fields);
41 let mut latitude = 0.0;
42 let mut longitude = 0.0;
43 let mut altitude = 0.0;
44 let mut power = 0;
45 let mut valid_location = true;
46
47 match msg.position_lat {
48 Some(res) => {
49
50 if res != 0x7FFFFFFF {
52 latitude = crate::fit_file::semicircles_to_degrees(res);
53 }
54 else {
55 valid_location = false;
56 }
57 }
58 None => {
59 valid_location = false;
60 }
61 }
62 match msg.position_long {
63 Some(res) => {
64
65 if res != 0x7FFFFFFF {
67 longitude = crate::fit_file::semicircles_to_degrees(res);
68 }
69 else {
70 valid_location = false;
71 }
72 }
73 None => {
74 valid_location = false;
75 }
76 }
77 match msg.altitude {
78 Some(res) => {
79
80 if res != 0xFFFF {
82 altitude = (res as f64 / 5.0) - 500.0;
83 }
84 }
85 None => {
86 }
87 }
88 match msg.power {
89 Some(res) => {
90 if res != 0xFFFF {
91 power = res;
92 }
93 }
94 None => {
95 }
96 }
97
98 data.num_records_processed = data.num_records_processed + 1;
100 data.accumulated_power = data.accumulated_power + power as u64;
101
102 if valid_location {
103 println!("[Record Message] Timestamp: {} Latitude: {} Longitude: {} Altitude: {}", timestamp, latitude, longitude, altitude);
104 }
105 else {
106 println!("[Record Message] Invalid location data");
107 }
108 }
109 else if global_message_num == crate::fit_file::GLOBAL_MSG_NUM_LENGTH {
110 data.num_length_msgs_processed = data.num_length_msgs_processed + 1;
112 }
113 else {
114 let global_message_names = crate::fit_file::init_global_msg_name_map();
115 let mut field_num = 1;
116
117 match global_message_names.get(&global_message_num) {
118 Some(name) => println!("[{} Message] Timestamp {}, Values: ", name, timestamp),
119 None => println!("[Global Message Num {} Local Message Type {}] Timestamp {}, Values: ", global_message_num, local_msg_type, timestamp)
120 }
121
122 for field in fields {
123 print!(" ({}) Base Type: {}, Value: ", field_num, field.base_type);
124
125 match field.type_enum {
126 crate::fit_file::FieldType::FieldTypeNotSet => { print!("[not set] "); },
127 crate::fit_file::FieldType::FieldTypeUInt => { print!("{} ", field.value_uint); },
128 crate::fit_file::FieldType::FieldTypeSInt => { print!("{} ", field.value_sint); },
129 crate::fit_file::FieldType::FieldTypeFloat => { print!("{} ", field.value_float); },
130 crate::fit_file::FieldType::FieldTypeByteArray => {
131 for byte in field.value_byte_array.iter() {
132 print!("{:#04x} ", byte);
133 }
134 },
135 crate::fit_file::FieldType::FieldTypeStr => { print!("\"{}\" ", field.value_string); },
136 }
137
138 field_num = field_num + 1;
139 println!("");
140 }
141 println!("");
142 }
143 }
144
145 struct Context {
147 num_records_processed: u16,
148 num_length_msgs_processed: u16,
149 accumulated_power: u64
150 }
151
152 impl Context {
153 pub fn new() -> Self {
154 let context = Context{ num_records_processed: 0, num_length_msgs_processed: 0, accumulated_power: 0 };
155 context
156 }
157 }
158
159 #[test]
160 fn file1_zwift() {
161 let file = std::fs::File::open("tests/20210218_zwift.fit").unwrap();
162 let mut reader = std::io::BufReader::new(file);
163 let mut context = Context::new();
164 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
165
166 match fit {
167 Ok(fit) => {
168 print!("FIT File Header: ");
169 fit.header.print();
170 println!("");
171 println!("Num records processed: {}", context.num_records_processed);
172 assert!(context.num_records_processed == 1163);
173 }
174 _ => { println!("Error"); },
175 }
176 }
177
178 #[test]
179 fn file2_bike() {
180 let file = std::fs::File::open("tests/20191117_bike_wahoo_elemnt.fit").unwrap();
181 let mut reader = std::io::BufReader::new(file);
182 let mut context = Context::new();
183 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
184
185 match fit {
186 Ok(fit) => {
187 print!("FIT File Header: ");
188 fit.header.print();
189 println!("");
190 println!("Num records processed: {}", context.num_records_processed);
191 assert!(context.num_records_processed == 4876);
192 }
193 _ => { println!("Error"); },
194 }
195 }
196
197 #[test]
198 fn file3_swim() {
199 let file = std::fs::File::open("tests/20200529_short_ocean_swim.fit").unwrap();
200 let mut reader = std::io::BufReader::new(file);
201 let mut context = Context::new();
202 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
203
204 match fit {
205 Ok(fit) => {
206 print!("FIT File Header: ");
207 fit.header.print();
208 println!("");
209 println!("Num records processed: {}", context.num_records_processed);
210 assert!(context.num_records_processed == 179);
211 }
212 _ => (),
213 }
214 }
215
216 #[test]
217 fn file4_run_with_power() {
218 let file = std::fs::File::open("tests/20210507_run_coros_pace_2.fit").unwrap();
219 let mut reader = std::io::BufReader::new(file);
220 let mut context = Context::new();
221 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
222
223 match fit {
224 Ok(fit) => {
225 print!("FIT File Header: ");
226 fit.header.print();
227 println!("");
228 println!("Num records processed: {}", context.num_records_processed);
229 println!("Accumulated power: {}", context.accumulated_power);
230 assert!(context.num_records_processed == 2364);
231 assert!(context.accumulated_power == 634203);
232 }
233 _ => (),
234 }
235 }
236
237 #[test]
238 fn file5_track_run() {
239 let file = std::fs::File::open("tests/20210610_track_garmin_fenix_6.fit").unwrap();
240 let mut reader = std::io::BufReader::new(file);
241 let mut context = Context::new();
242 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
243
244 match fit {
245 Ok(fit) => {
246 print!("FIT File Header: ");
247 fit.header.print();
248 println!("");
249 println!("Num records processed: {}", context.num_records_processed);
250 assert!(context.num_records_processed == 1672);
251 }
252 _ => (),
253 }
254 }
255
256 #[test]
257 fn file5_pool_swim() {
258 let file = std::fs::File::open("tests/20210709_pool_swim.fit").unwrap();
259 let mut reader = std::io::BufReader::new(file);
260 let mut context = Context::new();
261 let fit = crate::fit_file::read(&mut reader, callback, &mut context);
262
263 match fit {
264 Ok(fit) => {
265 print!("FIT File Header: ");
266 fit.header.print();
267 println!("");
268 println!("Num records processed: {}", context.num_length_msgs_processed);
269 assert!(context.num_length_msgs_processed == 55);
270 }
271 _ => (),
272 }
273 }
274
275 fn convert_to_camel_case(name: &String) -> String {
276 let mut new_name = String::new();
277 let mut need_upper_case = true;
278
279 for c in name.chars() {
280 if need_upper_case {
281 new_name.push(c.to_ascii_uppercase());
282 need_upper_case = false;
283 }
284 else if c == '_' {
285 need_upper_case = true;
286 }
287 else {
288 new_name.push(c);
289 }
290 }
291 new_name
292 }
293
294 fn print_message_struct(name: String, field_map: &HashMap::<String, (u8, String)>) {
295 let mut struct_name: String = "Fit".to_string();
296 struct_name.push_str(&convert_to_camel_case(&name));
297 struct_name.push_str("Msg");
298
299 println!("pub struct {} {{", struct_name);
300 for (field_name, (_field_id, field_type)) in field_map {
301 println!(" pub {}: Option<{}>,", field_name, *field_type);
302 }
303 println!("}}");
304 println!("");
305 println!("impl {} {{", struct_name);
306 println!("");
307 println!(" /// Constructor: Takes the fields that were read by the file parser and puts them into a structure.");
308 println!(" pub fn new(fields: Vec<FitFieldValue>) -> Self {{");
309 print!(" let mut msg = {} {{ ", struct_name);
310 let mut split_count = 0;
311 for (field_name, _field_details) in field_map {
312 print!("{}: None, ", field_name);
313 if split_count % 3 == 0 {
314 println!("");
315 print!(" ");
316 }
317 split_count = split_count + 1;
318 }
319 println!("");
320 println!(" }};");
321 println!("");
322 println!(" for field in fields {{");
323 println!(" if !field.is_dev_field {{");
324 println!(" match field.field_def {{");
325 for (field_name, (field_id, field_type)) in field_map.iter() {
326 println!(" {} => {{ msg.{} = Some(field.get_{}()); }},", field_id, field_name, *field_type);
327 }
328 println!("");
329 println!(" }}");
330 println!(" }}");
331 println!(" }}");
332 println!(" msg");
333 println!(" }}");
334 println!("}}");
335 println!("");
336 }
337
338 #[test]
339 fn create_message_structs() {
340 let file_path = "tests/Messages-Table.csv";
341 let file = match std::fs::File::open(&file_path) {
342 Err(why) => panic!("Couldn't open {} {}", file_path, why),
343 Ok(file) => file,
344 };
345
346 let mut reader = csv::Reader::from_reader(file);
347 let mut current_msg_name = String::new();
348 let mut field_map = HashMap::<String, (u8, String)>::new();
349
350 for record in reader.records() {
351 let record = record.unwrap();
352
353 let msg_name: String = record[0].parse().unwrap();
355 if msg_name.len() > 0 {
356
357 if current_msg_name.len() > 0 {
359 print_message_struct(current_msg_name, &field_map);
360 }
361
362 current_msg_name = String::from(msg_name);
363 field_map.clear();
364 }
365 else {
366 let field_id = &record[1];
367
368 if field_id.len() > 0 {
369 let field_id_num: u8 = field_id.parse::<u8>().unwrap();
370 let field_name: String = record[2].parse().unwrap();
371 let mut field_type_str: String = record[3].parse().unwrap();
372
373 if field_type_str == "byte" {
375 field_type_str = "u8".to_string();
376 }
377 else if field_type_str == "uint8" {
378 field_type_str = "u8".to_string();
379 }
380 else if field_type_str == "uint8z" {
381 field_type_str = "u8".to_string();
382 }
383 else if field_type_str == "uint16" {
384 field_type_str = "u16".to_string();
385 }
386 else if field_type_str == "uint16z" {
387 field_type_str = "u16".to_string();
388 }
389 else if field_type_str == "uint32" {
390 field_type_str = "u32".to_string();
391 }
392 else if field_type_str == "uint32z" {
393 field_type_str = "u32".to_string();
394 }
395 else if field_type_str == "sint8" {
396 field_type_str = "i8".to_string();
397 }
398 else if field_type_str == "sint16" {
399 field_type_str = "i16".to_string();
400 }
401 else if field_type_str == "sint32" {
402 field_type_str = "i32".to_string();
403 }
404 else if field_type_str == "float32" {
405 field_type_str = "f32".to_string();
406 }
407 else if field_type_str == "float64" {
408 field_type_str = "f64".to_string();
409 }
410
411 field_map.insert(field_name, (field_id_num, field_type_str));
412 }
413 }
414 }
415 }
416}
417
418#[cfg(test)]
419mod workout_tests {
420 use std::{fs::File, io::BufReader};
421
422 use crate::fit_file::{self, FitWorkoutStepMsg};
423
424 #[derive(Debug, PartialEq)]
425 struct Workout {
426 workout_message: Option<fit_file::FitWorkoutMsg>,
427 steps: Vec<fit_file::FitWorkoutStepMsg>,
428 }
429
430 impl Workout {
431 fn new() -> Workout {
432 Workout {
433 workout_message: None,
434 steps: Vec::new(),
435 }
436 }
437 }
438
439 fn callback(_timestamp: u32, global_message_num: u16, _local_msg_type: u8, message_index: u16, fields: Vec<crate::fit_file::FitFieldValue>, data: &mut Workout) {
440 if global_message_num == fit_file::GLOBAL_MSG_NUM_WORKOUT_STEP {
441 let step = fit_file::FitWorkoutStepMsg::new(message_index, fields);
442 data.steps.push(step);
443 } else if global_message_num == fit_file::GLOBAL_MSG_NUM_WORKOUT {
444 let workout = fit_file::FitWorkoutMsg::new(fields);
445 data.workout_message = Some(workout);
446 }
447 }
448
449 #[test]
450 fn it_parses_workout_with_repeated_steps() {
451 let mut wko = Workout::new();
452
453 let file = File::open("tests/WorkoutRepeatSteps.fit").unwrap();
454 let mut reader = BufReader::new(file);
455 fit_file::read(&mut reader, callback, &mut wko).unwrap();
456
457 let expected = Workout{
458 workout_message: Some(fit_file::FitWorkoutMsg {
459 message_index: None,
460 sport: None,
461 capabilities: None,
462 num_valid_steps: Some(4),
463 workout_name: Some("Example 2".into()),
464 sub_sport: None,
465 pool_length: None,
466 pool_length_unit: None,
467 }),
468 steps: vec![
469 FitWorkoutStepMsg {
470 message_index: 0,
471 step_name: Some("_A_".into()),
472 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_TIME),
473 duration_value: Some(60000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_HEART_RATE),
475 target_value: Some(2), custom_target_low: None,
477 custom_target_high: None,
478 intensity: Some(fit_file::INTENSITY_WARM_UP),
479 notes: None,
480 equipment: None,
481 secondary_target_type: None,
482 secondary_target_value: None,
483 secondary_custom_target_low: None,
484 secondary_custom_target_high: None,
485 },
486 FitWorkoutStepMsg {
487 message_index: 1,
488 step_name: Some("B1_".into()),
489 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_DISTANCE),
490 duration_value: Some(50000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
492 target_value: Some(5), custom_target_low: None,
494 custom_target_high: None,
495 intensity: Some(fit_file::INTENSITY_ACTIVE),
496 notes: None,
497 equipment: None,
498 secondary_target_type: None,
499 secondary_target_value: None,
500 secondary_custom_target_low: None,
501 secondary_custom_target_high: None,
502 },
503 FitWorkoutStepMsg {
504 message_index: 2,
505 step_name: Some("B2_".into()),
506 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_DISTANCE),
507 duration_value: Some(50000),
508 target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
509 target_value: Some(3),
510 custom_target_low: None,
511 custom_target_high: None,
512 intensity: Some(fit_file::INTENSITY_ACTIVE),
513 notes: None,
514 equipment: None,
515 secondary_target_type: None,
516 secondary_target_value: None,
517 secondary_custom_target_low: None,
518 secondary_custom_target_high: None,
519 },
520 FitWorkoutStepMsg {
521 message_index: 3,
522 step_name: Some("Rep".into()),
523 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_REPEAT_UNTIL_STEPS_COMPLETE),
524 duration_value: Some(1), target_type: Some(fit_file::WORKOUT_STEP_TARGET_OPEN),
526 target_value: Some(3), custom_target_low: None,
528 custom_target_high: None,
529 intensity: Some(fit_file::INTENSITY_ACTIVE),
530 notes: None,
531 equipment: None,
532 secondary_target_type: None,
533 secondary_target_value: None,
534 secondary_custom_target_low: None,
535 secondary_custom_target_high: None,
536 },
537 FitWorkoutStepMsg {
538 message_index: 4,
539 step_name: Some("_C_".into()),
540 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_HEART_RATE_LESS_THAN),
541 duration_value: Some(225), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
543 target_value: Some(1),
544 custom_target_low: None,
545 custom_target_high: None,
546 intensity: Some(fit_file::INTENSITY_COOL_DOWN),
547 notes: None,
548 equipment: None,
549 secondary_target_type: None,
550 secondary_target_value: None,
551 secondary_custom_target_low: None,
552 secondary_custom_target_high: None,
553 },
554 ],
555 };
556
557 assert_eq!(wko.workout_message, expected.workout_message);
558 assert_eq!(wko.steps.len(), expected.steps.len());
559 for (i, expected_step) in expected.steps.iter().enumerate() {
560 let wko_step = wko.steps.get(i).unwrap();
561 assert_eq!(wko_step, expected_step);
562 }
563 }
564
565 #[test]
566 fn it_parses_workout_with_custom_targets() {
567 let mut wko = Workout::new();
568
569 let file = File::open("tests/WorkoutCustomTargetValues.fit").unwrap();
570 let mut reader = BufReader::new(file);
571 fit_file::read(&mut reader, callback, &mut wko).unwrap();
572
573 let expected = Workout{
574 workout_message: Some(fit_file::FitWorkoutMsg {
575 message_index: None,
576 sport: None,
577 capabilities: None,
578 num_valid_steps: Some(4),
579 workout_name: Some("Example 1".into()),
580 sub_sport: None,
581 pool_length: None,
582 pool_length_unit: None,
583 }),
584 steps: vec![
585 FitWorkoutStepMsg {
586 message_index: 0,
587 step_name: Some("_A_".into()),
588 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_TIME),
589 duration_value: Some(60000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_HEART_RATE),
591 target_value: Some(0),
592 custom_target_low: Some(50), custom_target_high: Some(60), intensity: Some(fit_file::INTENSITY_WARM_UP),
595 notes: None,
596 equipment: None,
597 secondary_target_type: None,
598 secondary_target_value: None,
599 secondary_custom_target_low: None,
600 secondary_custom_target_high: None,
601 },
602 FitWorkoutStepMsg {
603 message_index: 1,
604 step_name: Some("B1_".into()),
605 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_DISTANCE),
606 duration_value: Some(50000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
608 target_value: Some(0), custom_target_low: Some(1300), custom_target_high: Some(1310), intensity: Some(fit_file::INTENSITY_ACTIVE),
612 notes: None,
613 equipment: None,
614 secondary_target_type: None,
615 secondary_target_value: None,
616 secondary_custom_target_low: None,
617 secondary_custom_target_high: None,
618 },
619 FitWorkoutStepMsg {
620 message_index: 2,
621 step_name: Some("B2_".into()),
622 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_DISTANCE),
623 duration_value: Some(50000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
625 target_value: Some(0),
626 custom_target_low: Some(1260), custom_target_high: Some(1270), intensity: Some(fit_file::INTENSITY_ACTIVE),
629 notes: None,
630 equipment: None,
631 secondary_target_type: None,
632 secondary_target_value: None,
633 secondary_custom_target_low: None,
634 secondary_custom_target_high: None,
635 },
636 FitWorkoutStepMsg {
637 message_index: 3,
638 step_name: Some("_C_".into()),
639 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_HEART_RATE_LESS_THAN),
640 duration_value: Some(225), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
642 target_value: Some(0),
643 custom_target_low: Some(1220), custom_target_high: Some(1230), intensity: Some(fit_file::INTENSITY_COOL_DOWN),
646 notes: None,
647 equipment: None,
648 secondary_target_type: None,
649 secondary_target_value: None,
650 secondary_custom_target_low: None,
651 secondary_custom_target_high: None,
652 },
653 ],
654 };
655
656 assert_eq!(wko.workout_message, expected.workout_message);
657 assert_eq!(wko.steps.len(), expected.steps.len());
658 for (i, expected_step) in expected.steps.iter().enumerate() {
659 let wko_step = wko.steps.get(i).unwrap();
660 assert_eq!(wko_step, expected_step);
661 }
662 }
663
664 #[test]
665 fn it_parses_trainingpeaks_workout_with_secondary_targets() {
666 let mut wko = Workout::new();
671 let file = File::open("tests/trainingpeaks_export.fit").unwrap();
672 let mut reader = BufReader::new(file);
673 fit_file::read(&mut reader, callback, &mut wko).unwrap();
674
675 let expected = Workout{
676 workout_message: Some(fit_file::FitWorkoutMsg {
677 message_index: None,
678 sport: Some(fit_file::FIT_SPORT_CYCLING),
679 capabilities: None,
680 num_valid_steps: Some(6),
681 workout_name: Some("Test #1".into()),
682 sub_sport: None,
683 pool_length: None,
684 pool_length_unit: None,
685 }),
686 steps: vec![
687 FitWorkoutStepMsg {
688 message_index: 0,
689 step_name: Some("Warm up".into()),
690 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_OPEN),
691 duration_value: Some(u32::MAX),
692 target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
693 target_value: Some(0),
694 custom_target_low: Some(1100), custom_target_high: Some(1125), intensity: Some(fit_file::INTENSITY_WARM_UP),
697 notes: None,
698 equipment: None,
699 secondary_target_type: Some(u8::MAX),
700 secondary_target_value: Some(u32::MAX),
701 secondary_custom_target_low: Some(u32::MAX),
702 secondary_custom_target_high: Some(u32::MAX),
703 },
704 FitWorkoutStepMsg {
705 message_index: 1,
706 step_name: Some("Hard".into()),
707 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_TIME),
708 duration_value: Some(360000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
710 target_value: Some(0), custom_target_low: Some(1212), custom_target_high: Some(1238), intensity: Some(fit_file::INTENSITY_ACTIVE),
714 notes: None,
715 equipment: None,
716 secondary_target_type: Some(fit_file::WORKOUT_STEP_TARGET_CADENCE),
717 secondary_target_value: Some(0),
718 secondary_custom_target_low: Some(95),
719 secondary_custom_target_high: Some(105),
720 },
721 FitWorkoutStepMsg {
722 message_index: 2,
723 step_name: Some("Easy".into()),
724 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_TIME),
725 duration_value: Some(180000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
727 target_value: Some(0),
728 custom_target_low: Some(1125), custom_target_high: Some(1150), intensity: Some(fit_file::INTENSITY_REST),
731 notes: None,
732 equipment: None,
733 secondary_target_type: Some(u8::MAX),
734 secondary_target_value: Some(u32::MAX),
735 secondary_custom_target_low: Some(u32::MAX),
736 secondary_custom_target_high: Some(u32::MAX),
737 },
738 FitWorkoutStepMsg {
739 message_index: 3,
740 step_name: Some("".into()),
741 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_REPEAT_UNTIL_STEPS_COMPLETE),
742 duration_value: Some(1), target_type: Some(u8::MAX),
744 target_value: Some(4), custom_target_low: Some(u32::MAX),
746 custom_target_high: Some(u32::MAX),
747 intensity: Some(u8::MAX),
748 notes: None,
749 equipment: None,
750 secondary_target_type: Some(u8::MAX),
751 secondary_target_value: Some(u32::MAX),
752 secondary_custom_target_low: Some(u32::MAX),
753 secondary_custom_target_high: Some(u32::MAX),
754 },
755 FitWorkoutStepMsg {
756 message_index: 4,
757 step_name: Some("Cool Down".into()),
758 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_TIME),
759 duration_value: Some(600000), target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
761 target_value: Some(0), custom_target_low: Some(1100), custom_target_high: Some(1125), intensity: Some(fit_file::INTENSITY_COOL_DOWN),
765 notes: None,
766 equipment: None,
767 secondary_target_type: Some(u8::MAX),
768 secondary_target_value: Some(u32::MAX),
769 secondary_custom_target_low: Some(u32::MAX),
770 secondary_custom_target_high: Some(u32::MAX),
771 },
772 FitWorkoutStepMsg {
773 message_index: 5,
774 step_name: Some("".into()),
775 duration_type: Some(fit_file::WORKOUT_STEP_DURATION_OPEN),
776 duration_value: Some(u32::MAX),
777 target_type: Some(u8::MAX),
778 target_value: Some(u32::MAX),
779 custom_target_low: Some(u32::MAX),
780 custom_target_high: Some(u32::MAX),
781 intensity: Some(fit_file::INTENSITY_COOL_DOWN),
782 notes: None,
783 equipment: None,
784 secondary_target_type: Some(u8::MAX),
785 secondary_target_value: Some(u32::MAX),
786 secondary_custom_target_low: Some(u32::MAX),
787 secondary_custom_target_high: Some(u32::MAX),
788 },
789 ],
790 };
791
792 assert_eq!(wko.workout_message, expected.workout_message);
793 for (i, expected_step) in expected.steps.iter().enumerate() {
794 let wko_step = wko.steps.get(i).unwrap();
795 assert_eq!(wko_step, expected_step);
796 }
797 assert_eq!(wko.steps.len(), expected.steps.len());
798 }
799}