120 lines
3.4 KiB
Python
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))
|