This commit is contained in:
Bastian Wagner
2026-05-05 19:26:43 +02:00
commit 8d07939527
29 changed files with 2646 additions and 0 deletions

119
tests/test_fit_device.py Normal file
View File

@@ -0,0 +1,119 @@
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))