Skip to content

Commit 00ba277

Browse files
committed
RFCT move crc computation to records.Crc, add test
1 parent acd5434 commit 00ba277

File tree

5 files changed

+97
-39
lines changed

5 files changed

+97
-39
lines changed

fitparse/base.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@
1212
from fitparse.processors import FitFileDataProcessor
1313
from fitparse.profile import FIELD_TYPE_TIMESTAMP, MESSAGE_TYPES
1414
from fitparse.records import (
15-
DataMessage, FieldData, FieldDefinition, DevFieldDefinition, DefinitionMessage, MessageHeader,
16-
BASE_TYPES, BASE_TYPE_BYTE, DevField,
15+
Crc, DataMessage, FieldData, FieldDefinition, DevFieldDefinition, DefinitionMessage, MessageHeader,
16+
BASE_TYPES, BASE_TYPE_BYTE,
1717
add_dev_data_id, add_dev_field_description, get_dev_type
1818
)
19-
from fitparse.utils import calc_crc, fileish_open, FitParseError, FitEOFError, FitCRCError, FitHeaderError
19+
from fitparse.utils import fileish_open, FitParseError, FitEOFError, FitCRCError, FitHeaderError
2020

2121

2222
class FitFile(object):
2323
def __init__(self, fileish, check_crc=True, data_processor=None):
2424
self._file = fileish_open(fileish, 'rb')
2525

2626
self.check_crc = check_crc
27+
self._crc = None
2728
self._processor = data_processor or FitFileDataProcessor()
2829

2930
# Get total filesize
@@ -55,34 +56,40 @@ def _read(self, size):
5556
if size <= 0:
5657
return None
5758
data = self._file.read(size)
58-
self._crc = calc_crc(data, self._crc)
59+
if size != len(data):
60+
raise FitEOFError("Tried to read %d bytes from .FIT file but got %d" % (size, len(data)))
61+
62+
if self.check_crc:
63+
self._crc.update(data)
5964
self._bytes_left -= len(data)
6065
return data
6166

6267
def _read_struct(self, fmt, endian='<', data=None, always_tuple=False):
63-
fmt_with_endian = "%s%s" % (endian, fmt)
68+
if fmt.startswith('<') or fmt.startswith('>'):
69+
# fmt contains endian
70+
fmt_with_endian = fmt
71+
else:
72+
fmt_with_endian = "%s%s" % (endian, fmt)
6473
size = struct.calcsize(fmt_with_endian)
6574
if size <= 0:
6675
raise FitParseError("Invalid struct format: %s" % fmt_with_endian)
6776

6877
if data is None:
6978
data = self._read(size)
7079

71-
if size != len(data):
72-
raise FitEOFError("Tried to read %d bytes from .FIT file but got %d" % (size, len(data)))
73-
7480
unpacked = struct.unpack(fmt_with_endian, data)
7581
# Flatten tuple if it's got only one value
7682
return unpacked if (len(unpacked) > 1) or always_tuple else unpacked[0]
7783

7884
def _read_and_assert_crc(self, allow_zero=False):
7985
# CRC Calculation is little endian from SDK
80-
crc_expected, crc_actual = self._crc, self._read_struct('H')
81-
82-
if (crc_actual != crc_expected) and not (allow_zero and (crc_actual == 0)):
83-
if self.check_crc:
84-
raise FitCRCError('CRC Mismatch [expected = 0x%04X, actual = 0x%04X]' % (
85-
crc_expected, crc_actual))
86+
crc_computed, crc_read = self._crc.value, self._read_struct(Crc.FMT)
87+
if not self.check_crc:
88+
return
89+
if crc_computed == crc_read or (allow_zero and crc_read == 0):
90+
return
91+
raise FitCRCError('CRC Mismatch [computed: 0x%04X, read: 0x%04X]' % (
92+
crc_computed, crc_read))
8693

8794
##########
8895
# Private Data Parsing Methods
@@ -94,7 +101,7 @@ def _parse_file_header(self):
94101
self._bytes_left = -1
95102
self._complete = False
96103
self._compressed_ts_accumulator = 0
97-
self._crc = 0
104+
self._crc = Crc()
98105
self._local_mesgs = {}
99106
self._messages = []
100107

fitparse/records.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,52 @@ def render(self, raw_value):
331331
return raw_value
332332

333333

334+
class Crc(object):
335+
"""FIT file CRC computation."""
336+
337+
CRC_TABLE = (
338+
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
339+
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
340+
)
341+
342+
FMT = '<H'
343+
344+
def __init__(self, value=0, byte_arr=None):
345+
self.value = value
346+
if byte_arr:
347+
self.update(byte_arr)
348+
349+
def __repr__(self):
350+
return '<%s %s>' % (self.__class__.__name__, self.value or "-")
351+
352+
def __str__(self):
353+
return self.format(self.value)
354+
355+
def update(self, byte_arr):
356+
"""Read bytes and update the CRC computed."""
357+
if byte_arr:
358+
self.value = self.compute(byte_arr, self.value)
359+
360+
@staticmethod
361+
def format(value):
362+
"""Format CRC value to string."""
363+
return '0x%04X' % value
364+
365+
@classmethod
366+
def compute(cls, byte_arr, crc=0):
367+
"""Compute CRC for input bytes."""
368+
for byte in bytearray(byte_arr):
369+
# Taken verbatim from FIT SDK docs
370+
tmp = cls.CRC_TABLE[crc & 0xF]
371+
crc = (crc >> 4) & 0x0FFF
372+
crc = crc ^ tmp ^ cls.CRC_TABLE[byte & 0xF]
373+
374+
tmp = cls.CRC_TABLE[crc & 0xF]
375+
crc = (crc >> 4) & 0x0FFF
376+
crc = crc ^ tmp ^ cls.CRC_TABLE[(byte >> 4) & 0xF]
377+
return crc
378+
379+
334380
def parse_string(string):
335381
try:
336382
end = string.index(0x00)

fitparse/utils.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,6 @@ class FitHeaderError(FitParseError):
1616
pass
1717

1818

19-
CRC_TABLE = (
20-
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401,
21-
0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400,
22-
)
23-
24-
25-
def calc_crc(byte_arr, crc=0):
26-
for byte in bytearray(byte_arr):
27-
# Taken verbatim from FIT SDK docs
28-
tmp = CRC_TABLE[crc & 0xF]
29-
crc = (crc >> 4) & 0x0FFF
30-
crc = crc ^ tmp ^ CRC_TABLE[byte & 0xF]
31-
32-
tmp = CRC_TABLE[crc & 0xF]
33-
crc = (crc >> 4) & 0x0FFF
34-
crc = crc ^ tmp ^ CRC_TABLE[(byte >> 4) & 0xF]
35-
return crc
36-
37-
3819
METHOD_NAME_SCRUBBER = re.compile(r'\W|^(?=\d)')
3920
UNIT_NAME_TO_FUNC_REPLACEMENTS = (
4021
('/', ' per '),

tests/test.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
import csv
44
import datetime
5-
import io
65
import os
76
from struct import pack
87
import sys
98

109
from fitparse import FitFile
1110
from fitparse.processors import UTC_REFERENCE, StandardUnitsDataProcessor
12-
from fitparse.records import BASE_TYPES
13-
from fitparse.utils import calc_crc, FitEOFError, FitCRCError, FitHeaderError
11+
from fitparse.records import BASE_TYPES, Crc
12+
from fitparse.utils import FitEOFError, FitCRCError, FitHeaderError
1413

1514
if sys.version_info >= (2, 7):
1615
import unittest
@@ -65,8 +64,8 @@ def generate_fitfile(data=None, endian='<'):
6564

6665
# Prototcol version 1.0, profile version 1.52
6766
header = pack('<2BHI4s', 14, 16, 152, len(fit_data), b'.FIT')
68-
file_data = header + pack('<H', calc_crc(header)) + fit_data
69-
return file_data + pack('<H', calc_crc(file_data))
67+
file_data = header + pack(Crc.FMT, Crc.compute(header)) + fit_data
68+
return file_data + pack(Crc.FMT, Crc.compute(file_data))
7069

7170

7271
def secs_to_dt(secs):

tests/test_records.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
5+
from fitparse.records import Crc
6+
7+
if sys.version_info >= (2, 7):
8+
import unittest
9+
else:
10+
import unittest2 as unittest
11+
12+
13+
class RecordsTestCase(unittest.TestCase):
14+
def test_crc(self):
15+
crc = Crc()
16+
self.assertEqual(0, crc.value)
17+
crc.update(b'\x0e\x10\x98\x00(\x00\x00\x00.FIT')
18+
self.assertEqual(0xace7, crc.value)
19+
# 0 must not change the crc
20+
crc.update(0)
21+
self.assertEqual(0xace7, crc.value)
22+
23+
24+
if __name__ == '__main__':
25+
unittest.main()

0 commit comments

Comments
 (0)