Files
mywhoosh-garmin-sync/tests/test_fit_device.py
Bastian Wagner 8d07939527 Init
2026-05-05 19:26:43 +02:00

120 lines
3.4 KiB
Python

from pathlib import Path
import struct
import pytest
from mywhoosh_garmin_sync.fit_crc import fit_crc
from mywhoosh_garmin_sync.fit_device import (
DeviceFieldValue,
FitFormatError,
GarminDevice,
convert_fit_device,
read_device_field_values,
)
def test_convert_fit_device_patches_metadata_and_crc(tmp_path: Path):
source = tmp_path / "source.fit"
output = tmp_path / "output.fit"
source.write_bytes(_minimal_activity_fit())
result = convert_fit_device(
source,
output,
GarminDevice(product_id=3578, product_name="Edge 1030 Plus", serial_number=42),
)
assert result.patched_field_count == 8
values = read_device_field_values(output)
assert DeviceFieldValue(0, 1, 1) in values
assert DeviceFieldValue(0, 2, 3578) in values
assert DeviceFieldValue(0, 3, 42) in values
assert DeviceFieldValue(0, 8, "Edge 1030 Plus") in values
assert DeviceFieldValue(23, 2, 1) in values
assert DeviceFieldValue(23, 3, 42) in values
assert DeviceFieldValue(23, 4, 3578) in values
assert DeviceFieldValue(23, 27, "Edge 1030 Plus") in values
data = output.read_bytes()
assert struct.unpack_from("<H", data, 12)[0] == fit_crc(data[:12])
assert struct.unpack_from("<H", data, len(data) - 2)[0] == fit_crc(data[:-2])
def test_convert_fit_device_rejects_bad_crc(tmp_path: Path):
source = tmp_path / "bad.fit"
output = tmp_path / "output.fit"
data = bytearray(_minimal_activity_fit())
data[-1] ^= 0xFF
source.write_bytes(data)
with pytest.raises(FitFormatError):
convert_fit_device(source, output)
def _minimal_activity_fit() -> bytes:
data = bytearray()
data.extend(
_definition(
local=0,
global_message=0,
fields=[
(0, 1, 0x00),
(1, 2, 0x84),
(2, 2, 0x84),
(3, 4, 0x8C),
(8, 20, 0x07),
],
)
)
data.extend(b"\x00")
data.extend(struct.pack("<BHHI", 4, 32, 40, 999))
data.extend(_fit_string("MyWhoosh", 20))
data.extend(
_definition(
local=1,
global_message=23,
fields=[
(2, 2, 0x84),
(3, 4, 0x8C),
(4, 2, 0x84),
(27, 20, 0x07),
],
)
)
data.extend(b"\x01")
data.extend(struct.pack("<HIH", 32, 999, 40))
data.extend(_fit_string("Trainer", 20))
header = bytearray(14)
header[0] = 14
header[1] = 0x10
struct.pack_into("<H", header, 2, 0x08)
struct.pack_into("<I", header, 4, len(data))
header[8:12] = b".FIT"
struct.pack_into("<H", header, 12, fit_crc(header[:12]))
fit_file = header + data + b"\x00\x00"
struct.pack_into("<H", fit_file, len(fit_file) - 2, fit_crc(fit_file[:-2]))
return bytes(fit_file)
def _definition(
local: int, global_message: int, fields: list[tuple[int, int, int]]
) -> bytes:
result = bytearray()
result.append(0x40 | local)
result.append(0)
result.append(0)
result.extend(struct.pack("<H", global_message))
result.append(len(fields))
for field_num, size, base_type in fields:
result.extend(bytes([field_num, size, base_type]))
return bytes(result)
def _fit_string(value: str, size: int) -> bytes:
encoded = value.encode("utf-8")[: size - 1] + b"\x00"
return encoded + b"\x00" * (size - len(encoded))