You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web/Sources/vendor/adriangibbons/php-fit-file-analysis/src/phpFITFileAnalysis.php

2957 lines
166 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace adriangibbons;
/**
* phpFITFileAnalysis
* =====================
* A PHP class for Analysing FIT files created by Garmin GPS devices.
* Adrian Gibbons, 2015
* Adrian.GitHub@gmail.com
*
* G Frogley edits:
* Added code to generate TRIMPexp and hrIF (Intensity Factor) value to measure session if Power is not present (June 2015).
* Added code to generate Quadrant Analysis data (September 2015).
*
* Rafael Nájera edits:
* Added code to support compressed timestamps (March 2017).
*
* https://github.com/adriangibbons/phpFITFileAnalysis
* http://www.thisisant.com/resources/fit
*/
if (!defined('DEFINITION_MESSAGE')) {
define('DEFINITION_MESSAGE', 1);
}
if (!defined('DATA_MESSAGE')) {
define('DATA_MESSAGE', 0);
}
/*
* This is the number of seconds difference between FIT and Unix timestamps.
* FIT timestamps are seconds since UTC 00:00:00 Dec 31 1989 (source FIT SDK)
* Unix time is the number of seconds since UTC 00:00:00 Jan 01 1970
*/
if (!defined('FIT_UNIX_TS_DIFF')) {
define('FIT_UNIX_TS_DIFF', 631065600);
}
class phpFITFileAnalysis
{
public $data_mesgs = []; // Used to store the data read from the file in associative arrays.
private $dev_field_descriptions = [];
private $options = null; // Options provided to __construct().
private $file_contents = ''; // FIT file is read-in to memory as a string, split into an array, and reversed. See __construct().
private $file_pointer = 0; // Points to the location in the file that shall be read next.
private $defn_mesgs = []; // Array of FIT 'Definition Messages', which describe the architecture, format, and fields of 'Data Messages'.
private $defn_mesgs_all = []; // Keeps a record of all Definition Messages as index ($local_mesg_type) of $defn_mesgs may be reused in file.
private $file_header = []; // Contains information about the FIT file such as the Protocol version, Profile version, and Data Size.
private $php_trader_ext_loaded = false; // Is the PHP Trader extension loaded? Use $this->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; $i<max(array_keys($result)) / $bucket_width; ++$i) {
if (!isset($result[$i * $bucket_width])) {
$result[$i * $bucket_width] = 0;
}
}
ksort($result);
return $result;
}
/**
* Helper functions / shortcuts.
*/
public function hrPartionedHRmaximum($hr_maximum)
{
return $this->partitionData('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 '<h3>Types</h3>';
echo '<table class=\'table table-condensed table-striped\'>'; // Bootstrap classes
echo '<thead>';
echo '<th>key</th>';
echo '<th>PHP unpack() format</th>';
echo '<th>Bytes</th>';
echo '</thead>';
echo '<tbody>';
foreach ($this->types as $key => $val) {
echo '<tr><td>'.$key.'</td><td>'.$val['format'].'</td><td>'.$val['bytes'].'</td></tr>';
}
echo '</tbody>';
echo '</table>';
echo '<br><hr><br>';
echo '<h3>Messages and Fields being listened for</h3>';
foreach ($this->data_mesg_info as $key => $val) {
echo '<h4>'.$val['mesg_name'].' ('.$key.')</h4>';
echo '<table class=\'table table-condensed table-striped\'>';
echo '<thead><th>ID</th><th>Name</th><th>Scale</th><th>Offset</th><th>Units</th></thead><tbody>';
foreach ($val['field_defns'] as $key2 => $val2) {
echo '<tr><td>'.$key2.'</td><td>'.$val2['field_name'].'</td><td>'.$val2['scale'].'</td><td>'.$val2['offset'].'</td><td>'.$val2['units'].'</td></tr>';
}
echo '</tbody></table><br><br>';
}
echo '<br><hr><br>';
echo '<h3>FIT Definition Messages contained within the file</h3>';
echo '<table class=\'table table-condensed table-striped\'>';
echo '<thead>';
echo '<th>global_mesg_num</th>';
echo '<th>num_fields</th>';
echo '<th>field defns</th>';
echo '<th>total_size</th>';
echo '</thead>';
echo '<tbody>';
foreach ($this->defn_mesgs_all as $key => $val) {
echo '<tr><td>'.$val['global_mesg_num'].(isset($this->data_mesg_info[$val['global_mesg_num']]) ? ' ('.$this->data_mesg_info[$val['global_mesg_num']]['mesg_name'].')' : ' (unknown)').'</td><td>'.$val['num_fields'].'</td><td>';
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') . ')<br>';
}
echo '</td><td>'.$val['total_size'].'</td></tr>';
}
echo '</tbody>';
echo '</table>';
echo '<br><hr><br>';
echo '<h3>Messages found in file</h3>';
foreach ($this->data_mesgs as $mesg_key => $mesg) {
echo '<table class=\'table table-condensed table-striped\'>';
echo '<thead><th>'.$mesg_key.'</th><th>count()</th></thead><tbody>';
foreach ($mesg as $field_key => $field) {
echo '<tr><td>'.$field_key.'</td><td>'.count($field).'</td></tr>';
}
echo '</tbody></table><br><br>';
}
}
/*
* 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]);
}
}
}