fit_file/
lib.rs

1// by Michael J. Simms
2// Copyright (c) 2021 Michael J. Simms
3
4// Permission is hereby granted, free of charge, to any person obtaining a copy
5// of this software and associated documentation files (the "Software"), to deal
6// in the Software without restriction, including without limitation the rights
7// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8// copies of the Software, and to permit persons to whom the Software is
9// furnished to do so, subject to the following conditions:
10// 
11// The above copyright notice and this permission notice shall be included in all
12// copies or substantial portions of the Software.
13// 
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20// SOFTWARE.
21 #![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    /// Called for each record message as it is processed.
31    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                    // Make sure we have a valid reading.
51                    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                    // Make sure we have a valid reading.
66                    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                    // Make sure we have a valid reading.
81                    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            // Increment the number of records processed.
99            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            // Increment the number of records processed.
111            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    /// Context structure. An instance of this will be passed to the parser and ultimately to the callback function so we can use it for whatever.
146    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            // First column is the message name.
354            let msg_name: String = record[0].parse().unwrap();
355            if msg_name.len() > 0 {
356
357                // Print the previous definition, if there is one.
358                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                    // Normalize the field type string.
374                    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), // 60s
474                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_HEART_RATE),
475                     target_value: Some(2), // HR zone 2
476                     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), // 500m
491                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
492                     target_value: Some(5), // Power zone 5
493                     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), // repeat from step with message_index 1
525                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_OPEN),
526                     target_value: Some(3), // 3 repetitions
527                     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), // 125BPM
542                     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), // 60s
590                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_HEART_RATE),
591                     target_value: Some(0),
592                     custom_target_low: Some(50), // 50% max HR
593                     custom_target_high: Some(60), // 50% max HR
594                     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), // 500m
607                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
608                     target_value: Some(0), // Custom
609                     custom_target_low: Some(1300), // 300W
610                     custom_target_high: Some(1310), // 310W
611                     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), // 500m
624                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
625                     target_value: Some(0),
626                     custom_target_low: Some(1260), // 260W
627                     custom_target_high: Some(1270), // 270W
628                     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), // 125 BPM
641                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
642                     target_value: Some(0),
643                     custom_target_low: Some(1220), // 220W
644                     custom_target_high: Some(1230), // 230W
645                     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        // TrainingPeaks exports .fit files with a lot of invalid values for enums
667        // instead of omitting the optional fields or using the specified value in the context
668        // of repeats.
669
670        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), // 100W
695                     custom_target_high: Some(1125), // 125W
696                     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), // 6m
709                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
710                     target_value: Some(0), // Custom
711                     custom_target_low: Some(1212), // 212W
712                     custom_target_high: Some(1238), // 238W
713                     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), // 3m
726                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
727                     target_value: Some(0),
728                     custom_target_low: Some(1125), // 125W
729                     custom_target_high: Some(1150), // 150W
730                     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), // step with message index 1
743                     target_type: Some(u8::MAX),
744                     target_value: Some(4), // 4 repetitions
745                     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), // 10m
760                     target_type: Some(fit_file::WORKOUT_STEP_TARGET_POWER),
761                     target_value: Some(0), // custom
762                     custom_target_low: Some(1100), // 100W
763                     custom_target_high: Some(1125), // 125W
764                     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}