Skip to content

Commit b300c26

Browse files
author
Jenkins
committed
Garmin FIT SDK 21.200.0
Change-Id: I34b6bd79f1cbd48ba26b31ab0789623bba82410e
1 parent fa2e79e commit b300c26

23 files changed

Lines changed: 3905 additions & 61 deletions

README.md

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ A file must pass all three of these tests to be considered a valid FIT file. See
5858
#### Read Method
5959
The Read method decodes all messages from the input stream and returns an object containing a list of errors encountered during the decoding and a dictionary of decoded messages grouped by message type. Any exceptions encountered during decoding will be caught by the Read method and added to the list of errors.
6060

61-
The Read method accepts an optional options object that can be used to customize how field data is represented in the decoded messages. All options are enabled by default. Disabling options may speed up file decoding. Options may also be enabled or disable based on how the decoded data will be used.
61+
The Read method accepts an optional options object that can be used to customize how field data is represented in the decoded messages. All options are enabled by default. Disabling options may speed up file decoding. Options may also be enabled or disabled based on how the decoded data will be used.
6262

6363
```py
6464
messages, errors = read(
@@ -72,7 +72,7 @@ messages, errors = read(
7272
mesg_listener = None)
7373
```
7474
#### mesg_listener
75-
Optional callback function that can be used to inspect or manipulate messages after they are fully decoded and all the options have been applied. The message is mutable and we be returned from the Read method in the messages dictionary.
75+
Optional callback function that can be used to inspect or manipulate messages after they are fully decoded and all the options have been applied. The message is mutable and will be returned from the Read method in the messages dictionary.
7676

7777
Example mesg_listener callback that tracks the field names across all Record messages.
7878

@@ -107,7 +107,7 @@ When true the scale and offset values as defined in the FIT Profile are applied
107107
When false the raw field value is used.
108108
```py
109109
{
110-
'altitude': 10435 ## raw value store in file
110+
'altitude': 10435 ## raw value stored in file
111111
}
112112
```
113113
#### enable_crc_check: true | false
@@ -164,7 +164,7 @@ When true FIT Epoch values are converted to Python datetime objects.
164164
```py
165165
{ 'time_created': {Python datetime object} }
166166
```
167-
When false the FIT Epoch value is used.
167+
When false the FIT Epoch value is used.
168168
```py
169169
{ 'time_created': 995749880 }
170170
```
@@ -210,8 +210,78 @@ The FIT_EPOCH_S value can be used to convert FIT Epoch values to Python datetime
210210
```py
211211
python_date = datetime.datetime.fromtimestamp(fitDateTime + FIT_EPOCH_S, datetime.UTC)
212212
```
213+
### BASE_TYPE_TO_FIELD_TYPE Constant
214+
`BASE_TYPE_TO_FIELD_TYPE` is a dictionary that maps FIT base type values to their corresponding field type name strings as defined in the FIT Profile.
215+
```py
216+
field_type_string = BASE_TYPE_TO_FIELD_TYPE[base_type_value]
217+
# e.g. BASE_TYPE_TO_FIELD_TYPE[BASE_TYPE['UINT32']] == 'uint32'
218+
```
219+
### FIELD_TYPE_TO_BASE_TYPE Constant
220+
`FIELD_TYPE_TO_BASE_TYPE` is the inverse mapping of `BASE_TYPE_TO_FIELD_TYPE`. It maps FIT field type name strings to their corresponding base type values as defined in the FIT Profile.
221+
```py
222+
base_type_value = FIELD_TYPE_TO_BASE_TYPE[field_type_string]
223+
# e.g. FIELD_TYPE_TO_BASE_TYPE['uint32'] == BASE_TYPE['UINT32']
224+
```
213225
### convert_timestamp_to_datetime Method
214226
A convenience method for converting FIT Epoch values to Python Datetime objects.
215227
```py
216228
python_date = convert_timestamp_to_datetime(fit_datetime)
217-
```
229+
```
230+
### convert_datetime_to_timestamp Method
231+
A convenience method for converting Python Datetime objects to FIT Epoch values.
232+
```py
233+
fit_datetime = convert_datetime_to_timestamp(python_date)
234+
```
235+
236+
## Encoder
237+
### Usage
238+
```py
239+
from datetime import datetime, timezone
240+
241+
from garmin_fit_sdk import Encoder, Profile
242+
243+
encoder = Encoder()
244+
245+
# Pass the MesgNum and message data as separate parameters to the onMesg() method
246+
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
247+
'manufacturer': 'development',
248+
'product': 1,
249+
'time_created': datetime.now(tz=timezone.utc),
250+
'type': 'activity',
251+
})
252+
253+
# The writeMesg() method expects the mesgNum to be included in the message data
254+
# Internally, writeMesg() calls onMesg()
255+
encoder.write_mesg({
256+
'mesg_num': Profile['mesg_num']['FILE_ID'],
257+
'manufacturer': 'development',
258+
'product': 1,
259+
'time_created': datetime.now(tz=timezone.utc),
260+
'type': 'activity',
261+
})
262+
263+
# Unknown values in the message will be ignored by the Encoder
264+
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
265+
'manufacturer': 'development',
266+
'product': 1,
267+
'time_created': datetime.now(tz=timezone.utc),
268+
'type': 'activity',
269+
'customField': 12345, # This value will be ignored by the Encoder
270+
})
271+
272+
# Subfield values in the message will be ignored by the Encoder
273+
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
274+
'manufacturer': 'development',
275+
'product': 4440, # This is the main product field, which is a uint16
276+
'garmin_product': 'edge_1050', # This value will be ignored by the Encoder, use the main field value instead
277+
'time_created': datetime.now(tz=timezone.utc),
278+
'type': 'activity',
279+
})
280+
281+
uint8_array = encoder.close()
282+
283+
# Write the bytes to a file
284+
with open('example.fit', 'wb') as f:
285+
f.write(uint8_array)
286+
```
287+
See the [Encode Activity Recipe](https://github.com/garmin/fit-python-sdk/blob/main/tests/test_encode_activity_recipe.py) for a complete example of encoding a FIT Activity file using the FIT Python SDK.

garmin_fit_sdk/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@
77
# Transfer (FIT) Protocol License.
88
###########################################################################################
99
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
10-
# Profile Version = 21.195.0Release
11-
# Tag = production/release/21.195.0-0-g569e7e5
10+
# Profile Version = 21.200.0Release
11+
# Tag = production/release/21.200.0-0-g28b5705d
1212
############################################################################################
1313

1414

1515
from garmin_fit_sdk.accumulator import Accumulator
1616
from garmin_fit_sdk.bitstream import BitStream
1717
from garmin_fit_sdk.crc_calculator import CrcCalculator
1818
from garmin_fit_sdk.decoder import Decoder
19+
from garmin_fit_sdk.encoder import Encoder
1920
from garmin_fit_sdk.fit import BASE_TYPE, BASE_TYPE_DEFINITIONS
2021
from garmin_fit_sdk.hr_mesg_utils import expand_heart_rates
2122
from garmin_fit_sdk.profile import Profile
2223
from garmin_fit_sdk.stream import Stream
23-
from garmin_fit_sdk.util import FIT_EPOCH_S, convert_timestamp_to_datetime
24+
from garmin_fit_sdk.util import FIT_EPOCH_S, convert_datetime_to_timestamp, convert_timestamp_to_datetime, BASE_TYPE_TO_FIELD_TYPE, FIELD_TYPE_TO_BASE_TYPE
2425

25-
__version__ = '21.195.0'
26+
__version__ = '21.200.0'

garmin_fit_sdk/accumulator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# Transfer (FIT) Protocol License.
88
###########################################################################################
99
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
10-
# Profile Version = 21.195.0Release
11-
# Tag = production/release/21.195.0-0-g569e7e5
10+
# Profile Version = 21.200.0Release
11+
# Tag = production/release/21.200.0-0-g28b5705d
1212
############################################################################################
1313

1414

garmin_fit_sdk/bitstream.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# Transfer (FIT) Protocol License.
88
###########################################################################################
99
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
10-
# Profile Version = 21.195.0Release
11-
# Tag = production/release/21.195.0-0-g569e7e5
10+
# Profile Version = 21.200.0Release
11+
# Tag = production/release/21.200.0-0-g28b5705d
1212
############################################################################################
1313

1414

garmin_fit_sdk/crc_calculator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# Transfer (FIT) Protocol License.
88
###########################################################################################
99
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
10-
# Profile Version = 21.195.0Release
11-
# Tag = production/release/21.195.0-0-g569e7e5
10+
# Profile Version = 21.200.0Release
11+
# Tag = production/release/21.200.0-0-g28b5705d
1212
############################################################################################
1313

1414

garmin_fit_sdk/decoder.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# Transfer (FIT) Protocol License.
88
###########################################################################################
99
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
10-
# Profile Version = 21.195.0Release
11-
# Tag = production/release/21.195.0-0-g569e7e5
10+
# Profile Version = 21.200.0Release
11+
# Tag = production/release/21.200.0-0-g28b5705d
1212
############################################################################################
1313

1414

@@ -23,10 +23,7 @@
2323

2424
_CRCSIZE = 2
2525
_COMPRESSED_HEADER_MASK = 0x80
26-
_MESG_DEFINITION_MASK = 0x40
2726
_MESG_HEADER_MASK = 0x00
28-
_LOCAL_MESG_NUM_MASK = 0x0F
29-
_DEV_DATA_MASK = 0x20
3027

3128
_HEADER_WITH_CRC_SIZE = 14
3229
_HEADER_WITHOUT_CRC_SIZE = 12
@@ -60,6 +57,8 @@ def __init__(self, stream: Stream):
6057
self._decode_mode = DecodeMode.NORMAL
6158

6259
self._mesg_listener = None
60+
self._mesg_definition_listener = None
61+
self._field_description_listener = None
6362
self._apply_scale_and_offset = True
6463
self._convert_timestamps_to_datetimes = True
6564
self._convert_types_to_strings = True
@@ -121,6 +120,8 @@ def read(self, apply_scale_and_offset = True,
121120
expand_components = True,
122121
merge_heart_rates = True,
123122
mesg_listener = None,
123+
mesg_definition_listener = None,
124+
field_description_listener = None,
124125
decode_mode = DecodeMode.NORMAL):
125126
'''Reads the entire contents of the fit file and returns the decoded messages'''
126127
self._apply_scale_and_offset = apply_scale_and_offset
@@ -131,6 +132,8 @@ def read(self, apply_scale_and_offset = True,
131132
self._expand_components = expand_components
132133
self._merge_heart_rates = merge_heart_rates
133134
self._mesg_listener = mesg_listener
135+
self._mesg_definition_listener = mesg_definition_listener
136+
self._field_description_listener = field_description_listener
134137
self._decode_mode = decode_mode
135138

136139
self._local_mesg_defs = {}
@@ -185,10 +188,10 @@ def __decode_next_record(self):
185188
if record_header & _COMPRESSED_HEADER_MASK == _COMPRESSED_HEADER_MASK:
186189
self.__decode_compressed_timestamp_message()
187190

188-
if record_header & _MESG_DEFINITION_MASK == _MESG_HEADER_MASK:
191+
if record_header & FIT.MESG_DEFINITION_MASK == _MESG_HEADER_MASK:
189192
self.__decode_message()
190193

191-
if record_header & _MESG_DEFINITION_MASK == _MESG_DEFINITION_MASK:
194+
if record_header & FIT.MESG_DEFINITION_MASK == FIT.MESG_DEFINITION_MASK:
192195
self.__decode_mesg_def()
193196

194197
def __decode_mesg_def(self):
@@ -197,7 +200,7 @@ def __decode_mesg_def(self):
197200
struct_format_string = ''
198201
mesg_def = {}
199202
mesg_def["record_header"] = record_header
200-
mesg_def["local_mesg_num"] = record_header & _LOCAL_MESG_NUM_MASK
203+
mesg_def["local_mesg_num"] = record_header & FIT.LOCAL_MESG_NUM_MASK
201204
mesg_def["reserved"] = self._stream.read_byte()
202205

203206
mesg_def["architecture"] = self._stream.read_byte()
@@ -236,7 +239,7 @@ def __decode_mesg_def(self):
236239
mesg_def["field_definitions"].append(field_definition)
237240
mesg_def["message_size"] += field_definition["size"]
238241

239-
if record_header & _DEV_DATA_MASK == _DEV_DATA_MASK:
242+
if record_header & FIT.DEV_DATA_MASK == FIT.DEV_DATA_MASK:
240243
num_dev_fields = self._stream.read_byte()
241244

242245
for i in range(num_dev_fields):
@@ -250,6 +253,9 @@ def __decode_mesg_def(self):
250253
mesg_def["developer_field_defs"].append(developer_field_definition)
251254
mesg_def["developer_data_size"] += developer_field_definition["size"]
252255

256+
if self._mesg_definition_listener is not None:
257+
self._mesg_definition_listener({**mesg_def})
258+
253259
if mesg_def["global_mesg_num"] in Profile['messages']:
254260
message_profile = Profile['messages'][mesg_def["global_mesg_num"]]
255261
else:
@@ -272,7 +278,7 @@ def __decode_mesg_def(self):
272278
def __decode_message(self):
273279
record_header = self._stream.read_byte()
274280

275-
local_mesg_num = record_header & _LOCAL_MESG_NUM_MASK
281+
local_mesg_num = record_header & FIT.LOCAL_MESG_NUM_MASK
276282
if local_mesg_num in self._local_mesg_defs:
277283
mesg_def = self._local_mesg_defs[local_mesg_num]
278284
else:
@@ -330,6 +336,13 @@ def __decode_message(self):
330336
if self._mesg_listener is not None:
331337
self._mesg_listener(mesg_def['global_mesg_num'], message)
332338

339+
if mesg_def['global_mesg_num'] == Profile['mesg_num']['FIELD_DESCRIPTION'] and self._field_description_listener is not None:
340+
developer_data_id_mesg = next(
341+
(m for m in self._messages.get('developer_data_id_mesgs', [])
342+
if m.get('developer_data_index') == message.get('developer_data_index')),
343+
{})
344+
self._field_description_listener(message.get('key'), {**developer_data_id_mesg}, {**message})
345+
333346
def __decode_compressed_timestamp_message(self):
334347
self.__raise_error("Compressed timestamp messages are not currently supported")
335348

@@ -632,7 +645,10 @@ def __add_field_description_to_profile(self, message):
632645
if message is None or message['developer_data_index'] is None or message['developer_data_index']['raw_field_value'] == 0xFF:
633646
return
634647

635-
if self._developer_data_defs[message['developer_data_index']['raw_field_value']] is None:
648+
developer_data_index = message['developer_data_index']['raw_field_value']
649+
developer_data_def = self._developer_data_defs.get(developer_data_index)
650+
651+
if developer_data_def is None:
636652
return
637653

638654
if message["fit_base_type_id"] is not None:
@@ -641,7 +657,7 @@ def __add_field_description_to_profile(self, message):
641657
else:
642658
base_type_code = None
643659

644-
self._developer_data_defs[message['developer_data_index']['raw_field_value']]['fields'].append({
660+
developer_data_def['fields'].append({
645661
'developer_data_index': message['developer_data_index']['raw_field_value'],
646662
'field_definition_number': message['field_definition_number']['raw_field_value'],
647663
'fit_base_type_id': message['fit_base_type_id']['raw_field_value'] & FIT.BASE_TYPE_MASK if 'fit_base_type_id' in message else None,

0 commit comments

Comments
 (0)