Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ A file must pass all three of these tests to be considered a valid FIT file. See
#### Read Method
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.

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.
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.

```py
messages, errors = read(
Expand All @@ -72,7 +72,7 @@ messages, errors = read(
mesg_listener = None)
```
#### mesg_listener
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.
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.

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

Expand Down Expand Up @@ -107,7 +107,7 @@ When true the scale and offset values as defined in the FIT Profile are applied
When false the raw field value is used.
```py
{
'altitude': 10435 ## raw value store in file
'altitude': 10435 ## raw value stored in file
}
```
#### enable_crc_check: true | false
Expand Down Expand Up @@ -164,7 +164,7 @@ When true FIT Epoch values are converted to Python datetime objects.
```py
{ 'time_created': {Python datetime object} }
```
When false the FIT Epoch value is used.
When false the FIT Epoch value is used.
```py
{ 'time_created': 995749880 }
```
Expand Down Expand Up @@ -210,8 +210,78 @@ The FIT_EPOCH_S value can be used to convert FIT Epoch values to Python datetime
```py
python_date = datetime.datetime.fromtimestamp(fitDateTime + FIT_EPOCH_S, datetime.UTC)
```
### BASE_TYPE_TO_FIELD_TYPE Constant
`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.
```py
field_type_string = BASE_TYPE_TO_FIELD_TYPE[base_type_value]
# e.g. BASE_TYPE_TO_FIELD_TYPE[BASE_TYPE['UINT32']] == 'uint32'
```
### FIELD_TYPE_TO_BASE_TYPE Constant
`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.
```py
base_type_value = FIELD_TYPE_TO_BASE_TYPE[field_type_string]
# e.g. FIELD_TYPE_TO_BASE_TYPE['uint32'] == BASE_TYPE['UINT32']
```
### convert_timestamp_to_datetime Method
A convenience method for converting FIT Epoch values to Python Datetime objects.
```py
python_date = convert_timestamp_to_datetime(fit_datetime)
```
```
### convert_datetime_to_timestamp Method
A convenience method for converting Python Datetime objects to FIT Epoch values.
```py
fit_datetime = convert_datetime_to_timestamp(python_date)
```

## Encoder
### Usage
```py
from datetime import datetime, timezone

from garmin_fit_sdk import Encoder, Profile

encoder = Encoder()

# Pass the MesgNum and message data as separate parameters to the onMesg() method
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
'manufacturer': 'development',
'product': 1,
'time_created': datetime.now(tz=timezone.utc),
'type': 'activity',
})

# The writeMesg() method expects the mesgNum to be included in the message data
# Internally, writeMesg() calls onMesg()
encoder.write_mesg({
'mesg_num': Profile['mesg_num']['FILE_ID'],
'manufacturer': 'development',
'product': 1,
'time_created': datetime.now(tz=timezone.utc),
'type': 'activity',
})

# Unknown values in the message will be ignored by the Encoder
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
'manufacturer': 'development',
'product': 1,
'time_created': datetime.now(tz=timezone.utc),
'type': 'activity',
'customField': 12345, # This value will be ignored by the Encoder
})

# Subfield values in the message will be ignored by the Encoder
encoder.on_mesg(Profile['mesg_num']['FILE_ID'], {
'manufacturer': 'development',
'product': 4440, # This is the main product field, which is a uint16
'garmin_product': 'edge_1050', # This value will be ignored by the Encoder, use the main field value instead
'time_created': datetime.now(tz=timezone.utc),
'type': 'activity',
})

uint8_array = encoder.close()

# Write the bytes to a file
with open('example.fit', 'wb') as f:
f.write(uint8_array)
```
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.
9 changes: 5 additions & 4 deletions garmin_fit_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
# Transfer (FIT) Protocol License.
###########################################################################################
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
# Profile Version = 21.195.0Release
# Tag = production/release/21.195.0-0-g569e7e5
# Profile Version = 21.200.0Release
# Tag = production/release/21.200.0-0-g28b5705d
############################################################################################


from garmin_fit_sdk.accumulator import Accumulator
from garmin_fit_sdk.bitstream import BitStream
from garmin_fit_sdk.crc_calculator import CrcCalculator
from garmin_fit_sdk.decoder import Decoder
from garmin_fit_sdk.encoder import Encoder
from garmin_fit_sdk.fit import BASE_TYPE, BASE_TYPE_DEFINITIONS
from garmin_fit_sdk.hr_mesg_utils import expand_heart_rates
from garmin_fit_sdk.profile import Profile
from garmin_fit_sdk.stream import Stream
from garmin_fit_sdk.util import FIT_EPOCH_S, convert_timestamp_to_datetime
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

__version__ = '21.195.0'
__version__ = '21.200.0'
4 changes: 2 additions & 2 deletions garmin_fit_sdk/accumulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Transfer (FIT) Protocol License.
###########################################################################################
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
# Profile Version = 21.195.0Release
# Tag = production/release/21.195.0-0-g569e7e5
# Profile Version = 21.200.0Release
# Tag = production/release/21.200.0-0-g28b5705d
############################################################################################


Expand Down
4 changes: 2 additions & 2 deletions garmin_fit_sdk/bitstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Transfer (FIT) Protocol License.
###########################################################################################
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
# Profile Version = 21.195.0Release
# Tag = production/release/21.195.0-0-g569e7e5
# Profile Version = 21.200.0Release
# Tag = production/release/21.200.0-0-g28b5705d
############################################################################################


Expand Down
4 changes: 2 additions & 2 deletions garmin_fit_sdk/crc_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Transfer (FIT) Protocol License.
###########################################################################################
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
# Profile Version = 21.195.0Release
# Tag = production/release/21.195.0-0-g569e7e5
# Profile Version = 21.200.0Release
# Tag = production/release/21.200.0-0-g28b5705d
############################################################################################


Expand Down
40 changes: 28 additions & 12 deletions garmin_fit_sdk/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
# Transfer (FIT) Protocol License.
###########################################################################################
# ****WARNING**** This file is auto-generated! Do NOT edit this file.
# Profile Version = 21.195.0Release
# Tag = production/release/21.195.0-0-g569e7e5
# Profile Version = 21.200.0Release
# Tag = production/release/21.200.0-0-g28b5705d
############################################################################################


Expand All @@ -23,10 +23,7 @@

_CRCSIZE = 2
_COMPRESSED_HEADER_MASK = 0x80
_MESG_DEFINITION_MASK = 0x40
_MESG_HEADER_MASK = 0x00
_LOCAL_MESG_NUM_MASK = 0x0F
_DEV_DATA_MASK = 0x20

_HEADER_WITH_CRC_SIZE = 14
_HEADER_WITHOUT_CRC_SIZE = 12
Expand Down Expand Up @@ -60,6 +57,8 @@ def __init__(self, stream: Stream):
self._decode_mode = DecodeMode.NORMAL

self._mesg_listener = None
self._mesg_definition_listener = None
self._field_description_listener = None
self._apply_scale_and_offset = True
self._convert_timestamps_to_datetimes = True
self._convert_types_to_strings = True
Expand Down Expand Up @@ -121,6 +120,8 @@ def read(self, apply_scale_and_offset = True,
expand_components = True,
merge_heart_rates = True,
mesg_listener = None,
mesg_definition_listener = None,
field_description_listener = None,
decode_mode = DecodeMode.NORMAL):
'''Reads the entire contents of the fit file and returns the decoded messages'''
self._apply_scale_and_offset = apply_scale_and_offset
Expand All @@ -131,6 +132,8 @@ def read(self, apply_scale_and_offset = True,
self._expand_components = expand_components
self._merge_heart_rates = merge_heart_rates
self._mesg_listener = mesg_listener
self._mesg_definition_listener = mesg_definition_listener
self._field_description_listener = field_description_listener
self._decode_mode = decode_mode

self._local_mesg_defs = {}
Expand Down Expand Up @@ -185,10 +188,10 @@ def __decode_next_record(self):
if record_header & _COMPRESSED_HEADER_MASK == _COMPRESSED_HEADER_MASK:
self.__decode_compressed_timestamp_message()

if record_header & _MESG_DEFINITION_MASK == _MESG_HEADER_MASK:
if record_header & FIT.MESG_DEFINITION_MASK == _MESG_HEADER_MASK:
self.__decode_message()

if record_header & _MESG_DEFINITION_MASK == _MESG_DEFINITION_MASK:
if record_header & FIT.MESG_DEFINITION_MASK == FIT.MESG_DEFINITION_MASK:
self.__decode_mesg_def()

def __decode_mesg_def(self):
Expand All @@ -197,7 +200,7 @@ def __decode_mesg_def(self):
struct_format_string = ''
mesg_def = {}
mesg_def["record_header"] = record_header
mesg_def["local_mesg_num"] = record_header & _LOCAL_MESG_NUM_MASK
mesg_def["local_mesg_num"] = record_header & FIT.LOCAL_MESG_NUM_MASK
mesg_def["reserved"] = self._stream.read_byte()

mesg_def["architecture"] = self._stream.read_byte()
Expand Down Expand Up @@ -236,7 +239,7 @@ def __decode_mesg_def(self):
mesg_def["field_definitions"].append(field_definition)
mesg_def["message_size"] += field_definition["size"]

if record_header & _DEV_DATA_MASK == _DEV_DATA_MASK:
if record_header & FIT.DEV_DATA_MASK == FIT.DEV_DATA_MASK:
num_dev_fields = self._stream.read_byte()

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

if self._mesg_definition_listener is not None:
self._mesg_definition_listener({**mesg_def})

if mesg_def["global_mesg_num"] in Profile['messages']:
message_profile = Profile['messages'][mesg_def["global_mesg_num"]]
else:
Expand All @@ -272,7 +278,7 @@ def __decode_mesg_def(self):
def __decode_message(self):
record_header = self._stream.read_byte()

local_mesg_num = record_header & _LOCAL_MESG_NUM_MASK
local_mesg_num = record_header & FIT.LOCAL_MESG_NUM_MASK
if local_mesg_num in self._local_mesg_defs:
mesg_def = self._local_mesg_defs[local_mesg_num]
else:
Expand Down Expand Up @@ -330,6 +336,13 @@ def __decode_message(self):
if self._mesg_listener is not None:
self._mesg_listener(mesg_def['global_mesg_num'], message)

if mesg_def['global_mesg_num'] == Profile['mesg_num']['FIELD_DESCRIPTION'] and self._field_description_listener is not None:
developer_data_id_mesg = next(
(m for m in self._messages.get('developer_data_id_mesgs', [])
if m.get('developer_data_index') == message.get('developer_data_index')),
{})
self._field_description_listener(message.get('key'), {**developer_data_id_mesg}, {**message})

def __decode_compressed_timestamp_message(self):
self.__raise_error("Compressed timestamp messages are not currently supported")

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

if self._developer_data_defs[message['developer_data_index']['raw_field_value']] is None:
developer_data_index = message['developer_data_index']['raw_field_value']
developer_data_def = self._developer_data_defs.get(developer_data_index)

if developer_data_def is None:
return

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

self._developer_data_defs[message['developer_data_index']['raw_field_value']]['fields'].append({
developer_data_def['fields'].append({
'developer_data_index': message['developer_data_index']['raw_field_value'],
'field_definition_number': message['field_definition_number']['raw_field_value'],
'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,
Expand Down
Loading
Loading