sma() algorithm if not available. private $types = null; // Set by $endianness depending on architecture in Definition Message. private $garmin_timestamps = false; // By default the constant FIT_UNIX_TS_DIFF will be added to timestamps. // Enumerated data looked up by enumData(). // Values from 'Profile.xls' contained within the FIT SDK. private $enum_data = [ 'activity' => [0 => 'manual', 1 => 'auto_multi_sport'], 'ant_network' => [0 => 'public', 1 => 'antplus', 2 => 'antfs', 3 => 'private'], 'battery_status' => [1 => 'new', 2 => 'good', 3 => 'ok', 4 => 'low', 5 => 'critical', 7 => 'unknown'], 'body_location' => [ 0 => 'left_leg', 1 => 'left_calf', 2 => 'left_shin', 3 => 'left_hamstring', 4 => 'left_quad', 5 => 'left_glute', 6 => 'right_leg', 7 => 'right_calf', 8 => 'right_shin', 9 => 'right_hamstring', 10 => 'right_quad', 11 => 'right_glute', 12 => 'torso_back', 13 => 'left_lower_back', 14 => 'left_upper_back', 15 => 'right_lower_back', 16 => 'right_upper_back', 17 => 'torso_front', 18 => 'left_abdomen', 19 => 'left_chest', 20 => 'right_abdomen', 21 => 'right_chest', 22 => 'left_arm', 23 => 'left_shoulder', 24 => 'left_bicep', 25 => 'left_tricep', 26 => 'left_brachioradialis', 27 => 'left_forearm_extensors', 28 => 'right_arm', 29 => 'right_shoulder', 30 => 'right_bicep', 31 => 'right_tricep', 32 => 'right_brachioradialis', 33 => 'right_forearm_extensors', 34 => 'neck', 35 => 'throat' ], 'display_heart' => [0 => 'bpm', 1 => 'max', 2 => 'reserve'], 'display_measure' => [0 => 'metric', 1 => 'statute'], 'display_position' => [ 0 => 'degree', //dd.dddddd 1 => 'degree_minute', //dddmm.mmm 2 => 'degree_minute_second', //dddmmss 3 => 'austrian_grid', //Austrian Grid (BMN) 4 => 'british_grid', //British National Grid 5 => 'dutch_grid', //Dutch grid system 6 => 'hungarian_grid', //Hungarian grid system 7 => 'finnish_grid', //Finnish grid system Zone3 KKJ27 8 => 'german_grid', //Gausss Krueger (German) 9 => 'icelandic_grid', //Icelandic Grid 10 => 'indonesian_equatorial', //Indonesian Equatorial LCO 11 => 'indonesian_irian', //Indonesian Irian LCO 12 => 'indonesian_southern', //Indonesian Southern LCO 13 => 'india_zone_0', //India zone 0 14 => 'india_zone_IA', //India zone IA 15 => 'india_zone_IB', //India zone IB 16 => 'india_zone_IIA', //India zone IIA 17 => 'india_zone_IIB', //India zone IIB 18 => 'india_zone_IIIA', //India zone IIIA 19 => 'india_zone_IIIB', //India zone IIIB 20 => 'india_zone_IVA', //India zone IVA 21 => 'india_zone_IVB', //India zone IVB 22 => 'irish_transverse', //Irish Transverse Mercator 23 => 'irish_grid', //Irish Grid 24 => 'loran', //Loran TD 25 => 'maidenhead_grid', //Maidenhead grid system 26 => 'mgrs_grid', //MGRS grid system 27 => 'new_zealand_grid', //New Zealand grid system 28 => 'new_zealand_transverse', //New Zealand Transverse Mercator 29 => 'qatar_grid', //Qatar National Grid 30 => 'modified_swedish_grid', //Modified RT-90 (Sweden) 31 => 'swedish_grid', //RT-90 (Sweden) 32 => 'south_african_grid', //South African Grid 33 => 'swiss_grid', //Swiss CH-1903 grid 34 => 'taiwan_grid', //Taiwan Grid 35 => 'united_states_grid', //United States National Grid 36 => 'utm_ups_grid', //UTM/UPS grid system 37 => 'west_malayan', //West Malayan RSO 38 => 'borneo_rso', //Borneo RSO 39 => 'estonian_grid', //Estonian grid system 40 => 'latvian_grid', //Latvian Transverse Mercator 41 => 'swedish_ref_99_grid', //Reference Grid 99 TM (Swedish) ], 'display_power' => [0 => 'watts', 1 => 'percent_ftp'], 'event' => [ 0 => 'timer', 3 => 'workout', 4 => 'workout_step', 5 => 'power_down', 6 => 'power_up', 7 => 'off_course', 8 => 'session', 9 => 'lap', 10 => 'course_point', 11 => 'battery', 12 => 'virtual_partner_pace', 13 => 'hr_high_alert', 14 => 'hr_low_alert', 15 => 'speed_high_alert', 16 => 'speed_low_alert', 17 => 'cad_high_alert', 18 => 'cad_low_alert', 19 => 'power_high_alert', 20 => 'power_low_alert', 21 => 'recovery_hr', 22 => 'battery_low', 23 => 'time_duration_alert', 24 => 'distance_duration_alert', 25 => 'calorie_duration_alert', 26 => 'activity', 27 => 'fitness_equipment', 28 => 'length', 32 => 'user_marker', 33 => 'sport_point', 36 => 'calibration', 42 => 'front_gear_change', 43 => 'rear_gear_change', 44 => 'rider_position_change', 45 => 'elev_high_alert', 46 => 'elev_low_alert', 47 => 'comm_timeout' ], 'event_type' => [ 0 => 'start', 1 => 'stop', 2 => 'consecutive_depreciated', 3 => 'marker', 4 => 'stop_all', 5 => 'begin_depreciated', 6 => 'end_depreciated', 7 => 'end_all_depreciated', 8 => 'stop_disable', 9 => 'stop_disable_all' ], 'file' => [ 1 => 'device', 2 => 'settings', 3 => 'sport', 4 => 'activity', 5 => 'workout', 6 => 'course', 7 => 'schedules', 9 => 'weight', 10 => 'totals', 11 => 'goals', 14 => 'blood_pressure', 15 => 'monitoring_a', 20 => 'activity_summary', 28 => 'monitoring_daily', 32 => 'monitoring_b', 0xF7 => 'mfg_range_min', 0xFE => 'mfg_range_max' ], 'gender' => [0 => 'female', 1 => 'male'], 'hr_zone_calc' => [0 => 'custom', 1 => 'percent_max_hr', 2 => 'percent_hrr'], 'intensity' => [0 => 'active', 1 => 'rest', 2 => 'warmup', 3 => 'cooldown'], 'language' => [ 0 => 'english', 1 => 'french', 2 => 'italian', 3 => 'german', 4 => 'spanish', 5 => 'croatian', 6 => 'czech', 7 => 'danish', 8 => 'dutch', 9 => 'finnish', 10 => 'greek', 11 => 'hungarian', 12 => 'norwegian', 13 => 'polish', 14 => 'portuguese', 15 => 'slovakian', 16 => 'slovenian', 17 => 'swedish', 18 => 'russian', 19 => 'turkish', 20 => 'latvian', 21 => 'ukrainian', 22 => 'arabic', 23 => 'farsi', 24 => 'bulgarian', 25 => 'romanian', 254 => 'custom' ], 'length_type' => [0 => 'idle', 1 => 'active'], 'manufacturer' => [ // Have capitalised select manufacturers 1 => 'Garmin', 2 => 'garmin_fr405_antfs', 3 => 'zephyr', 4 => 'dayton', 5 => 'idt', 6 => 'SRM', 7 => 'Quarq', 8 => 'iBike', 9 => 'saris', 10 => 'spark_hk', 11 => 'Tanita', 12 => 'Echowell', 13 => 'dynastream_oem', 14 => 'nautilus', 15 => 'dynastream', 16 => 'Timex', 17 => 'metrigear', 18 => 'xelic', 19 => 'beurer', 20 => 'cardiosport', 21 => 'a_and_d', 22 => 'hmm', 23 => 'Suunto', 24 => 'thita_elektronik', 25 => 'gpulse', 26 => 'clean_mobile', 27 => 'pedal_brain', 28 => 'peaksware', 29 => 'saxonar', 30 => 'lemond_fitness', 31 => 'dexcom', 32 => 'Wahoo Fitness', 33 => 'octane_fitness', 34 => 'archinoetics', 35 => 'the_hurt_box', 36 => 'citizen_systems', 37 => 'Magellan', 38 => 'osynce', 39 => 'holux', 40 => 'concept2', 42 => 'one_giant_leap', 43 => 'ace_sensor', 44 => 'brim_brothers', 45 => 'xplova', 46 => 'perception_digital', 47 => 'bf1systems', 48 => 'pioneer', 49 => 'spantec', 50 => 'metalogics', 51 => '4iiiis', 52 => 'seiko_epson', 53 => 'seiko_epson_oem', 54 => 'ifor_powell', 55 => 'maxwell_guider', 56 => 'star_trac', 57 => 'breakaway', 58 => 'alatech_technology_ltd', 59 => 'mio_technology_europe', 60 => 'Rotor', 61 => 'geonaute', 62 => 'id_bike', 63 => 'Specialized', 64 => 'wtek', 65 => 'physical_enterprises', 66 => 'north_pole_engineering', 67 => 'BKOOL', 68 => 'Cateye', 69 => 'Stages Cycling', 70 => 'Sigmasport', 71 => 'TomTom', 72 => 'peripedal', 73 => 'Wattbike', 76 => 'moxy', 77 => 'ciclosport', 78 => 'powerbahn', 79 => 'acorn_projects_aps', 80 => 'lifebeam', 81 => 'Bontrager', 82 => 'wellgo', 83 => 'scosche', 84 => 'magura', 85 => 'woodway', 86 => 'elite', 87 => 'nielsen_kellerman', 88 => 'dk_city', 89 => 'Tacx', 90 => 'direction_technology', 91 => 'magtonic', 92 => '1partcarbon', 93 => 'inside_ride_technologies', 94 => 'sound_of_motion', 95 => 'stryd', 96 => 'icg', 97 => 'MiPulse', 98 => 'bsx_athletics', 99 => 'look', 100 => 'campagnolo_srl', 101 => 'body_bike_smart', 102 => 'praxisworks', 103 => 'limits_technology', 104 => 'topaction_technology', 105 => 'cosinuss', 106 => 'fitcare', 107 => 'magene', 108 => 'giant_manufacturing_co', 109 => 'tigrasport', 110 => 'salutron', 111 => 'technogym', 112 => 'bryton_sensors', 113 => 'latitude_limited', 114 => 'soaring_technology', 115 => 'igpsport', 116 => 'thinkrider', 117 => 'gopher_sport', 118 => 'waterrower', 119 => 'orangetheory', 120 => 'inpeak', 121 => 'kinetic', 122 => 'johnson_health_tech', 123 => 'polar_electro', 124 => 'seesense', 125 => 'nci_technology', 126 => 'iqsquare', 127 => 'leomo', 128 => 'ifit_com', 255 => 'development', 257 => 'healthandlife', 258 => 'Lezyne', 259 => 'scribe_labs', 260 => 'Zwift', 261 => 'watteam', 262 => 'recon', 263 => 'favero_electronics', 264 => 'dynovelo', 265 => 'Strava', 266 => 'precor', 267 => 'Bryton', 268 => 'sram', 269 => 'navman', 270 => 'cobi', 271 => 'spivi', 272 => 'mio_magellan', 273 => 'evesports', 274 => 'sensitivus_gauge', 275 => 'podoon', 276 => 'life_time_fitness', 277 => 'falco_e_motors', 278 => 'minoura', 279 => 'cycliq', 280 => 'luxottica', 281 => 'trainer_road', 282 => 'the_sufferfest', 283 => 'fullspeedahead', 284 => 'virtualtraining', 285 => 'feedbacksports', 286 => 'omata', 287 => 'vdo', 288 => 'magneticdays', 289 => 'hammerhead', 290 => 'kinetic_by_kurt', 291 => 'shapelog', 292 => 'dabuziduo', 293 => 'jetblack', 294 => 'coros', 295 => 'virtugo', 296 => 'velosense', 297 => 'cycligentinc', 298 => 'trailforks', 299 => 'mahle_ebikemotion', 5759 => 'actigraphcorp' ], 'pwr_zone_calc' => [0 => 'custom', 1 => 'percent_ftp'], 'product' => [ // Have formatted for devices known to use FIT format. (Original text commented-out). 1 => 'hrm1', 2 => 'axh01', 3 => 'axb01', 4 => 'axb02', 5 => 'hrm2ss', 6 => 'dsi_alf02', 7 => 'hrm3ss', 8 => 'hrm_run_single_byte_product_id', 9 => 'bsm', 10 => 'bcm', 11 => 'axs01', 12 => 'HRM-Tri', // 'hrm_tri_single_byte_product_id', 14 => 'Forerunner 225', // 'fr225_single_byte_product_id', 473 => 'Forerunner 301', // 'fr301_china', 474 => 'Forerunner 301', // 'fr301_japan', 475 => 'Forerunner 301', // 'fr301_korea', 494 => 'Forerunner 301', // 'fr301_taiwan', 717 => 'Forerunner 405', // 'fr405', 782 => 'Forerunner 50', // 'fr50', 987 => 'Forerunner 405', // 'fr405_japan', 988 => 'Forerunner 60', // 'fr60', 1011 => 'dsi_alf01', 1018 => 'Forerunner 310XT', // 'fr310xt', 1036 => 'Edge 500', // 'edge500', 1124 => 'Forerunner 110', // 'fr110', 1169 => 'Edge 800', // 'edge800', 1199 => 'Edge 500', // 'edge500_taiwan', 1213 => 'Edge 500', // 'edge500_japan', 1253 => 'chirp', 1274 => 'Forerunner 110', // 'fr110_japan', 1325 => 'edge200', 1328 => 'Forerunner 910XT', // 'fr910xt', 1333 => 'Edge 800', // 'edge800_taiwan', 1334 => 'Edge 800', // 'edge800_japan', 1341 => 'alf04', 1345 => 'Forerunner 610', // 'fr610', 1360 => 'Forerunner 210', // 'fr210_japan', 1380 => 'Vector 2S', // vector_ss 1381 => 'Vector 2', // vector_cp 1386 => 'Edge 800', // 'edge800_china', 1387 => 'Edge 500', // 'edge500_china', 1410 => 'Forerunner 610', // 'fr610_japan', 1422 => 'Edge 500', // 'edge500_korea', 1436 => 'Forerunner 70', // 'fr70', 1446 => 'Forerunner 310XT', // 'fr310xt_4t', 1461 => 'amx', 1482 => 'Forerunner 10', // 'fr10', 1497 => 'Edge 800', // 'edge800_korea', 1499 => 'swim', 1537 => 'Forerunner 910XT', // 'fr910xt_china', 1551 => 'Fenix', // fenix 1555 => 'edge200_taiwan', 1561 => 'Edge 510', // 'edge510', 1567 => 'Edge 810', // 'edge810', 1570 => 'tempe', 1600 => 'Forerunner 910XT', // 'fr910xt_japan', 1623 => 'Forerunner 620', // 'fr620', 1632 => 'Forerunner 220', // 'fr220', 1664 => 'Forerunner 910XT', // 'fr910xt_korea', 1688 => 'Forerunner 10', // 'fr10_japan', 1721 => 'Edge 810', // 'edge810_japan', 1735 => 'virb_elite', 1736 => 'edge_touring', 1742 => 'Edge 510', // 'edge510_japan', 1743 => 'HRM-Tri', // 'hrm_tri', 1752 => 'hrm_run', 1765 => 'Forerunner 920XT', // 'fr920xt', 1821 => 'Edge 510', // 'edge510_asia', 1822 => 'Edge 810', // 'edge810_china', 1823 => 'Edge 810', // 'edge810_taiwan', 1836 => 'Edge 1000', // 'edge1000', 1837 => 'vivo_fit', 1853 => 'virb_remote', 1885 => 'vivo_ki', 1903 => 'Forerunner 15', // 'fr15', 1907 => 'vivoactive', // 'vivo_active', 1918 => 'Edge 510', // 'edge510_korea', 1928 => 'Forerunner 620', // 'fr620_japan', 1929 => 'Forerunner 620', // 'fr620_china', 1930 => 'Forerunner 220', // 'fr220_japan', 1931 => 'Forerunner 220', // 'fr220_china', 1936 => 'Approach S6', // 'approach_s6' 1956 => 'vívosmart', // 'vivo_smart', 1967 => 'Fenix 2', // fenix2 1988 => 'epix', 2050 => 'Fenix 3', // fenix3 2052 => 'Edge 1000', // edge1000_taiwan 2053 => 'Edge 1000', // edge1000_japan 2061 => 'Forerunner 15', // fr15_japan 2067 => 'Edge 520', // edge520 2070 => 'Edge 1000', // edge1000_china 2072 => 'Forerunner 620', // fr620_russia 2073 => 'Forerunner 220', // fr220_russia 2079 => 'Vector S', // vector_s 2100 => 'Edge 1000', // edge1000_korea 2130 => 'Forerunner 920', // fr920xt_taiwan 2131 => 'Forerunner 920', // fr920xt_china 2132 => 'Forerunner 920', // fr920xt_japan 2134 => 'virbx', 2135 => 'vívosmart', // vivo_smart_apac', 2140 => 'etrex_touch', 2147 => 'Edge 25', // edge25 2148 => 'Forerunner 25', // fr25 2150 => 'vivo_fit2', 2153 => 'Forerunner 225', // fr225 2156 => 'Forerunner 630', // fr630 2157 => 'Forerunner 230', // fr230 2160 => 'vivo_active_apac', 2161 => 'Vector 2', // vector_2 2162 => 'Vector 2S', // vector_2s 2172 => 'virbxe', 2173 => 'Forerunner 620', // fr620_taiwan 2174 => 'Forerunner 220', // fr220_taiwan 2175 => 'truswing', 2188 => 'Fenix 3', // fenix3_china 2189 => 'Fenix 3', // fenix3_twn 2192 => 'varia_headlight', 2193 => 'varia_taillight_old', 2204 => 'Edge Explore 1000', // edge_explore_1000 2219 => 'Forerunner 225', // fr225_asia 2225 => 'varia_radar_taillight', 2226 => 'varia_radar_display', 2238 => 'Edge 20', // edge20 2262 => 'D2 Bravo', // d2_bravo 2266 => 'approach_s20', 2276 => 'varia_remote', 2327 => 'hrm4_run', 2337 => 'vivo_active_hr', 2347 => 'vivo_smart_gps_hr', 2348 => 'vivo_smart_hr', 2368 => 'vivo_move', 2398 => 'varia_vision', 2406 => 'vivo_fit3', 2413 => 'Fenix 3 HR', // fenix3_hr 2417 => 'Virb Ultra 30', // virb_ultra_30 2429 => 'index_smart_scale', 2431 => 'Forerunner 235', // fr235 2432 => 'Fenix 3 Chronos', // fenix3_chronos 2441 => 'oregon7xx', 2444 => 'rino7xx', 2496 => 'nautix', 2530 => 'Edge 820', // edge_820 2531 => 'Edge Explore 820', // edge_explore_820 2544 => 'fenix5s', 2547 => 'D2 Bravo Titanium', // d2_bravo_titanium 2593 => 'Running Dynamics Pod', // running_dynamics_pod 2604 => 'Fenix 5x', // fenix5x 2606 => 'vivofit jr', // vivo_fit_jr 2691 => 'Forerunner 935', // fr935 2697 => 'Fenix 5', // fenix5 2700 => 'vivoactive3', 2769 => 'foretrex_601_701', 2772 => 'vivo_move_hr', 2713 => 'Edge 1030', // edge_1030 2806 => 'approach_z80', 2831 => 'vivo_smart3_apac', 2832 => 'vivo_sport_apac', 2859 => 'descent', 2886 => 'Forerunner 645', // fr645 2888 => 'Forerunner 645', // fr645m 2900 => 'Fenix 5S Plus', // fenix5s_plus 2909 => 'Edge 130', // Edge_130 2927 => 'vivosmart_4', 2962 => 'approach_x10', 2988 => 'vivoactive3m_w', 3011 => 'edge_explore', 3028 => 'gpsmap66', 3049 => 'approach_s10', 3066 => 'vivoactive3m_l', 3085 => 'approach_g80', 3110 => 'Fenix 5 Plus', // fenix5_plus 3111 => 'Fenix 5X Plus', // fenix5x_plus 3112 => 'Edge 520 Plus', // edge_520_plus 3299 => 'hrm_dual', 3314 => 'approach_s40', 10007 => 'SDM4 footpod', // sdm4 10014 => 'edge_remote', 20119 => 'training_center', 65531 => 'connectiq_simulator', 65532 => 'android_antplus_plugin', 65534 => 'Garmin Connect website' // connect ], 'sport' => [ // Have capitalised and replaced underscores with spaces. 0 => 'Generic', 1 => 'Running', 2 => 'Cycling', 3 => 'Transition', 4 => 'Fitness equipment', 5 => 'Swimming', 6 => 'Basketball', 7 => 'Soccer', 8 => 'Tennis', 9 => 'American football', 10 => 'Training', 11 => 'Walking', 12 => 'Cross country skiing', 13 => 'Alpine skiing', 14 => 'Snowboarding', 15 => 'Rowing', 16 => 'Mountaineering', 17 => 'Hiking', 18 => 'Multisport', 19 => 'Paddling', 254 => 'All' ], 'sub_sport' => [ // Have capitalised and replaced underscores with spaces. 0 => 'Generic', 1 => 'Treadmill', 2 => 'Street', 3 => 'Trail', 4 => 'Track', 5 => 'Spin', 6 => 'Indoor cycling', 7 => 'Road', 8 => 'Mountain', 9 => 'Downhill', 10 => 'Recumbent', 11 => 'Cyclocross', 12 => 'Hand cycling', 13 => 'Track cycling', 14 => 'Indoor rowing', 15 => 'Elliptical', 16 => 'Stair climbing', 17 => 'Lap swimming', 18 => 'Open water', 19 => 'Flexibility training', 20 => 'Strength training', 21 => 'Warm up', 22 => 'Match', 23 => 'Exercise', 24 => 'Challenge', 25 => 'Indoor skiing', 26 => 'Cardio training', 27 => 'Indoor walking', 28 => 'E-Bike Fitness', 254 => 'All' ], 'session_trigger' => [0 => 'activity_end', 1 => 'manual', 2 => 'auto_multi_sport', 3 => 'fitness_equipment'], 'source_type' => [ 0 => 'ant', //External device connected with ANT 1 => 'antplus', //External device connected with ANT+ 2 => 'bluetooth', //External device connected with BT 3 => 'bluetooth_low_energy', //External device connected with BLE 4 => 'wifi', //External device connected with Wifi 5 => 'local', //Onboard device ], 'swim_stroke' => [0 => 'Freestyle', 1 => 'Backstroke', 2 => 'Breaststroke', 3 => 'Butterfly', 4 => 'Drill', 5 => 'Mixed', 6 => 'IM'], // Have capitalised. 'water_type' => [0 => 'fresh', 1 => 'salt', 2 => 'en13319', 3 => 'custom'], 'tissue_model_type' => [0 => 'zhl_16c'], 'dive_gas_status' => [0 => 'disabled', 1 => 'enabled', 2 => 'backup_only'], 'dive_alarm_type' => [0 => 'depth', 1 => 'time'], 'dive_backlight_mode' => [0 => 'at_depth', 1 => 'always_on'], ]; /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 2.2.pdf * Table 4-6. FIT Base Types and Invalid Values * * $types array holds a string used by the PHP unpack() function to format binary data. * 'tmp' is the name of the (single element) array created. */ private $endianness = [ 0 => [ // Little Endianness 0 => ['format' => 'Ctmp', 'bytes' => 1], // enum 1 => ['format' => 'ctmp', 'bytes' => 1], // sint8 2 => ['format' => 'Ctmp', 'bytes' => 1], // uint8 131 => ['format' => 'vtmp', 'bytes' => 2], // sint16 - manually convert uint16 to sint16 in fixData() 132 => ['format' => 'vtmp', 'bytes' => 2], // uint16 133 => ['format' => 'Vtmp', 'bytes' => 4], // sint32 - manually convert uint32 to sint32 in fixData() 134 => ['format' => 'Vtmp', 'bytes' => 4], // uint32 7 => ['format' => 'a*tmp', 'bytes' => 1], // string 136 => ['format' => 'ftmp', 'bytes' => 4], // float32 137 => ['format' => 'dtmp', 'bytes' => 8], // float64 10 => ['format' => 'Ctmp', 'bytes' => 1], // uint8z 139 => ['format' => 'vtmp', 'bytes' => 2], // uint16z 140 => ['format' => 'Vtmp', 'bytes' => 4], // uint32z 13 => ['format' => 'Ctmp', 'bytes' => 1], // byte 142 => ['format' => 'Ptmp', 'bytes' => 8], // sint64 - manually convert uint64 to sint64 in fixData() 143 => ['format' => 'Ptmp', 'bytes' => 8], // uint64 144 => ['format' => 'Ptmp', 'bytes' => 8] // uint64z ], 1 => [ // Big Endianness 0 => ['format' => 'Ctmp', 'bytes' => 1], // enum 1 => ['format' => 'ctmp', 'bytes' => 1], // sint8 2 => ['format' => 'Ctmp', 'bytes' => 1], // uint8 131 => ['format' => 'ntmp', 'bytes' => 2], // sint16 - manually convert uint16 to sint16 in fixData() 132 => ['format' => 'ntmp', 'bytes' => 2], // uint16 133 => ['format' => 'Ntmp', 'bytes' => 4], // sint32 - manually convert uint32 to sint32 in fixData() 134 => ['format' => 'Ntmp', 'bytes' => 4], // uint32 7 => ['format' => 'a*tmp', 'bytes' => 1], // string 136 => ['format' => 'ftmp', 'bytes' => 4], // float32 137 => ['format' => 'dtmp', 'bytes' => 8], // float64 10 => ['format' => 'Ctmp', 'bytes' => 1], // uint8z 139 => ['format' => 'ntmp', 'bytes' => 2], // uint16z 140 => ['format' => 'Ntmp', 'bytes' => 4], // uint32z 13 => ['format' => 'Ctmp', 'bytes' => 1], // byte 142 => ['format' => 'Jtmp', 'bytes' => 8], // sint64 - manually convert uint64 to sint64 in fixData() 143 => ['format' => 'Jtmp', 'bytes' => 8], // uint64 144 => ['format' => 'Jtmp', 'bytes' => 8] // uint64z ] ]; private $invalid_values = [ 0 => 255, // 0xFF 1 => 127, // 0x7F 2 => 255, // 0xFF 131 => 32767, // 0x7FFF 132 => 65535, // 0xFFFF 133 => 2147483647, // 0x7FFFFFFF 134 => 4294967295, // 0xFFFFFFFF 7 => 0, // 0x00 136 => 4294967295, // 0xFFFFFFFF 137 => 9223372036854775807, // 0xFFFFFFFFFFFFFFFF 10 => 0, // 0x00 139 => 0, // 0x0000 140 => 0, // 0x00000000 13 => 255, // 0xFF 142 => 9223372036854775807, // 0x7FFFFFFFFFFFFFFF 143 => 18446744073709551615, // 0xFFFFFFFFFFFFFFFF 144 => 0 // 0x0000000000000000 ]; /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 1.7.pdf * 4.4 Scale/Offset * When specified, the binary quantity is divided by the scale factor and then the offset is subtracted, yielding a floating point quantity. */ private $data_mesg_info = [ 0 => [ 'mesg_name' => 'file_id', 'field_defns' => [ 0 => ['field_name' => 'type', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'manufacturer', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'product', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'serial_number', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'time_created', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'number', 'scale' => 1, 'offset' => 0, 'units' => ''], ] ], 2 => [ 'mesg_name' => 'device_settings', 'field_defns' => [ 0 => ['field_name' => 'active_time_zone', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'utc_offset', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'time_zone_offset', 'scale' => 4, 'offset' => 0, 'units' => 'hr'], ] ], 3 => [ 'mesg_name' => 'user_profile', 'field_defns' => [ 0 => ['field_name' => 'friendly_name', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'gender', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'age', 'scale' => 1, 'offset' => 0, 'units' => 'years'], 3 => ['field_name' => 'height', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 4 => ['field_name' => 'weight', 'scale' => 10, 'offset' => 0, 'units' => 'kg'], 5 => ['field_name' => 'language', 'scale' => 1, 'offset' => 0, 'units' => ''], 6 => ['field_name' => 'elev_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 7 => ['field_name' => 'weight_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 8 => ['field_name' => 'resting_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 10 => ['field_name' => 'default_max_biking_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 11 => ['field_name' => 'default_max_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 12 => ['field_name' => 'hr_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 13 => ['field_name' => 'speed_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 14 => ['field_name' => 'dist_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 16 => ['field_name' => 'power_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 17 => ['field_name' => 'activity_class', 'scale' => 1, 'offset' => 0, 'units' => ''], 18 => ['field_name' => 'position_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], 21 => ['field_name' => 'temperature_setting', 'scale' => 1, 'offset' => 0, 'units' => ''], ] ], 7 => [ 'mesg_name' => 'zones_target', 'field_defns' => [ 1 => ['field_name' => 'max_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'threshold_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'functional_threshold_power', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'hr_calc_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 7 => ['field_name' => 'pwr_calc_type', 'scale' => 1, 'offset' => 0, 'units' => ''], ] ], 12 => [ 'mesg_name' => 'sport', 'field_defns' => [ 0 => ['field_name' => 'sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'sub_sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'name', 'scale' => 1, 'offset' => 0, 'units' => ''], ] ], 18 => [ 'mesg_name' => 'session', 'field_defns' => [ 0 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'start_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'start_position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 4 => ['field_name' => 'start_position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 5 => ['field_name' => 'sport', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 6 => ['field_name' => 'sub_sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 7 => ['field_name' => 'total_elapsed_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 8 => ['field_name' => 'total_timer_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 9 => ['field_name' => 'total_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 10 => ['field_name' => 'total_cycles', 'scale' => 1, 'offset' => 0, 'units' => 'cycles'], 11 => ['field_name' => 'total_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 13 => ['field_name' => 'total_fat_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 14 => ['field_name' => 'avg_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 15 => ['field_name' => 'max_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 16 => ['field_name' => 'avg_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 17 => ['field_name' => 'max_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 18 => ['field_name' => 'avg_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 19 => ['field_name' => 'max_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 20 => ['field_name' => 'avg_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 21 => ['field_name' => 'max_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 22 => ['field_name' => 'total_ascent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 23 => ['field_name' => 'total_descent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 24 => ['field_name' => 'total_training_effect', 'scale' => 10, 'offset' => 0, 'units' => ''], 25 => ['field_name' => 'first_lap_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 26 => ['field_name' => 'num_laps', 'scale' => 1, 'offset' => 0, 'units' => ''], 27 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 28 => ['field_name' => 'trigger', 'scale' => 1, 'offset' => 0, 'units' => ''], 29 => ['field_name' => 'nec_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 30 => ['field_name' => 'nec_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 31 => ['field_name' => 'swc_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 32 => ['field_name' => 'swc_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 34 => ['field_name' => 'normalized_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 35 => ['field_name' => 'training_stress_score', 'scale' => 10, 'offset' => 0, 'units' => 'tss'], 36 => ['field_name' => 'intensity_factor', 'scale' => 1000, 'offset' => 0, 'units' => 'if'], 37 => ['field_name' => 'left_right_balance', 'scale' => 1, 'offset' => 0, 'units' => ''], 41 => ['field_name' => 'avg_stroke_count', 'scale' => 10, 'offset' => 0, 'units' => 'strokes/lap'], 42 => ['field_name' => 'avg_stroke_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 43 => ['field_name' => 'swim_stroke', 'scale' => 1, 'offset' => 0, 'units' => 'swim_stroke'], 44 => ['field_name' => 'pool_length', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 45 => ['field_name' => 'threshold_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 46 => ['field_name' => 'pool_length_unit', 'scale' => 1, 'offset' => 0, 'units' => ''], 47 => ['field_name' => 'num_active_lengths', 'scale' => 1, 'offset' => 0, 'units' => 'lengths'], 48 => ['field_name' => 'total_work', 'scale' => 1, 'offset' => 0, 'units' => 'J'], 65 => ['field_name' => 'time_in_hr_zone', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 68 => ['field_name' => 'time_in_power_zone', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 89 => ['field_name' => 'avg_vertical_oscillation', 'scale' => 10, 'offset' => 0, 'units' => 'mm'], 90 => ['field_name' => 'avg_stance_time_percent', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 91 => ['field_name' => 'avg_stance_time', 'scale' => 10, 'offset' => 0, 'units' => 'ms'], 92 => ['field_name' => 'avg_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 93 => ['field_name' => 'max_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 94 => ['field_name' => 'total_fractional_cycles', 'scale' => 128, 'offset' => 0, 'units' => 'cycles'], 101 => ['field_name' => 'avg_left_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 102 => ['field_name' => 'avg_right_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 103 => ['field_name' => 'avg_left_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 104 => ['field_name' => 'avg_right_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 105 => ['field_name' => 'avg_combined_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 111 => ['field_name' => 'sport_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 112 => ['field_name' => 'time_standing', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 113 => ['field_name' => 'stand_count', 'scale' => 1, 'offset' => 0, 'units' => ''], 114 => ['field_name' => 'avg_left_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 115 => ['field_name' => 'avg_right_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 116 => ['field_name' => 'avg_left_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 117 => ['field_name' => 'avg_left_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 118 => ['field_name' => 'avg_right_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 119 => ['field_name' => 'avg_right_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 120 => ['field_name' => 'avg_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 121 => ['field_name' => 'max_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 122 => ['field_name' => 'avg_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 123 => ['field_name' => 'max_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''], ] ], 19 => [ 'mesg_name' => 'lap', 'field_defns' => [ 0 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'start_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'start_position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 4 => ['field_name' => 'start_position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 5 => ['field_name' => 'end_position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 6 => ['field_name' => 'end_position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 7 => ['field_name' => 'total_elapsed_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 8 => ['field_name' => 'total_timer_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 9 => ['field_name' => 'total_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 10 => ['field_name' => 'total_cycles', 'scale' => 1, 'offset' => 0, 'units' => 'cycles'], 11 => ['field_name' => 'total_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 12 => ['field_name' => 'total_fat_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 13 => ['field_name' => 'avg_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 14 => ['field_name' => 'max_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 15 => ['field_name' => 'avg_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 16 => ['field_name' => 'max_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 17 => ['field_name' => 'avg_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 18 => ['field_name' => 'max_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 19 => ['field_name' => 'avg_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 20 => ['field_name' => 'max_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 21 => ['field_name' => 'total_ascent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 22 => ['field_name' => 'total_descent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 23 => ['field_name' => 'intensity', 'scale' => 1, 'offset' => 0, 'units' => ''], 24 => ['field_name' => 'lap_trigger', 'scale' => 1, 'offset' => 0, 'units' => ''], 25 => ['field_name' => 'sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 26 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 32 => ['field_name' => 'num_lengths', 'scale' => 1, 'offset' => 0, 'units' => 'lengths'], 33 => ['field_name' => 'normalized_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 34 => ['field_name' => 'left_right_balance', 'scale' => 1, 'offset' => 0, 'units' => ''], 35 => ['field_name' => 'first_length_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 37 => ['field_name' => 'avg_stroke_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 38 => ['field_name' => 'swim_stroke', 'scale' => 1, 'offset' => 0, 'units' => ''], 39 => ['field_name' => 'sub_sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 40 => ['field_name' => 'num_active_lengths', 'scale' => 1, 'offset' => 0, 'units' => 'lengths'], 41 => ['field_name' => 'total_work', 'scale' => 1, 'offset' => 0, 'units' => 'J'], 57 => ['field_name' => 'time_in_hr_zone', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 60 => ['field_name' => 'time_in_power_zone', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 71 => ['field_name' => 'wkt_step_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 77 => ['field_name' => 'avg_vertical_oscillation', 'scale' => 10, 'offset' => 0, 'units' => 'mm'], 78 => ['field_name' => 'avg_stance_time_percent', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 79 => ['field_name' => 'avg_stance_time', 'scale' => 10, 'offset' => 0, 'units' => 'ms'], 80 => ['field_name' => 'avg_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 81 => ['field_name' => 'max_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 82 => ['field_name' => 'total_fractional_cycles', 'scale' => 128, 'offset' => 0, 'units' => 'cycles'], 91 => ['field_name' => 'avg_left_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 92 => ['field_name' => 'avg_right_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 93 => ['field_name' => 'avg_left_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 94 => ['field_name' => 'avg_right_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 95 => ['field_name' => 'avg_combined_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 98 => ['field_name' => 'time_standing', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 99 => ['field_name' => 'stand_count', 'scale' => 1, 'offset' => 0, 'units' => ''], 100 => ['field_name' => 'avg_left_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 101 => ['field_name' => 'avg_right_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 102 => ['field_name' => 'avg_left_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 103 => ['field_name' => 'avg_left_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 104 => ['field_name' => 'avg_right_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 105 => ['field_name' => 'avg_right_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 106 => ['field_name' => 'avg_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 107 => ['field_name' => 'max_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 108 => ['field_name' => 'avg_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 109 => ['field_name' => 'max_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 20 => [ 'mesg_name' => 'record', 'field_defns' => [ 0 => ['field_name' => 'position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 1 => ['field_name' => 'position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 2 => ['field_name' => 'altitude', 'scale' => 5, 'offset' => 500, 'units' => 'm'], 3 => ['field_name' => 'heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 4 => ['field_name' => 'cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 5 => ['field_name' => 'distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 6 => ['field_name' => 'speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 7 => ['field_name' => 'power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 8 => ['field_name' => 'compressed_speed_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm/s,m'], 9 => ['field_name' => 'grade', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 10 => ['field_name' => 'resistance', 'scale' => 1, 'offset' => 0, 'units' => ''], 11 => ['field_name' => 'time_from_course', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 12 => ['field_name' => 'cycle_length', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 13 => ['field_name' => 'temperature', 'scale' => 1, 'offset' => 0, 'units' => 'C'], 17 => ['field_name' => 'speed_1s', 'scale' => 16, 'offset' => 0, 'units' => 'm/s'], 18 => ['field_name' => 'cycles', 'scale' => 1, 'offset' => 0, 'units' => 'cycles'], 19 => ['field_name' => 'total_cycles', 'scale' => 1, 'offset' => 0, 'units' => 'cycles'], 28 => ['field_name' => 'compressed_accumulated_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 29 => ['field_name' => 'accumulated_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 30 => ['field_name' => 'left_right_balance', 'scale' => 1, 'offset' => 0, 'units' => ''], 31 => ['field_name' => 'gps_accuracy', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 32 => ['field_name' => 'vertical_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 33 => ['field_name' => 'calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 39 => ['field_name' => 'vertical_oscillation', 'scale' => 10, 'offset' => 0, 'units' => 'mm'], 40 => ['field_name' => 'stance_time_percent', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 41 => ['field_name' => 'stance_time', 'scale' => 10, 'offset' => 0, 'units' => 'ms'], 42 => ['field_name' => 'activity_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 43 => ['field_name' => 'left_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 44 => ['field_name' => 'right_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 45 => ['field_name' => 'left_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 46 => ['field_name' => 'right_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 47 => ['field_name' => 'combined_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 48 => ['field_name' => 'time128', 'scale' => 128, 'offset' => 0, 'units' => 's'], 49 => ['field_name' => 'stroke_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 50 => ['field_name' => 'zone', 'scale' => 1, 'offset' => 0, 'units' => ''], 51 => ['field_name' => 'ball_speed', 'scale' => 100, 'offset' => 0, 'units' => 'm/s'], 52 => ['field_name' => 'cadence256', 'scale' => 256, 'offset' => 0, 'units' => 'rpm'], 53 => ['field_name' => 'fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 54 => ['field_name' => 'total_hemoglobin_conc', 'scale' => 100, 'offset' => 0, 'units' => 'g/dL'], 55 => ['field_name' => 'total_hemoglobin_conc_min', 'scale' => 100, 'offset' => 0, 'units' => 'g/dL'], 56 => ['field_name' => 'total_hemoglobin_conc_max', 'scale' => 100, 'offset' => 0, 'units' => 'g/dL'], 57 => ['field_name' => 'saturated_hemoglobin_percent', 'scale' => 10, 'offset' => 0, 'units' => '%'], 58 => ['field_name' => 'saturated_hemoglobin_percent_min', 'scale' => 10, 'offset' => 0, 'units' => '%'], 59 => ['field_name' => 'saturated_hemoglobin_percent_max', 'scale' => 10, 'offset' => 0, 'units' => '%'], 62 => ['field_name' => 'device_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 67 => ['field_name' => 'left_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 68 => ['field_name' => 'right_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 69 => ['field_name' => 'left_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 70 => ['field_name' => 'left_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 71 => ['field_name' => 'right_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 72 => ['field_name' => 'right_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 73 => ['field_name' => 'enhanced_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 78 => ['field_name' => 'enhanced_altitude', 'scale' => 5, 'offset' => 500, 'units' => 'm'], 81 => ['field_name' => 'battery_soc', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 82 => ['field_name' => 'motor_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 83 => ['field_name' => 'vertical_ratio', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 84 => ['field_name' => 'stance_time_balance', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 85 => ['field_name' => 'step_length', 'scale' => 10, 'offset' => 0, 'units' => 'mm'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ], 21 => [ 'mesg_name' => 'event', 'field_defns' => [ 0 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'data', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ], 23 => [ 'mesg_name' => 'device_info', 'field_defns' => [ 0 => ['field_name' => 'device_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'device_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'manufacturer', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'serial_number', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'product', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'software_version', 'scale' => 1, 'offset' => 0, 'units' => ''], 6 => ['field_name' => 'hardware_version', 'scale' => 1, 'offset' => 0, 'units' => ''], 7 => ['field_name' => 'cum_operating_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 10 => ['field_name' => 'battery_voltage', 'scale' => 1, 'offset' => 0, 'units' => ''], 11 => ['field_name' => 'battery_status', 'scale' => 1, 'offset' => 0, 'units' => ''], 20 => ['field_name' => 'ant_transmission_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 21 => ['field_name' => 'ant_device_number', 'scale' => 1, 'offset' => 0, 'units' => ''], 22 => ['field_name' => 'ant_network', 'scale' => 1, 'offset' => 0, 'units' => ''], 25 => ['field_name' => 'source_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ], 34 => [ 'mesg_name' => 'activity', 'field_defns' => [ 0 => ['field_name' => 'total_timer_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 1 => ['field_name' => 'num_sessions', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'type', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'local_timestamp', 'scale' => 1, 'offset' => 0, 'units' => ''], 6 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ], 49 => [ 'mesg_name' => 'file_creator', 'field_defns' => [ 0 => ['field_name' => 'software_version', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'hardware_version', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 78 => [ 'mesg_name' => 'hrv', 'field_defns' => [ 0 => ['field_name' => 'time', 'scale' => 1000, 'offset' => 0, 'units' => 's'] ] ], 101 => [ 'mesg_name' => 'length', 'field_defns' => [ 0 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'start_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'total_elapsed_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 4 => ['field_name' => 'total_timer_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 5 => ['field_name' => 'total_strokes', 'scale' => 1, 'offset' => 0, 'units' => 'strokes'], 6 => ['field_name' => 'avg_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 7 => ['field_name' => 'swim_stroke', 'scale' => 1, 'offset' => 0, 'units' => 'swim_stroke'], 9 => ['field_name' => 'avg_swimming_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'strokes/min'], 10 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 11 => ['field_name' => 'total_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 12 => ['field_name' => 'length_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], // 'event_timestamp' and 'event_timestamp_12' should have scale of 1024 but due to floating point rounding errors. // These are manually divided by 1024 later in the processHrMessages() function. 132 => [ 'mesg_name' => 'hr', 'field_defns' => [ 0 => ['field_name' => 'fractional_timestamp', 'scale' => 32768, 'offset' => 0, 'units' => 's'], 1 => ['field_name' => 'time256', 'scale' => 256, 'offset' => 0, 'units' => 's'], 6 => ['field_name' => 'filtered_bpm', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 9 => ['field_name' => 'event_timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'], 10 => ['field_name' => 'event_timestamp_12', 'scale' => 1, 'offset' => 0, 'units' => 's'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ], 142 => [ 'mesg_name' => 'segment_lap', 'field_defns' => [ 0 => ['field_name' => 'event', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'event_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'start_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'start_position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 4 => ['field_name' => 'start_position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 5 => ['field_name' => 'end_position_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 6 => ['field_name' => 'end_position_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 7 => ['field_name' => 'total_elapsed_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 8 => ['field_name' => 'total_timer_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 9 => ['field_name' => 'total_distance', 'scale' => 100, 'offset' => 0, 'units' => 'm'], 10 => ['field_name' => 'total_cycles', 'scale' => 1, 'offset' => 0, 'units' => 'cycles'], 11 => ['field_name' => 'total_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 12 => ['field_name' => 'total_fat_calories', 'scale' => 1, 'offset' => 0, 'units' => 'kcal'], 13 => ['field_name' => 'avg_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 14 => ['field_name' => 'max_speed', 'scale' => 1000, 'offset' => 0, 'units' => 'm/s'], 15 => ['field_name' => 'avg_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 16 => ['field_name' => 'max_heart_rate', 'scale' => 1, 'offset' => 0, 'units' => 'bpm'], 17 => ['field_name' => 'avg_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 18 => ['field_name' => 'max_cadence', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 19 => ['field_name' => 'avg_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 20 => ['field_name' => 'max_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 21 => ['field_name' => 'total_ascent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 22 => ['field_name' => 'total_descent', 'scale' => 1, 'offset' => 0, 'units' => 'm'], 23 => ['field_name' => 'sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 24 => ['field_name' => 'event_group', 'scale' => 1, 'offset' => 0, 'units' => ''], 25 => ['field_name' => 'nec_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 26 => ['field_name' => 'nec_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 27 => ['field_name' => 'swc_lat', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 28 => ['field_name' => 'swc_long', 'scale' => 1, 'offset' => 0, 'units' => 'semicircles'], 29 => ['field_name' => 'name', 'scale' => 1, 'offset' => 0, 'units' => ''], 30 => ['field_name' => 'normalized_power', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 31 => ['field_name' => 'left_right_balance', 'scale' => 1, 'offset' => 0, 'units' => ''], 32 => ['field_name' => 'sub_sport', 'scale' => 1, 'offset' => 0, 'units' => ''], 33 => ['field_name' => 'total_work', 'scale' => 1, 'offset' => 0, 'units' => 'J'], 58 => ['field_name' => 'sport_event', 'scale' => 1, 'offset' => 0, 'units' => ''], 59 => ['field_name' => 'avg_left_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 60 => ['field_name' => 'avg_right_torque_effectiveness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 61 => ['field_name' => 'avg_left_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 62 => ['field_name' => 'avg_right_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 63 => ['field_name' => 'avg_combined_pedal_smoothness', 'scale' => 2, 'offset' => 0, 'units' => 'percent'], 64 => ['field_name' => 'status', 'scale' => 1, 'offset' => 0, 'units' => ''], 65 => ['field_name' => 'uuid', 'scale' => 1, 'offset' => 0, 'units' => ''], 66 => ['field_name' => 'avg_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 67 => ['field_name' => 'max_fractional_cadence', 'scale' => 128, 'offset' => 0, 'units' => 'rpm'], 68 => ['field_name' => 'total_fractional_cycles', 'scale' => 128, 'offset' => 0, 'units' => 'cycles'], 69 => ['field_name' => 'front_gear_shift_count', 'scale' => 1, 'offset' => 0, 'units' => ''], 70 => ['field_name' => 'rear_gear_shift_count', 'scale' => 1, 'offset' => 0, 'units' => ''], 71 => ['field_name' => 'time_standing', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 72 => ['field_name' => 'stand_count', 'scale' => 1, 'offset' => 0, 'units' => ''], 73 => ['field_name' => 'avg_left_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 74 => ['field_name' => 'avg_right_pco', 'scale' => 1, 'offset' => 0, 'units' => 'mm'], 75 => ['field_name' => 'avg_left_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 76 => ['field_name' => 'avg_left_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 77 => ['field_name' => 'avg_right_power_phase', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 78 => ['field_name' => 'avg_right_power_phase_peak', 'scale' => 0.7111111, 'offset' => 0, 'units' => 'degrees'], 79 => ['field_name' => 'avg_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 80 => ['field_name' => 'max_power_position', 'scale' => 1, 'offset' => 0, 'units' => 'watts'], 81 => ['field_name' => 'avg_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 82 => ['field_name' => 'max_cadence_position', 'scale' => 1, 'offset' => 0, 'units' => 'rpm'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 206 => [ 'mesg_name' => 'field_description', 'field_defns' => [ 0 => ['field_name' => 'developer_data_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'field_definition_number', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'fit_base_type_id', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'field_name', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'array', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'components', 'scale' => 1, 'offset' => 0, 'units' => ''], 6 => ['field_name' => 'scale', 'scale' => 1, 'offset' => 0, 'units' => ''], 7 => ['field_name' => 'offset', 'scale' => 1, 'offset' => 0, 'units' => ''], 8 => ['field_name' => 'units', 'scale' => 1, 'offset' => 0, 'units' => ''], 9 => ['field_name' => 'bits', 'scale' => 1, 'offset' => 0, 'units' => ''], 10 => ['field_name' => 'accumulate', 'scale' => 1, 'offset' => 0, 'units' => ''], 13 => ['field_name' => 'fit_base_unit_id', 'scale' => 1, 'offset' => 0, 'units' => ''], 14 => ['field_name' => 'native_mesg_num', 'scale' => 1, 'offset' => 0, 'units' => ''], 15 => ['field_name' => 'native_field_num', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 207 => [ 'mesg_name' => 'developer_data_id', 'field_defns' => [ 0 => ['field_name' => 'developer_id', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'application_id', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'manufacturer_id', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'developer_data_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'application_version', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 258 => [ 'mesg_name' => 'dive_settings', 'field_defns' => [ 0 => ['field_name' => 'name', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'model', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'gf_low', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 3 => ['field_name' => 'gf_high', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 4 => ['field_name' => 'water_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 5 => ['field_name' => 'water_density', 'scale' => 1, 'offset' => 0, 'units' => 'kg/m^3'], 6 => ['field_name' => 'po2_warn', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 7 => ['field_name' => 'po2_critical', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 8 => ['field_name' => 'po2_deco', 'scale' => 100, 'offset' => 0, 'units' => 'percent'], 9 => ['field_name' => 'safety_stop_enabled', 'scale' => 1, 'offset' => 0, 'units' => ''], 10 => ['field_name' => 'bottom_depth', 'scale' => 1, 'offset' => 0, 'units' => ''], 11 => ['field_name' => 'bottom_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 12 => ['field_name' => 'apnea_countdown_enabled', 'scale' => 1, 'offset' => 0, 'units' => ''], 13 => ['field_name' => 'apnea_countdown_time', 'scale' => 1, 'offset' => 0, 'units' => ''], 14 => ['field_name' => 'backlight_mode', 'scale' => 1, 'offset' => 0, 'units' => ''], 15 => ['field_name' => 'backlight_brightness', 'scale' => 1, 'offset' => 0, 'units' => ''], 16 => ['field_name' => 'backlight_timeout', 'scale' => 1, 'offset' => 0, 'units' => ''], 17 => ['field_name' => 'repeat_dive_interval', 'scale' => 1, 'offset' => 0, 'units' => 's'], 18 => ['field_name' => 'safety_stop_time', 'scale' => 1, 'offset' => 0, 'units' => 's'], 19 => ['field_name' => 'heart_rate_source_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 20 => ['field_name' => 'heart_rate_source', 'scale' => 1, 'offset' => 0, 'units' => ''], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 259 => [ 'mesg_name' => 'dive_gas', 'field_defns' => [ 0 => ['field_name' => 'helium_content', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 1 => ['field_name' => 'oxygen_content', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 2 => ['field_name' => 'status', 'scale' => 1, 'offset' => 0, 'units' => ''], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 262 => [ 'mesg_name' => 'dive_alarm', 'field_defns' => [ 0 => ['field_name' => 'depth', 'scale' => 1000, 'offset' => 0, 'units' => 'm'], 1 => ['field_name' => 'time', 'scale' => 1, 'offset' => 0, 'units' => 's'], 2 => ['field_name' => 'enabled', 'scale' => 1, 'offset' => 0, 'units' => ''], 3 => ['field_name' => 'alarm_type', 'scale' => 1, 'offset' => 0, 'units' => ''], 4 => ['field_name' => 'sound', 'scale' => 1, 'offset' => 0, 'units' => ''], 254 => ['field_name' => 'message_index', 'scale' => 1, 'offset' => 0, 'units' => ''] ] ], 268 => [ 'mesg_name' => 'dive_summary', 'field_defns' => [ 0 => ['field_name' => 'reference_mesg', 'scale' => 1, 'offset' => 0, 'units' => ''], 1 => ['field_name' => 'reference_index', 'scale' => 1, 'offset' => 0, 'units' => ''], 2 => ['field_name' => 'avg_depth', 'scale' => 1000, 'offset' => 0, 'units' => 'm'], 3 => ['field_name' => 'max_depth', 'scale' => 1000, 'offset' => 0, 'units' => 'm'], 4 => ['field_name' => 'surface_interval', 'scale' => 1, 'offset' => 0, 'units' => 's'], 5 => ['field_name' => 'start_cns', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 6 => ['field_name' => 'end_cns', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 7 => ['field_name' => 'start_n2', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 8 => ['field_name' => 'end_n2', 'scale' => 1, 'offset' => 0, 'units' => 'percent'], 9 => ['field_name' => 'o2_toxicity', 'scale' => 1, 'offset' => 0, 'units' => 'OTUs'], 10 => ['field_name' => 'dive_number', 'scale' => 1, 'offset' => 0, 'units' => ''], 11 => ['field_name' => 'bottom_time', 'scale' => 1000, 'offset' => 0, 'units' => 's'], 253 => ['field_name' => 'timestamp', 'scale' => 1, 'offset' => 0, 'units' => 's'] ] ] ]; // PHP Constructor - called when an object of the class is instantiated. public function __construct($file_path_or_data, $options = null) { if( isset( $options['input_is_data'] ) ){ $this->file_contents = $file_path_or_data; }else{ if (empty($file_path_or_data)) { throw new \Exception('phpFITFileAnalysis->__construct(): file_path is empty!'); } if (!file_exists($file_path_or_data)) { throw new \Exception('phpFITFileAnalysis->__construct(): file \''.$file_path_or_data.'\' does not exist!'); } /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 1.7.pdf * 3.3 FIT File Structure * Header . Data Records . CRC */ $this->file_contents = file_get_contents($file_path_or_data); // Read the entire file into a string } $this->options = $options; if (isset($options['garmin_timestamps']) && $options['garmin_timestamps'] == true) { $this->garmin_timestamps = true; } $this->options['overwrite_with_dev_data'] = false; if (isset($this->options['overwrite_with_dev_data']) && $this->options['overwrite_with_dev_data'] == true) { $this->options['overwrite_with_dev_data'] = true; } $this->php_trader_ext_loaded = extension_loaded('trader'); // Process the file contents. $this->readHeader(); $this->readDataRecords(); $this->oneElementArrays(); // Process HR messages $this->processHrMessages(); // Handle options. $this->fixData($this->options); $this->setUnits($this->options); } /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 1.7.pdf * Table 3-1. Byte Description of File Header */ private function readHeader() { $header_size = unpack('C1header_size', substr($this->file_contents, $this->file_pointer, 1))['header_size']; $this->file_pointer++; if ($header_size != 12 && $header_size != 14) { throw new \Exception('phpFITFileAnalysis->readHeader(): not a valid header size!'); } $header_fields = 'C1protocol_version/' . 'v1profile_version/' . 'V1data_size/' . 'C4data_type'; if ($header_size > 12) { $header_fields .= '/v1crc'; } $this->file_header = unpack($header_fields, substr($this->file_contents, $this->file_pointer, $header_size - 1)); $this->file_header['header_size'] = $header_size; $this->file_pointer += $this->file_header['header_size'] - 1; $file_extension = sprintf('%c%c%c%c', $this->file_header['data_type1'], $this->file_header['data_type2'], $this->file_header['data_type3'], $this->file_header['data_type4']); if ($file_extension != '.FIT' || $this->file_header['data_size'] <= 0) { throw new \Exception('phpFITFileAnalysis->readHeader(): not a valid FIT file!'); } if (strlen($this->file_contents) - $header_size - 2 !== $this->file_header['data_size']) { // Overwrite the data_size. Seems to be incorrect if there are buffered messages e.g. HR records. $this->file_header['data_size'] = $this->file_header['crc'] - $header_size + 2; } } /** * Reads the remainder of $this->file_contents and store the data in the $this->data_mesgs array. */ private function readDataRecords() { $record_header_byte = 0; $message_type = 0; $developer_data_flag = 0; $local_mesg_type = 0; $previousTS = 0; while ($this->file_header['header_size'] + $this->file_header['data_size'] > $this->file_pointer) { $record_header_byte = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $compressedTimestamp = false; $tsOffset = 0; /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 2.2.pdf * Table 4-1. Normal Header Bit Field Description */ if (($record_header_byte >> 7) & 1) { // Check that it's a normal header // Header with compressed timestamp $message_type = 0; //always 0: DATA_MESSAGE $developer_data_flag = 0; // always 0: DATA_MESSAGE $local_mesg_type = ($record_header_byte >> 5) & 3; // bindec('0011') == 3 $tsOffset = $record_header_byte & 31; $compressedTimestamp = true; } else { //Normal header $message_type = ($record_header_byte >> 6) & 1; // 1: DEFINITION_MESSAGE; 0: DATA_MESSAGE $developer_data_flag = ($record_header_byte >> 5) & 1; // 1: DEFINITION_MESSAGE; 0: DATA_MESSAGE $local_mesg_type = $record_header_byte & 15; // bindec('1111') == 15 } switch ($message_type) { case DEFINITION_MESSAGE: /** * D00001275 Flexible & Interoperable Data Transfer (FIT) Protocol Rev 1.7.pdf * Table 4-1. Normal Header Bit Field Description */ $this->file_pointer++; // Reserved - IGNORED $architecture = ord(substr($this->file_contents, $this->file_pointer, 1)); // Architecture $this->file_pointer++; $this->types = $this->endianness[$architecture]; $global_mesg_num = ($architecture === 0) ? unpack('v1tmp', substr($this->file_contents, $this->file_pointer, 2))['tmp'] : unpack('n1tmp', substr($this->file_contents, $this->file_pointer, 2))['tmp']; $this->file_pointer += 2; $num_fields = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $field_definitions = []; $total_size = 0; for ($i=0; $i<$num_fields; ++$i) { $field_definition_number = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $size = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $base_type = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $field_definitions[] = ['field_definition_number' => $field_definition_number, 'size' => $size, 'base_type' => $base_type]; $total_size += $size; } $num_dev_fields = 0; $dev_field_definitions = []; if ($developer_data_flag === 1) { $num_dev_fields = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; for ($i=0; $i<$num_dev_fields; ++$i) { $field_definition_number = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $size = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $developer_data_index = ord(substr($this->file_contents, $this->file_pointer, 1)); $this->file_pointer++; $dev_field_definitions[] = ['field_definition_number' => $field_definition_number, 'size' => $size, 'developer_data_index' => $developer_data_index]; $total_size += $size; } } $this->defn_mesgs[$local_mesg_type] = [ 'global_mesg_num' => $global_mesg_num, 'num_fields' => $num_fields, 'field_defns' => $field_definitions, 'num_dev_fields' => $num_dev_fields, 'dev_field_definitions' => $dev_field_definitions, 'total_size' => $total_size ]; $this->defn_mesgs_all[] = [ 'global_mesg_num' => $global_mesg_num, 'num_fields' => $num_fields, 'field_defns' => $field_definitions, 'num_dev_fields' => $num_dev_fields, 'dev_field_definitions' => $dev_field_definitions, 'total_size' => $total_size ]; break; case DATA_MESSAGE: // Check that we have information on the Data Message. if (isset($this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']])) { $tmp_record_array = []; // Temporary array to store Record data message pieces $tmp_value = null; // Placeholder for value for checking before inserting into the tmp_record_array foreach ($this->defn_mesgs[$local_mesg_type]['field_defns'] as $field_defn) { // Check that we have information on the Field Definition and a valid base type exists. if (isset($this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]) && isset($this->types[$field_defn['base_type']])) { // Check if it's an invalid value for the type $tmp_value = unpack($this->types[$field_defn['base_type']]['format'], substr($this->file_contents, $this->file_pointer, $field_defn['size']))['tmp']; if ($tmp_value !== $this->invalid_values[$field_defn['base_type']] || $this->defn_mesgs[$local_mesg_type]['global_mesg_num'] === 132) { // If it's a timestamp, compensate between different in FIT and Unix timestamp epochs if ($field_defn['field_definition_number'] === 253 && !$this->garmin_timestamps) { $tmp_value += FIT_UNIX_TS_DIFF; } // If it's a Record data message, store all the pieces in the temporary array as the timestamp may not be first... if ($this->defn_mesgs[$local_mesg_type]['global_mesg_num'] === 20) { $tmp_record_array[$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['field_name']] = $tmp_value / $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['scale'] - $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['offset']; } elseif ($this->defn_mesgs[$local_mesg_type]['global_mesg_num'] === 206) { // Developer Data $tmp_record_array[$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['field_name']] = $tmp_value; } else { if ($field_defn['base_type'] === 7) { // Handle strings appropriately $this->data_mesgs[$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['mesg_name']][$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['field_name']][] = filter_var($tmp_value, FILTER_SANITIZE_STRING); } else { // Handle arrays if ($field_defn['size'] !== $this->types[$field_defn['base_type']]['bytes']) { $tmp_array = []; $num_vals = $field_defn['size'] / $this->types[$field_defn['base_type']]['bytes']; for ($i=0; $i<$num_vals; ++$i) { $tmp_array[] = unpack($this->types[$field_defn['base_type']]['format'], substr($this->file_contents, $this->file_pointer + ($i * $this->types[$field_defn['base_type']]['bytes']), $field_defn['size']))['tmp']/ $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['scale'] - $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['offset']; } $this->data_mesgs[$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['mesg_name']][$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['field_name']][] = $tmp_array; } else { $this->data_mesgs[$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['mesg_name']][$this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['field_name']][] = $tmp_value / $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['scale'] - $this->data_mesg_info[$this->defn_mesgs[$local_mesg_type]['global_mesg_num']]['field_defns'][$field_defn['field_definition_number']]['offset']; } } } } } $this->file_pointer += $field_defn['size']; } // Handle Developer Data if ($this->defn_mesgs[$local_mesg_type]['global_mesg_num'] === 206) { $developer_data_index = $tmp_record_array['developer_data_index']; $field_definition_number = $tmp_record_array['field_definition_number']; unset($tmp_record_array['developer_data_index']); unset($tmp_record_array['field_definition_number']); if (isset($tmp_record_array['field_name'])) { // Get rid of special/invalid characters after the null terminated string $tmp_record_array['field_name'] = strtolower(implode('', explode("\0", $tmp_record_array['field_name']))); } if (isset($tmp_record_array['units'])) { $tmp_record_array['units'] = strtolower(implode('', explode("\0", $tmp_record_array['units']))); } $this->dev_field_descriptions[$developer_data_index][$field_definition_number] = $tmp_record_array; unset($tmp_record_array); } foreach ($this->defn_mesgs[$local_mesg_type]['dev_field_definitions'] as $field_defn) { // Units $this->data_mesgs['developer_data'][$this->dev_field_descriptions[$field_defn['developer_data_index']][$field_defn['field_definition_number']]['field_name']]['units'] = $this->dev_field_descriptions[$field_defn['developer_data_index']][$field_defn['field_definition_number']]['units']; // Data $this->data_mesgs['developer_data'][$this->dev_field_descriptions[$field_defn['developer_data_index']][$field_defn['field_definition_number']]['field_name']]['data'][] = unpack($this->types[$this->dev_field_descriptions[$field_defn['developer_data_index']][$field_defn['field_definition_number']]['fit_base_type_id']]['format'], substr($this->file_contents, $this->file_pointer, $field_defn['size']))['tmp']; $this->file_pointer += $field_defn['size']; } // Process the temporary array and load values into the public data messages array if (!empty($tmp_record_array)) { $timestamp = isset($this->data_mesgs['record']['timestamp']) ? max($this->data_mesgs['record']['timestamp']) + 1 : 0; if ($compressedTimestamp) { if ($previousTS === 0) { // This should not happen! Throw exception? } else { $previousTS -= FIT_UNIX_TS_DIFF; // back to FIT timestamps epoch $fiveLsb = $previousTS & 0x1F; if ($tsOffset >= $fiveLsb) { // No rollover $timestamp = $previousTS - $fiveLsb + $tsOffset; } else { // Rollover $timestamp = $previousTS - $fiveLsb + $tsOffset + 32; } $timestamp += FIT_UNIX_TS_DIFF; // back to Unix timestamps epoch $previousTS += FIT_UNIX_TS_DIFF; } } else { if (isset($tmp_record_array['timestamp'])) { if ($tmp_record_array['timestamp'] > 0) { $timestamp = $tmp_record_array['timestamp']; $previousTS = $timestamp; } unset($tmp_record_array['timestamp']); } } $this->data_mesgs['record']['timestamp'][] = $timestamp; foreach ($tmp_record_array as $key => $value) { if ($value !== null) { $this->data_mesgs['record'][$key][$timestamp] = $value; } } } } else { $this->file_pointer += $this->defn_mesgs[$local_mesg_type]['total_size']; } } } // Overwrite native FIT fields (e.g. Power, HR, Cadence, etc) with developer data by default if (!empty($this->dev_field_descriptions)) { foreach ($this->dev_field_descriptions as $developer_data_index) { foreach ($developer_data_index as $field_definition_number) { if (isset($field_definition_number['native_field_num'])) { if (isset($this->data_mesgs['record'][$field_definition_number['field_name']]) && !$this->options['overwrite_with_dev_data']) { continue; } $this->data_mesgs['record'][$field_definition_number['field_name']] = $this->data_mesgs['developer_data'][$field_definition_number['field_name']]['data']; } } } } } /** * If the user has requested for the data to be fixed, identify the missing keys for that data. */ private function fixData($options) { // By default the constant FIT_UNIX_TS_DIFF will be added to timestamps, which have field type of date_time (or local_date_time). // Timestamp fields (field number == 253) converted after being unpacked in $this->readDataRecords(). if (!$this->garmin_timestamps) { $date_times = [ ['message_name' => 'activity', 'field_name' => 'local_timestamp'], ['message_name' => 'course_point', 'field_name' => 'timestamp'], ['message_name' => 'file_id', 'field_name' => 'time_created'], ['message_name' => 'goal', 'field_name' => 'end_date'], ['message_name' => 'goal', 'field_name' => 'start_date'], ['message_name' => 'lap', 'field_name' => 'start_time'], ['message_name' => 'length', 'field_name' => 'start_time'], ['message_name' => 'monitoring', 'field_name' => 'local_timestamp'], ['message_name' => 'monitoring_info', 'field_name' => 'local_timestamp'], ['message_name' => 'obdii_data', 'field_name' => 'start_timestamp'], ['message_name' => 'schedule', 'field_name' => 'scheduled_time'], ['message_name' => 'schedule', 'field_name' => 'time_created'], ['message_name' => 'segment_lap', 'field_name' => 'start_time'], ['message_name' => 'session', 'field_name' => 'start_time'], ['message_name' => 'timestamp_correlation', 'field_name' => 'local_timestamp'], ['message_name' => 'timestamp_correlation', 'field_name' => 'system_timestamp'], ['message_name' => 'training_file', 'field_name' => 'time_created'], ['message_name' => 'video_clip', 'field_name' => 'end_timestamp'], ['message_name' => 'video_clip', 'field_name' => 'start_timestamp'] ]; foreach ($date_times as $date_time) { if (isset($this->data_mesgs[$date_time['message_name']][$date_time['field_name']])) { if (is_array($this->data_mesgs[$date_time['message_name']][$date_time['field_name']])) { foreach ($this->data_mesgs[$date_time['message_name']][$date_time['field_name']] as &$element) { $element += FIT_UNIX_TS_DIFF; } } else { $this->data_mesgs[$date_time['message_name']][$date_time['field_name']] += FIT_UNIX_TS_DIFF; } } } } // Find messages that have been unpacked as unsigned integers that should be signed integers. // http://php.net/manual/en/function.pack.php - signed integers endianness is always machine dependent. // 131 s signed short (always 16 bit, machine byte order) // 133 l signed long (always 32 bit, machine byte order) // 142 q signed long long (always 64 bit, machine byte order) foreach ($this->defn_mesgs_all as $mesg) { if (isset($this->data_mesg_info[$mesg['global_mesg_num']])) { $mesg_name = $this->data_mesg_info[$mesg['global_mesg_num']]['mesg_name']; foreach ($mesg['field_defns'] as $field) { // Convert uint16 to sint16 if ($field['base_type'] === 131 && isset($this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name'])) { $field_name = $this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name']; if (isset($this->data_mesgs[$mesg_name][$field_name])) { if (is_array($this->data_mesgs[$mesg_name][$field_name])) { foreach ($this->data_mesgs[$mesg_name][$field_name] as &$v) { if (PHP_INT_SIZE === 8 && $v > 0x7FFF) { $v -= 0x10000; } if ($v > 0x7FFF) { $v = -1 * ($v - 0x7FFF); } } } elseif ($this->data_mesgs[$mesg_name][$field_name] > 0x7FFF) { if (PHP_INT_SIZE === 8) { $this->data_mesgs[$mesg_name][$field_name] -= 0x10000; } $this->data_mesgs[$mesg_name][$field_name] = -1 * ($this->data_mesgs[$mesg_name][$field_name] - 0x7FFF); } } } // Convert uint32 to sint32 elseif ($field['base_type'] === 133 && isset($this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name'])) { $field_name = $this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name']; if (isset($this->data_mesgs[$mesg_name][$field_name])) { if (is_array($this->data_mesgs[$mesg_name][$field_name])) { foreach ($this->data_mesgs[$mesg_name][$field_name] as &$v) { if (PHP_INT_SIZE === 8 && $v > 0x7FFFFFFF) { $v -= 0x100000000; } if ($v > 0x7FFFFFFF) { $v = -1 * ($v - 0x7FFFFFFF); } } } elseif ($this->data_mesgs[$mesg_name][$field_name] > 0x7FFFFFFF) { if (PHP_INT_SIZE === 8) { $this->data_mesgs[$mesg_name][$field_name] -= 0x100000000; } if( $this->data_mesgs[$mesg_name][$field_name] > 0x7FFFFFFF ){ $this->data_mesgs[$mesg_name][$field_name] = -1 * ($this->data_mesgs[$mesg_name][$field_name] - 0x7FFFFFFF); } } } } // Convert uint64 to sint64 elseif ($field['base_type'] === 142 && isset($this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name'])) { $field_name = $this->data_mesg_info[$mesg['global_mesg_num']]['field_defns'][$field['field_definition_number']]['field_name']; if (isset($this->data_mesgs[$mesg_name][$field_name])) { if (is_array($this->data_mesgs[$mesg_name][$field_name])) { foreach ($this->data_mesgs[$mesg_name][$field_name] as &$v) { if (PHP_INT_SIZE === 8 && $v > 0x7FFFFFFFFFFFFFFF) { $v -= 0x10000000000000000; } if ($v > 0x7FFFFFFFFFFFFFFF) { $v = -1 * ($v - 0x7FFFFFFFFFFFFFFF); } } } elseif ($this->data_mesgs[$mesg_name][$field_name] > 0x7FFFFFFFFFFFFFFF) { if (PHP_INT_SIZE === 8) { $this->data_mesgs[$mesg_name][$field_name] -= 0x10000000000000000; } $this->data_mesgs[$mesg_name][$field_name] = -1 * ($this->data_mesgs[$mesg_name][$field_name] - 0x7FFFFFFFFFFFFFFF); } } } } } } // Remove duplicate timestamps if (isset($this->data_mesgs['record']['timestamp']) && is_array($this->data_mesgs['record']['timestamp'])) { $this->data_mesgs['record']['timestamp'] = array_unique($this->data_mesgs['record']['timestamp']); } // Return if no option set if (empty($options['fix_data']) && empty($options['data_every_second'])) { return; } // If $options['data_every_second'], then create timestamp array for every second from min to max if (!empty($options['data_every_second']) && !(is_string($options['data_every_second']) && strtolower($options['data_every_second']) === 'false')) { // If user has not specified the data to be fixed, assume all if (empty($options['fix_data'])) { $options['fix_data'] = ['all']; } $min_ts = min($this->data_mesgs['record']['timestamp']); $max_ts = max($this->data_mesgs['record']['timestamp']); unset($this->data_mesgs['record']['timestamp']); for ($i=$min_ts; $i<=$max_ts; ++$i) { $this->data_mesgs['record']['timestamp'][] = $i; } } // Check if valid option(s) provided array_walk($options['fix_data'], function (&$value) { $value = strtolower($value); }); // Make all lower-case. if (count(array_intersect(['all', 'cadence', 'distance', 'heart_rate', 'lat_lon', 'speed', 'power'], $options['fix_data'])) === 0) { throw new \Exception('phpFITFileAnalysis->fixData(): option not valid!'); } $bCadence = $bDistance = $bHeartRate = $bLatitudeLongitude = $bSpeed = $bPower = false; if (in_array('all', $options['fix_data'])) { $bCadence = isset($this->data_mesgs['record']['cadence']); $bDistance = isset($this->data_mesgs['record']['distance']); $bHeartRate = isset($this->data_mesgs['record']['heart_rate']); $bLatitudeLongitude = isset($this->data_mesgs['record']['position_lat']) && isset($this->data_mesgs['record']['position_long']); $bSpeed = isset($this->data_mesgs['record']['speed']); $bPower = isset($this->data_mesgs['record']['power']); } else { if (isset($this->data_mesgs['record']['timestamp'])) { $count_timestamp = count($this->data_mesgs['record']['timestamp']); // No point try to insert missing values if we know there aren't any. if (isset($this->data_mesgs['record']['cadence'])) { $bCadence = (count($this->data_mesgs['record']['cadence']) === $count_timestamp) ? false : in_array('cadence', $options['fix_data']); } if (isset($this->data_mesgs['record']['distance'])) { $bDistance = (count($this->data_mesgs['record']['distance']) === $count_timestamp) ? false : in_array('distance', $options['fix_data']); } if (isset($this->data_mesgs['record']['heart_rate'])) { $bHeartRate = (count($this->data_mesgs['record']['heart_rate']) === $count_timestamp) ? false : in_array('heart_rate', $options['fix_data']); } if (isset($this->data_mesgs['record']['position_lat']) && isset($this->data_mesgs['record']['position_long'])) { $bLatitudeLongitude = (count($this->data_mesgs['record']['position_lat']) === $count_timestamp && count($this->data_mesgs['record']['position_long']) === $count_timestamp) ? false : in_array('lat_lon', $options['fix_data']); } if (isset($this->data_mesgs['record']['speed'])) { $bSpeed = (count($this->data_mesgs['record']['speed']) === $count_timestamp) ? false : in_array('speed', $options['fix_data']); } if (isset($this->data_mesgs['record']['power'])) { $bPower = (count($this->data_mesgs['record']['power']) === $count_timestamp) ? false : in_array('power', $options['fix_data']); } } } $missing_distance_keys = []; $missing_hr_keys = []; $missing_lat_keys = []; $missing_lon_keys = []; $missing_speed_keys = []; $missing_power_keys = []; foreach ($this->data_mesgs['record']['timestamp'] as $timestamp) { if ($bCadence) { // Assumes all missing cadence values are zeros if (!isset($this->data_mesgs['record']['cadence'][$timestamp])) { $this->data_mesgs['record']['cadence'][$timestamp] = 0; } } if ($bDistance) { if (!isset($this->data_mesgs['record']['distance'][$timestamp])) { $missing_distance_keys[] = $timestamp; } } if ($bHeartRate) { if (!isset($this->data_mesgs['record']['heart_rate'][$timestamp])) { $missing_hr_keys[] = $timestamp; } } if ($bLatitudeLongitude) { if (!isset($this->data_mesgs['record']['position_lat'][$timestamp])) { $missing_lat_keys[] = $timestamp; } if (!isset($this->data_mesgs['record']['position_long'][$timestamp])) { $missing_lon_keys[] = $timestamp; } } if ($bSpeed) { if (!isset($this->data_mesgs['record']['speed'][$timestamp])) { $missing_speed_keys[] = $timestamp; } } if ($bPower) { if (!isset($this->data_mesgs['record']['power'][$timestamp])) { $missing_power_keys[] = $timestamp; } } } if ($bCadence) { ksort($this->data_mesgs['record']['cadence']); // no interpolation; zeros added earlier } if ($bDistance) { $this->interpolateMissingData($missing_distance_keys, $this->data_mesgs['record']['distance']); } if ($bHeartRate) { $this->interpolateMissingData($missing_hr_keys, $this->data_mesgs['record']['heart_rate']); } if ($bLatitudeLongitude) { $this->interpolateMissingData($missing_lat_keys, $this->data_mesgs['record']['position_lat']); $this->interpolateMissingData($missing_lon_keys, $this->data_mesgs['record']['position_long']); } if ($bSpeed) { $this->interpolateMissingData($missing_speed_keys, $this->data_mesgs['record']['speed']); } if ($bPower) { $this->interpolateMissingData($missing_power_keys, $this->data_mesgs['record']['power']); } } /** * For the missing keys in the data, interpolate using values either side and insert as necessary. */ private function interpolateMissingData(&$missing_keys, &$array) { if (!is_array($array)) { return; // Can't interpolate if not an array } $num_points = 2; $min_key = min(array_keys($array)); $max_key = max(array_keys($array)); $count = count($missing_keys); for ($i=0; $i<$count; ++$i) { if ($missing_keys[$i] !== 0) { // Interpolating outside recorded range is impossible - use edge values instead if ($missing_keys[$i] > $max_key) { $array[$missing_keys[$i]] = $array[$max_key]; continue; } elseif ($missing_keys[$i] < $min_key) { $array[$missing_keys[$i]] = $array[$min_key]; continue; } $prev_value = $next_value = reset($array); while ($missing_keys[$i] > key($array)) { $prev_value = current($array); $next_value = next($array); } for ($j=$i+1; $j<$count; ++$j) { if ($missing_keys[$j] < key($array)) { $num_points++; } else { break; } } $gap = ($next_value - $prev_value) / $num_points; for ($k=0; $k<=$num_points-2; ++$k) { $array[$missing_keys[$i+$k]] = $prev_value + ($gap * ($k+1)); } for ($k=0; $k<=$num_points-2; ++$k) { $missing_keys[$i+$k] = 0; } $num_points = 2; } } ksort($array); // sort using keys } /** * Change arrays that contain only one element into non-arrays so you can use $variable rather than $variable[0] to access. */ private function oneElementArrays() { foreach ($this->data_mesgs as $mesg_key => $mesg) { if ($mesg_key === 'developer_data') { continue; } foreach ($mesg as $field_key => $field) { if (count($field) === 1) { $first_key = key($field); $this->data_mesgs[$mesg_key][$field_key] = $field[$first_key]; } } } } /** * The FIT protocol makes use of enumerated data types. * Where these values have been identified in the FIT SDK, they have been included in $this->enum_data * This function returns the enumerated value for a given message type. */ public function enumData($type, $value) { if (is_array($value)) { $tmp = []; foreach ($value as $element) { if (isset($this->enum_data[$type][$element])) { $tmp[] = $this->enum_data[$type][$element]; } else { $tmp[] = 'unknown'; } } return $tmp; } else { return isset($this->enum_data[$type][$value]) ? $this->enum_data[$type][$value] : 'unknown'; } } /** * Short-hand access to commonly used enumerated data. */ public function manufacturer() { $tmp = $this->enumData('manufacturer', $this->data_mesgs['device_info']['manufacturer']); return is_array($tmp) ? $tmp[0] : $tmp; } public function product() { $tmp = $this->enumData('product', $this->data_mesgs['device_info']['product']); return is_array($tmp) ? $tmp[0] : $tmp; } public function sport() { $tmp = $this->enumData('sport', $this->data_mesgs['session']['sport']); return is_array($tmp) ? $tmp[0] : $tmp; } /** * Transform the values read from the FIT file into the units requested by the user. */ private function setUnits($options) { if (!empty($options['units'])) { // Handle $options['units'] not being passed as array and/or not in lowercase. $units = strtolower((is_array($options['units'])) ? $options['units'][0] : $options['units']); } else { $units = 'metric'; } // Handle $options['pace'] being pass as array and/or boolean vs string and/or lowercase. $bPace = false; if (isset($options['pace'])) { $pace = is_array($options['pace']) ? $options['pace'][0] : $options['pace']; if (is_bool($pace)) { $bPace = $pace; } elseif (is_string($pace)) { $pace = strtolower($pace); if ($pace === 'true' || $pace === 'false') { $bPace = $pace; } else { throw new \Exception('phpFITFileAnalysis->setUnits(): pace option not valid!'); } } else { throw new \Exception('phpFITFileAnalysis->setUnits(): pace option not valid!'); } } // Set units for all messages $messages = ['session', 'lap', 'record', 'segment_lap']; $c_fields = [ 'avg_temperature', 'max_temperature', 'temperature' ]; $m_fields = [ 'distance', 'total_distance' ]; $m_ft_fields = [ 'altitude', 'avg_altitude', 'enhanced_avg_altitude', 'enhanced_max_altitude', 'enhanced_min_altitude', 'max_altitude', 'min_altitude', 'total_ascent', 'total_descent' ]; $ms_fields = [ 'avg_neg_vertical_speed', 'avg_pos_vertical_speed', 'avg_speed', 'enhanced_avg_speed', 'enhanced_max_speed', 'enhanced_speed', 'max_neg_vertical_speed', 'max_pos_vertical_speed', 'max_speed', 'speed' ]; $semi_fields = [ 'end_position_lat', 'end_position_long', 'nec_lat', 'nec_long', 'position_lat', 'position_long', 'start_position_lat', 'start_position_long', 'swc_lat', 'swc_long' ]; foreach ($messages as $message) { switch ($units) { case 'statute': // convert from celsius to fahrenheit foreach ($c_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round((($value * 9) / 5) + 32, 2); } } else { $this->data_mesgs[$message][$field] = round((($this->data_mesgs[$message][$field] * 9) / 5) + 32, 2); } } } // convert from meters to miles foreach ($m_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round($value * 0.000621371192, 2); } } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * 0.000621371192, 2); } } } // convert from meters to feet foreach ($m_ft_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round($value * 3.2808399, 1); } } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * 3.2808399, 1); } } } // convert meters per second to miles per hour foreach ($ms_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { if ($bPace) { $value = round(60 / 2.23693629 / $value, 3); } else { $value = round($value * 2.23693629, 3); } } } else { if ($bPace) { $this->data_mesgs[$message][$field] = round(60 / 2.23693629 / $this->data_mesgs[$message][$field], 3); } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * 2.23693629, 3); } } } } // convert from semicircles to degress foreach ($semi_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round($value * (180.0 / pow(2, 31)), 5); } } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * (180.0 / pow(2, 31)), 5); } } } break; case 'raw': // Do nothing - leave values as read from file. break; case 'metric': // convert from meters to kilometers foreach ($m_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round($value * 0.001, 2); } } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * 0.001, 2); } } } // convert meters per second to kilometers per hour foreach ($ms_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { if ($bPace) { $value = ($value != 0) ? round(60 / 3.6 / $value, 3) : 0; } else { $value = round($value * 3.6, 3); } } } else { if ($this->data_mesgs[$message][$field] === 0) { // Prevent divide by zero error continue; } if ($bPace) { $this->data_mesgs[$message][$field] = round(60 / 3.6 / $this->data_mesgs[$message][$field], 3); } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * 3.6, 3); } } } } // convert from semicircles to degress foreach ($semi_fields as $field) { if (isset($this->data_mesgs[$message][$field])) { if (is_array($this->data_mesgs[$message][$field])) { foreach ($this->data_mesgs[$message][$field] as &$value) { $value = round($value * (180.0 / pow(2, 31)), 5); } } else { $this->data_mesgs[$message][$field] = round($this->data_mesgs[$message][$field] * (180.0 / pow(2, 31)), 5); } } } break; default: throw new \Exception('phpFITFileAnalysis->setUnits(): units option not valid!'); break; } } } /** * Calculate HR zones using HRmax formula: zone = HRmax * percentage. */ public function hrZonesMax($hr_maximum, $percentages_array = [0.60, 0.75, 0.85, 0.95]) { if (array_walk($percentages_array, function (&$value, $key, $hr_maximum) { $value = round($value * $hr_maximum); }, $hr_maximum)) { return $percentages_array; } else { throw new \Exception('phpFITFileAnalysis->hrZonesMax(): cannot calculate zones, please check inputs!'); } } /** * Calculate HR zones using HRreserve formula: zone = HRresting + ((HRmax - HRresting) * percentage). */ public function hrZonesReserve($hr_resting, $hr_maximum, $percentages_array = [0.60, 0.65, 0.75, 0.82, 0.89, 0.94 ]) { if (array_walk($percentages_array, function (&$value, $key, $params) { $value = round($params[0] + ($value * $params[1])); }, [$hr_resting, $hr_maximum - $hr_resting])) { return $percentages_array; } else { throw new \Exception('phpFITFileAnalysis->hrZonesReserve(): cannot calculate zones, please check inputs!'); } } /** * Calculate power zones using Functional Threshold Power value: zone = FTP * percentage. */ public function powerZones($functional_threshold_power, $percentages_array = [0.55, 0.75, 0.90, 1.05, 1.20, 1.50]) { if (array_walk($percentages_array, function (&$value, $key, $functional_threshold_power) { $value = round($value * $functional_threshold_power) + 1; }, $functional_threshold_power)) { return $percentages_array; } else { throw new \Exception('phpFITFileAnalysis->powerZones(): cannot calculate zones, please check inputs!'); } } /** * Partition the data (e.g. cadence, heart_rate, power, speed) using thresholds provided as an array. */ public function partitionData($record_field = '', $thresholds = null, $percentages = true, $labels_for_keys = true) { if (!isset($this->data_mesgs['record'][$record_field])) { throw new \Exception('phpFITFileAnalysis->partitionData(): '.$record_field.' data not present in FIT file!'); } if (!is_array($thresholds)) { throw new \Exception('phpFITFileAnalysis->partitionData(): thresholds must be an array e.g. [10,20,30,40,50]!'); } foreach ($thresholds as $threshold) { if (!is_numeric($threshold) || $threshold < 0) { throw new \Exception('phpFITFileAnalysis->partitionData(): '.$threshold.' not valid in thresholds!'); } if (isset($last_threshold) && $last_threshold >= $threshold) { throw new \Exception('phpFITFileAnalysis->partitionData(): error near ..., '.$last_threshold.', '.$threshold.', ... - each element in thresholds array must be greater than previous element!'); } $last_threshold = $threshold; } $result = array_fill(0, count($thresholds)+1, 0); foreach ($this->data_mesgs['record'][$record_field] as $value) { $key = 0; $count = count($thresholds); for ($key; $key<$count; ++$key) { if ($value < $thresholds[$key]) { break; } } $result[$key]++; } array_unshift($thresholds, 0); $keys = []; if ($labels_for_keys === true) { $count = count($thresholds); for ($i=0; $i<$count; ++$i) { $keys[] = $thresholds[$i] . (isset($thresholds[$i+1]) ? '-'.($thresholds[$i+1] - 1) : '+'); } $result = array_combine($keys, $result); } if ($percentages === true) { $total = array_sum($result); array_walk($result, function (&$value, $key, $total) { $value = round($value / $total * 100, 1); }, $total); } return $result; } /** * Split data into buckets/bins using a Counting Sort algorithm (http://en.wikipedia.org/wiki/Counting_sort) to generate data for a histogram plot. */ public function histogram($bucket_width = 25, $record_field = '') { if (!isset($this->data_mesgs['record'][$record_field])) { throw new \Exception('phpFITFileAnalysis->histogram(): '.$record_field.' data not present in FIT file!'); } if (!is_numeric($bucket_width) || $bucket_width <= 0) { throw new \Exception('phpFITFileAnalysis->histogram(): bucket width is not valid!'); } foreach ($this->data_mesgs['record'][$record_field] as $value) { $key = round($value / $bucket_width) * $bucket_width; isset($result[$key]) ? $result[$key]++ : $result[$key] = 1; } for ($i=0; $ipartitionData('heart_rate', $this->hrZonesMax($hr_maximum)); } public function hrPartionedHRreserve($hr_resting, $hr_maximum) { return $this->partitionData('heart_rate', $this->hrZonesReserve($hr_resting, $hr_maximum)); } public function powerPartioned($functional_threshold_power) { return $this->partitionData('power', $this->powerZones($functional_threshold_power)); } public function powerHistogram($bucket_width = 25) { return $this->histogram($bucket_width, 'power'); } /** * Simple moving average algorithm */ private function sma($array, $time_period) { $sma_data = []; $data = array_values($array); $count = count($array); for ($i=0; $i<$count-$time_period; ++$i) { $sma_data[] = array_sum(array_slice($data, $i, $time_period)) / $time_period; } return $sma_data; } /** * Calculate TRIMP (TRaining IMPulse) and an Intensity Factor using HR data. Useful if power data not available. * hr_FT is heart rate at Functional Threshold, or Lactate Threshold Heart Rate (LTHR) */ public function hrMetrics($hr_resting, $hr_maximum, $hr_FT, $gender) { $hr_metrics = [ // array to hold HR analysis data 'TRIMPexp' => 0.0, 'hrIF' => 0.0, ]; if (in_array($gender, ['F', 'f', 'Female', 'female'])) { $gender_coeff = 1.67; } else { $gender_coeff = 1.92; } foreach ($this->data_mesgs['record']['heart_rate'] as $hr) { // TRIMPexp formula from http://fellrnr.com/wiki/TRIMP // TRIMPexp = sum(D x HRr x 0.64ey) $temp_heart_rate = ($hr - $hr_resting) / ($hr_maximum - $hr_resting); $hr_metrics['TRIMPexp'] += ((1/60) * $temp_heart_rate * 0.64 * (exp($gender_coeff * $temp_heart_rate))); } $hr_metrics['TRIMPexp'] = round($hr_metrics['TRIMPexp']); $hr_metrics['hrIF'] = round((array_sum($this->data_mesgs['record']['heart_rate'])/(count($this->data_mesgs['record']['heart_rate']))) / $hr_FT, 2); return $hr_metrics; } /** * Returns 'Average Power', 'Kilojoules', 'Normalised Power', 'Variability Index', 'Intensity Factor', and 'Training Stress Score' in an array. * * Normalised Power (and metrics dependent on it) require the PHP trader extension to be loaded * http://php.net/manual/en/book.trader.php */ public function powerMetrics($functional_threshold_power) { if (!isset($this->data_mesgs['record']['power'])) { throw new \Exception('phpFITFileAnalysis->powerMetrics(): power data not present in FIT file!'); } $power_metrics['Average Power'] = array_sum($this->data_mesgs['record']['power']) / count($this->data_mesgs['record']['power']); $power_metrics['Kilojoules'] = ($power_metrics['Average Power'] * count($this->data_mesgs['record']['power'])) / 1000; // NP1 capture all values for rolling 30s averages $NP_values = ($this->php_trader_ext_loaded) ? trader_sma($this->data_mesgs['record']['power'], 30) : $this->sma($this->data_mesgs['record']['power'], 30); $NormalisedPower = 0.0; foreach ($NP_values as $value) { // NP2 Raise all the values obtained in step NP1 to the fourth power $NormalisedPower += pow($value, 4); } $NormalisedPower /= count($NP_values); // NP3 Find the average of the values in NP2 $power_metrics['Normalised Power'] = pow($NormalisedPower, 1/4); // NP4 taking the fourth root of the value obtained in step NP3 $power_metrics['Variability Index'] = $power_metrics['Normalised Power'] / $power_metrics['Average Power']; $power_metrics['Intensity Factor'] = $power_metrics['Normalised Power'] / $functional_threshold_power; $power_metrics['Training Stress Score'] = (count($this->data_mesgs['record']['power']) * $power_metrics['Normalised Power'] * $power_metrics['Intensity Factor']) / ($functional_threshold_power * 36); // Round the values to make them something sensible. $power_metrics['Average Power'] = (int)round($power_metrics['Average Power']); $power_metrics['Kilojoules'] = (int)round($power_metrics['Kilojoules']); $power_metrics['Normalised Power'] = (int)round($power_metrics['Normalised Power']); $power_metrics['Variability Index'] = round($power_metrics['Variability Index'], 2); $power_metrics['Intensity Factor'] = round($power_metrics['Intensity Factor'], 2); $power_metrics['Training Stress Score'] = (int)round($power_metrics['Training Stress Score']); return $power_metrics; } /** * Returns Critical Power (Best Efforts) values for supplied time period(s). */ public function criticalPower($time_periods) { if (!isset($this->data_mesgs['record']['power'])) { throw new \Exception('phpFITFileAnalysis->criticalPower(): power data not present in FIT file!'); } if (is_array($time_periods)) { $count = count($this->data_mesgs['record']['power']); foreach ($time_periods as $time_period) { if (!is_numeric($time_period)) { throw new \Exception('phpFITFileAnalysis->criticalPower(): time periods must only contain numeric data!'); } if ($time_period < 0) { throw new \Exception('phpFITFileAnalysis->criticalPower(): time periods cannot be negative!'); } if ($time_period > $count) { break; } $averages = ($this->php_trader_ext_loaded) ? trader_sma($this->data_mesgs['record']['power'], $time_period) : $this->sma($this->data_mesgs['record']['power'], $time_period); if ($averages !== false) { $criticalPower_values[$time_period] = max($averages); } } return $criticalPower_values; } elseif (is_numeric($time_periods) && $time_periods > 0) { if ($time_periods > count($this->data_mesgs['record']['power'])) { $criticalPower_values[$time_periods] = 0; } else { $averages = ($this->php_trader_ext_loaded) ? trader_sma($this->data_mesgs['record']['power'], $time_periods) : $this->sma($this->data_mesgs['record']['power'], $time_periods); if ($averages !== false) { $criticalPower_values[$time_periods] = max($averages); } } return $criticalPower_values; } else { throw new \Exception('phpFITFileAnalysis->criticalPower(): time periods not valid!'); } } /** * Returns array of booleans using timestamp as key. * true == timer paused (e.g. autopause) */ public function isPaused() { /** * Event enumerated values of interest * 0 = timer */ $tek = array_keys($this->data_mesgs['event']['event'], 0); // timer event keys $timer_start = []; $timer_stop = []; foreach ($tek as $v) { if ($this->data_mesgs['event']['event_type'][$v] === 0) { $timer_start[$v] = $this->data_mesgs['event']['timestamp'][$v]; } elseif ($this->data_mesgs['event']['event_type'][$v] === 4) { $timer_stop[$v] = $this->data_mesgs['event']['timestamp'][$v]; } } $first_ts = min($this->data_mesgs['record']['timestamp']); // first timestamp $last_ts = max($this->data_mesgs['record']['timestamp']); // last timestamp reset($timer_start); $cur_start = next($timer_start); $cur_stop = reset($timer_stop); $is_paused = []; $bPaused = false; for ($i = $first_ts; $i < $last_ts; ++$i) { if ($i == $cur_stop) { $bPaused = true; $cur_stop = next($timer_stop); } elseif ($i == $cur_start) { $bPaused = false; $cur_start = next($timer_start); } $is_paused[$i] = $bPaused; } $is_paused[$last_ts] = end($this->data_mesgs['record']['speed']) == 0 ? true : false; return $is_paused; } /** * Returns an array that can be used to plot Circumferential Pedal Velocity (x-axis) vs Average Effective Pedal Force (y-axis). * NB Crank length is in metres. */ public function quadrantAnalysis($crank_length, $ftp, $selected_cadence = 90, $use_timestamps = false) { if ($crank_length === null || $ftp === null) { return []; } if (empty($this->data_mesgs['record']['power']) || empty($this->data_mesgs['record']['cadence'])) { return []; } $quadrant_plot = []; $quadrant_plot['selected_cadence'] = $selected_cadence; $quadrant_plot['aepf_threshold'] = round(($ftp * 60) / ($selected_cadence * 2 * pi() * $crank_length), 3); $quadrant_plot['cpv_threshold'] = round(($selected_cadence * $crank_length * 2 * pi()) / 60, 3); // Used to calculate percentage of points in each quadrant $quad_percent = ['hf_hv' => 0, 'hf_lv' => 0, 'lf_lv' => 0, 'lf_hv' => 0]; // Filter zeroes from cadence array (otherwise !div/0 error for AEPF) $cadence = array_filter($this->data_mesgs['record']['cadence']); $cpv = $aepf = 0.0; foreach ($cadence as $k => $c) { $p = isset($this->data_mesgs['record']['power'][$k]) ? $this->data_mesgs['record']['power'][$k] : 0; // Circumferential Pedal Velocity (CPV, m/s) = (Cadence × Crank Length × 2 × Pi) / 60 $cpv = round(($c * $crank_length * 2 * pi()) / 60, 3); // Average Effective Pedal Force (AEPF, N) = (Power × 60) / (Cadence × 2 × Pi × Crank Length) $aepf = round(($p * 60) / ($c * 2 * pi() * $crank_length), 3); if ($use_timestamps === true) { $quadrant_plot['plot'][$k] = [$cpv, $aepf]; } else { $quadrant_plot['plot'][] = [$cpv, $aepf]; } if ($aepf > $quadrant_plot['aepf_threshold']) { // high force if ($cpv > $quadrant_plot['cpv_threshold']) { // high velocity $quad_percent['hf_hv']++; } else { $quad_percent['hf_lv']++; } } else { // low force if ($cpv > $quadrant_plot['cpv_threshold']) { // high velocity $quad_percent['lf_hv']++; } else { $quad_percent['lf_lv']++; } } } // Convert to percentages and add to array that will be returned by the function $sum = array_sum($quad_percent); foreach ($quad_percent as $k => $v) { $quad_percent[$k] = round($v / $sum * 100, 2); } $quadrant_plot['quad_percent'] = $quad_percent; // Calculate CPV and AEPF for cadences between 20 and 150rpm at and near to FTP for ($c = 20; $c <= 150; $c += 5) { $cpv = round((($c * $crank_length * 2 * pi()) / 60), 3); $quadrant_plot['ftp-25w'][] = [$cpv, round((($ftp - 25) * 60) / ($c * 2 * pi() * $crank_length), 3)]; $quadrant_plot['ftp'][] = [$cpv, round(($ftp * 60) / ($c * 2 * pi() * $crank_length), 3)]; $quadrant_plot['ftp+25w'][] = [$cpv, round((($ftp + 25) * 60) / ($c * 2 * pi() * $crank_length), 3)]; } return $quadrant_plot; } /** * Returns array of gear change information. */ public function gearChanges($bIgnoreTimerPaused = true) { /** * Event enumerated values of interest * 42 = front_gear_change * 43 = rear_gear_change */ $fgcek = array_keys($this->data_mesgs['event']['event'], 42); // front gear change event keys $rgcek = array_keys($this->data_mesgs['event']['event'], 43); // rear gear change event keys /** * gear_change_data (uint32) * components: * rear_gear_num 00000000 00000000 00000000 11111111 * rear_gear 00000000 00000000 11111111 00000000 * front_gear_num 00000000 11111111 00000000 00000000 * front_gear 11111111 00000000 00000000 00000000 * scale: 1, 1, 1, 1 * bits: 8, 8, 8, 8 */ $fgc = []; // front gear components $front_gears = []; foreach ($fgcek as $k) { $fgc_tmp = [ 'timestamp' => $this->data_mesgs['event']['timestamp'][$k], // 'data' => $this->data_mesgs['event']['data'][$k], // 'event_type' => $this->data_mesgs['event']['event_type'][$k], // 'event_group' => $this->data_mesgs['event']['event_group'][$k], 'rear_gear_num' => $this->data_mesgs['event']['data'][$k] & 255, 'rear_gear' => ($this->data_mesgs['event']['data'][$k] >> 8) & 255, 'front_gear_num' => ($this->data_mesgs['event']['data'][$k] >> 16) & 255, 'front_gear' => ($this->data_mesgs['event']['data'][$k] >> 24) & 255 ]; $fgc[] = $fgc_tmp; if (!array_key_exists($fgc_tmp['front_gear_num'], $front_gears)) { $front_gears[$fgc_tmp['front_gear_num']] = $fgc_tmp['front_gear']; } } ksort($front_gears); $rgc = []; // rear gear components $rear_gears = []; foreach ($rgcek as $k) { $rgc_tmp = [ 'timestamp' => $this->data_mesgs['event']['timestamp'][$k], // 'data' => $this->data_mesgs['event']['data'][$k], // 'event_type' => $this->data_mesgs['event']['event_type'][$k], // 'event_group' => $this->data_mesgs['event']['event_group'][$k], 'rear_gear_num' => $this->data_mesgs['event']['data'][$k] & 255, 'rear_gear' => ($this->data_mesgs['event']['data'][$k] >> 8) & 255, 'front_gear_num' => ($this->data_mesgs['event']['data'][$k] >> 16) & 255, 'front_gear' => ($this->data_mesgs['event']['data'][$k] >> 24) & 255 ]; $rgc[] = $rgc_tmp; if (!array_key_exists($rgc_tmp['rear_gear_num'], $rear_gears)) { $rear_gears[$rgc_tmp['rear_gear_num']] = $rgc_tmp['rear_gear']; } } ksort($rear_gears); $timestamps = $this->data_mesgs['record']['timestamp']; $first_ts = min($timestamps); // first timestamp $last_ts = max($timestamps); // last timestamp $fg = 0; // front gear at start of ride $rg = 0; // rear gear at start of ride if (isset($fgc[0]['timestamp'])) { if ($first_ts == $fgc[0]['timestamp']) { $fg = $fgc[0]['front_gear']; } else { $fg = $fgc[0]['front_gear_num'] == 1 ? $front_gears[2] : $front_gears[1]; } } if (isset($rgc[0]['timestamp'])) { if ($first_ts == $rgc[0]['timestamp']) { $rg = $rgc[0]['rear_gear']; } else { $rg = $rgc[0]['rear_gear_num'] == min($rear_gears) ? $rear_gears[$rgc[0]['rear_gear_num'] + 1] : $rear_gears[$rgc[0]['rear_gear_num'] - 1]; } } $fg_summary = []; $rg_summary = []; $combined = []; $gears_array = []; if ($bIgnoreTimerPaused === true) { $is_paused = $this->isPaused(); } reset($fgc); reset($rgc); for ($i = $first_ts; $i < $last_ts; ++$i) { if ($bIgnoreTimerPaused === true && $is_paused[$i] === true) { continue; } $fgc_tmp = current($fgc); $rgc_tmp = current($rgc); if ($i > $fgc_tmp['timestamp']) { if (next($fgc) !== false) { $fg = $fgc_tmp['front_gear']; } } $fg_summary[$fg] = isset($fg_summary[$fg]) ? $fg_summary[$fg] + 1 : 1; if ($i > $rgc_tmp['timestamp']) { if (next($rgc) !== false) { $rg = $rgc_tmp['rear_gear']; } } $rg_summary[$rg] = isset($rg_summary[$rg]) ? $rg_summary[$rg] + 1 : 1; $combined[$fg][$rg] = isset($combined[$fg][$rg]) ? $combined[$fg][$rg] + 1 : 1; $gears_array[$i] = ['front_gear' => $fg, 'rear_gear' => $rg]; } krsort($fg_summary); krsort($rg_summary); krsort($combined); $output = ['front_gear_summary' => $fg_summary, 'rear_gear_summary' => $rg_summary, 'combined_summary' => $combined, 'gears_array' => $gears_array]; return $output; } /** * Create a JSON object that contains available record message information and CPV/AEPF if requested/available. */ public function getJSON($crank_length = null, $ftp = null, $data_required = ['all'], $selected_cadence = 90) { if (!is_array($data_required)) { $data_required = [$data_required]; } foreach ($data_required as &$datum) { $datum = strtolower($datum); } $all = in_array('all', $data_required); $timestamp = ($all || in_array('timestamp', $data_required)); $paused = ($all || in_array('paused', $data_required)); $temperature = ($all || in_array('temperature', $data_required)); $lap = ($all || in_array('lap', $data_required)); $position_lat = ($all || in_array('position_lat', $data_required)); $position_long = ($all || in_array('position_long', $data_required)); $distance = ($all || in_array('distance', $data_required)); $altitude = ($all || in_array('altitude', $data_required)); $speed = ($all || in_array('speed', $data_required)); $heart_rate = ($all || in_array('heart_rate', $data_required)); $cadence = ($all || in_array('cadence', $data_required)); $power = ($all || in_array('power', $data_required)); $quadrant_analysis = ($all || in_array('quadrant-analysis', $data_required)); $for_json = []; $for_json['fix_data'] = isset($this->options['fix_data']) ? $this->options['fix_data'] : null; $for_json['units'] = isset($this->options['units']) ? $this->options['units'] : null; $for_json['pace'] = isset($this->options['pace']) ? $this->options['pace'] : null; $lap_count = 1; $data = []; if ($quadrant_analysis) { $quadrant_plot = $this->quadrantAnalysis($crank_length, $ftp, $selected_cadence, true); if (!empty($quadrant_plot)) { $for_json['aepf_threshold'] = $quadrant_plot['aepf_threshold']; $for_json['cpv_threshold'] = $quadrant_plot['cpv_threshold']; } } if ($paused) { $is_paused = $this->isPaused(); } foreach ($this->data_mesgs['record']['timestamp'] as $ts) { if ($lap && is_array($this->data_mesgs['lap']['timestamp']) && $ts >= $this->data_mesgs['lap']['timestamp'][$lap_count - 1]) { $lap_count++; } $tmp = []; if ($timestamp) { $tmp['timestamp'] = $ts; } if ($lap) { $tmp['lap'] = $lap_count; } foreach ($this->data_mesgs['record'] as $key => $value) { if ($key !== 'timestamp') { if ($$key) { $tmp[$key] = isset($value[$ts]) ? $value[$ts] : null; } } } if ($quadrant_analysis) { if (!empty($quadrant_plot)) { $tmp['cpv'] = isset($quadrant_plot['plot'][$ts]) ? $quadrant_plot['plot'][$ts][0] : null; $tmp['aepf'] = isset($quadrant_plot['plot'][$ts]) ? $quadrant_plot['plot'][$ts][1] : null; } } if ($paused) { $tmp['paused'] = $is_paused[$ts]; } $data[] = $tmp; unset($tmp); } $for_json['data'] = $data; return json_encode($for_json); } /** * Create a JSON object that contains available lap message information. */ public function getJSONLap() { $for_json = []; $for_json['fix_data'] = isset($this->options['fix_data']) ? $this->options['fix_data'] : null; $for_json['units'] = isset($this->options['units']) ? $this->options['units'] : null; $for_json['pace'] = isset($this->options['pace']) ? $this->options['pace'] : null; $for_json['num_laps'] = count($this->data_mesgs['lap']['timestamp']); $data = []; for ($i=0; $i<$for_json['num_laps']; $i++) { $data[$i]['lap'] = $i; foreach ($this->data_mesgs['lap'] as $key => $value) { $data[$i][$key] = $value[$i]; } } $for_json['data'] = $data; return json_encode($for_json); } /** * Outputs tables of information being listened for and found within the processed FIT file. */ public function showDebugInfo() { asort($this->defn_mesgs_all); // Sort the definition messages echo '

Types

'; echo ''; // Bootstrap classes echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($this->types as $key => $val) { echo ''; } echo ''; echo '
keyPHP unpack() formatBytes
'.$key.''.$val['format'].''.$val['bytes'].'
'; echo '


'; echo '

Messages and Fields being listened for

'; foreach ($this->data_mesg_info as $key => $val) { echo '

'.$val['mesg_name'].' ('.$key.')

'; echo ''; echo ''; foreach ($val['field_defns'] as $key2 => $val2) { echo ''; } echo '
IDNameScaleOffsetUnits
'.$key2.''.$val2['field_name'].''.$val2['scale'].''.$val2['offset'].''.$val2['units'].'


'; } echo '


'; echo '

FIT Definition Messages contained within the file

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; foreach ($this->defn_mesgs_all as $key => $val) { echo ''; } echo ''; echo '
global_mesg_numnum_fieldsfield defnstotal_size
'.$val['global_mesg_num'].(isset($this->data_mesg_info[$val['global_mesg_num']]) ? ' ('.$this->data_mesg_info[$val['global_mesg_num']]['mesg_name'].')' : ' (unknown)').''.$val['num_fields'].''; foreach ($val['field_defns'] as $defn) { echo 'defn: '.$defn['field_definition_number'].'; size: '.$defn['size'].'; type: '.$defn['base_type']; echo ' (' . (isset($this->data_mesg_info[$val['global_mesg_num']]['field_defns'][$defn['field_definition_number']]) ? $this->data_mesg_info[$val['global_mesg_num']]['field_defns'][$defn['field_definition_number']]['field_name'] : 'unknown') . ')
'; } echo '
'.$val['total_size'].'
'; echo '


'; echo '

Messages found in file

'; foreach ($this->data_mesgs as $mesg_key => $mesg) { echo ''; echo ''; foreach ($mesg as $field_key => $field) { echo ''; } echo '
'.$mesg_key.'count()
'.$field_key.''.count($field).'


'; } } /* * Process HR messages * * Based heavily on logic in commit: * https://github.com/GoldenCheetah/GoldenCheetah/commit/957ae470999b9a57b5b8ec57e75512d4baede1ec * Particularly the decodeHr() method */ private function processHrMessages() { // Check that we have received HR messages if (empty($this->data_mesgs['hr'])) { return; } $hr = []; $timestamps = []; // Load all filtered_bpm values into the $hr array foreach ($this->data_mesgs['hr']['filtered_bpm'] as $hr_val) { if (is_array($hr_val)) { foreach ($hr_val as $sub_hr_val) { $hr[] = $sub_hr_val; } } else { $hr[] = $hr_val; } } // Manually scale timestamps (i.e. divide by 1024) $last_event_timestamp = $this->data_mesgs['hr']['event_timestamp']; if (is_array($last_event_timestamp)) { $last_event_timestamp = $last_event_timestamp[0]; } $start_timestamp = $this->data_mesgs['hr']['timestamp'] - $last_event_timestamp / 1024.0; $timestamps[] = $last_event_timestamp / 1024.0; // Determine timestamps (similar to compressed timestamps) foreach ($this->data_mesgs['hr']['event_timestamp_12'] as $event_timestamp_12_val) { $j=0; for ($i=0; $i<11; $i++) { $last_event_timestamp12 = $last_event_timestamp & 0xFFF; $next_event_timestamp12; if ($j % 2 === 0) { $next_event_timestamp12 = $event_timestamp_12_val[$i] + (($event_timestamp_12_val[$i+1] & 0xF) << 8); $last_event_timestamp = ($last_event_timestamp & 0xFFFFF000) + $next_event_timestamp12; } else { $next_event_timestamp12 = 16 * $event_timestamp_12_val[$i+1] + (($event_timestamp_12_val[$i] & 0xF0) >> 4); $last_event_timestamp = ($last_event_timestamp & 0xFFFFF000) + $next_event_timestamp12; $i++; } if ($next_event_timestamp12 < $last_event_timestamp12) { $last_event_timestamp += 0x1000; } $timestamps[] = $last_event_timestamp / 1024.0; $j++; } } // Map HR values to timestamps $filtered_bpm_arr = []; $secs = 0; $min_record_ts = min($this->data_mesgs['record']['timestamp']); $max_record_ts = max($this->data_mesgs['record']['timestamp']); foreach ($timestamps as $idx => $timestamp) { $ts_secs = round($timestamp + $start_timestamp); // Skip timestamps outside of the range we're interested in if ($ts_secs >= $min_record_ts && $ts_secs <= $max_record_ts) { if (isset($filtered_bpm_arr[$ts_secs])) { $filtered_bpm_arr[$ts_secs][0] += $hr[$idx]; $filtered_bpm_arr[$ts_secs][1]++; } else { $filtered_bpm_arr[$ts_secs] = [$hr[$idx], 1]; } } } // Populate the heart_rate fields for record messages foreach ($filtered_bpm_arr as $idx => $arr) { $this->data_mesgs['record']['heart_rate'][$idx] = (int)round($arr[0] / $arr[1]); } } }