نیازمندی «بستهٔ خراب پذیرفته نشود» برای ساخت گیرنده کافی نیست. باید معلوم باشد کدام بایت‌ها بررسی می‌شوند، مدل CRC چیست و پس از تطبیق آن، چه قواعد دیگری باقی می‌ماند. در این درس یک بستهٔ ساختگیِ شش‌بایتی برای مقدار میلی‌ولت می‌سازیم. معیار پذیرش را با بردار مرجع، ترتیب بایت، تغییر کنترل‌شده و پاسخ دستی می‌آزماییم. حسگر، خط مخابراتی یا فرستندهٔ واقعی نداریم و نتیجهٔ عددی به دقت اندازه‌گیری تعمیم داده نمی‌شود.

۱. نام CRC کافی نیست؛ قرارداد مدل را کامل بنویسیم

پارامترهای ثابت مدل CRC و بردار بررسی مستقل
تصویر مفهومی: پارامترهای ثابت مدل CRC و بردار بررسی مستقل؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مدل انتخابی CRC-16/X-25 است. پهنا شانزده، چندجمله‌ای معمول 0x1021، مقدار آغازین 0xFFFF و XOR پایانی 0xFFFF است؛ بازتاب ورودی و خروجی نیز فعال‌اند. در نمایش چندجمله‌ای، جملهٔ پیشروِ درجهٔ شانزده ضمنی است. فقط نوشتن «CRC شانزده‌بیتی» یا حتی نام چندجمله‌ای، همهٔ این تصمیم‌ها را تعیین نمی‌کند. دو پیاده‌سازی می‌توانند با چندجمله‌ای یکسان و آغاز متفاوت، پاسخ متفاوت بسازند.

فهرست مدل‌های رسمی pycrc این پارامترها و مقدار Check برابر 0x906E را ثبت کرده است. تعریف مدل پارامتری همان پروژه، Check را نتیجهٔ رشتهٔ ASCII «123456789» می‌داند. ورودی دقیقاً نه بایت بدون پایان خط و بدون بایت صفر پایانی است. برای این بردار، دو بایت بررسیِ کم‌ارزش‌اول، 6e و 90 می‌شوند.

این مرجع بیرونی لازم است؛ اگر سازنده و گیرنده هر دو یک خطای مشترک داشته باشند، رفت‌وبرگشت موفق می‌شود ولی سازگاری با مدل معلوم ثابت نمی‌شود. مقدار ورودی خالی هم در قرارداد ما صفر است. از این رفتار، مجاز بودن بستهٔ خالی را نتیجه نمی‌گیریم؛ تابع CRC و پذیرش بستهٔ کاربردی دو سطح متفاوت‌اند. ابزار pycrc اجرا نشده و فقط مستندات اولیهٔ آن خوانده شده است.

۲. ترتیب بیت، نمایش چندجمله‌ای و بایت خروجی را جدا کنیم

تفکیک چندجمله‌ای، ترتیب بیت و ترتیب بایت
تصویر مفهومی: تفکیک چندجمله‌ای، ترتیب بیت و ترتیب بایت؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

کد اصلی ثبات را به راست شیفت می‌دهد و از چندجمله‌ای بازتاب‌یافتهٔ 0x8408 استفاده می‌کند. این عدد، مدل دیگری به‌جای 0x1021 نیست؛ نمایش مناسب جهت پردازشِ انتخابی است. هشت بیت هر بایت با این روش پردازش می‌شود. در این پیاده‌سازی، بایت صفر اضافی به انتهای پیام اضافه نمی‌کنیم؛ مخلوط کردن الگوریتم مستقیم و قرارداد افزوده، پاسخ را تغییر می‌دهد.

مرجع مستقل برعکس، ثبات را به چپ شیفت می‌دهد، بایت‌ها را صریح بازتاب می‌کند و از 0x1021 استفاده می‌کند؛ نتیجه نیز قبل از XOR پایانی بازتاب می‌شود. بنابراین صرفاً فراخوانی دوبارهٔ تابع اصلی نیست. هر دو راه روی بردار منتشرشده، همهٔ ۲۵۶ ورودی تک‌بایتی و شش دادهٔ محدود مقایسه شده‌اند. این مجموعهٔ محدود، اثبات صحت روی همهٔ پیام‌های ممکن نیست.

ضمیمهٔ C.2 در RFC 1662 تکمیل FCS و قرار گرفتن بایت کم‌ارزش در ابتدا را نشان می‌دهد. با پردازش داده همراه دو بایت بررسیِ درست، ثبات خام به 0xF0B8 می‌رسد؛ این مقدار قبل از XOR پایانی است. تابع نهایی ما روی همان کل بسته 0x0F47 می‌دهد. پس مقایسهٔ آن با 0xF0B8 خطاست. از این قرارداد بررسی استفاده می‌کنیم؛ قالب کامل PPP را پیاده نکرده‌ایم.

۳. بدنهٔ چهار‌بایتی و کد کامل پذیرش بسته

بدنهٔ ولتاژ میلی‌ولت و مقدار بررسی انتهای بسته
تصویر مفهومی: بدنهٔ ولتاژ میلی‌ولت و مقدار بررسی انتهای بسته؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

بدنه شامل نسخهٔ یک‌بایتی، شمارهٔ یک‌بایتی و مقدار علامت‌دار دوبایتی است. واحد مقدار mV و بازهٔ مجازِ بسته منفی پنج‌هزار تا مثبت پنج‌هزار میلی‌ولت است؛ دو سر بازه پذیرفته می‌شوند. شماره بین صفر و ۲۵۵ است. شماره در این مثال برای تشخیص گم‌شدن، تکرار یا بازپخش استفاده نمی‌شود. نسخه فقط یک پذیرفته می‌شود و تبدیل خودکار نسخه نداریم.

مستندات struct برای پیشوند < اندازهٔ استاندارد، ترتیب کم‌ارزش‌اول و نبود فاصله‌گذاری خودکار را مشخص می‌کند. قالب <BBh چهار بایت می‌سازد. نسخهٔ یک، شمارهٔ هفت و ۱۲۵۰ میلی‌ولت به بدنهٔ 0107e204 می‌رسند. مقدار منفی ۱۲۵۰ در همان دو بایت به صورت 1efb ذخیره می‌شود؛ ترتیب بایت با مثبت یا منفی بودن مقدار عوض نمی‌شود.

"""Original offline CRC-16/X-25 acceptance lesson; no device, PPP or network."""
import json
import platform
import struct


def raw_update(data, state=0xFFFF):
    """Unfinalized reflected register; no trailing zero augmentation."""
    if type(data) is not bytes:
        raise TypeError('data must be exact bytes')
    if type(state) is not int:
        raise TypeError('state must be exact int')
    if not 0 <= state <= 0xFFFF:
        raise ValueError('state outside 16-bit register')
    for octet in data:
        state ^= octet
        for _ in range(8):
            state = (state >> 1) ^ (0x8408 if state & 1 else 0)
    return state


def crc16_x25(data):
    return raw_update(data) ^ 0xFFFF


def _reflect(value, width):
    result = 0
    for _ in range(width):
        result = (result << 1) | (value & 1)
        value >>= 1
    return result


def reference_normal(data):
    """Independent MSB-first orientation with explicit byte/result reflection."""
    if type(data) is not bytes:
        raise TypeError('data must be exact bytes')
    register = 0xFFFF
    for octet in data:
        register ^= _reflect(octet, 8) << 8
        for _ in range(8):
            top = register & 0x8000
            register = (register << 1) & 0xFFFF
            if top:
                register ^= 0x1021
    return _reflect(register, 16) ^ 0xFFFF


def append_fcs(body):
    return body + crc16_x25(body).to_bytes(2, 'little')


def _integer_in_range(value, lower, upper, field):
    if type(value) is not int:
        raise TypeError(field + ' must be exact int')
    if not lower <= value <= upper:
        raise ValueError(field + ' outside contract')


def build_packet(sequence, millivolts):
    _integer_in_range(sequence, 0, 255, 'sequence')
    _integer_in_range(millivolts, -5000, 5000, 'millivolts')
    body = struct.pack('<BBh', 1, sequence, millivolts)
    return append_fcs(body)


def parse_packet(packet):
    if type(packet) is not bytes:
        raise TypeError('packet must be exact bytes')
    if len(packet) != 6:
        raise ValueError('packet must contain exactly six bytes')
    body, fcs_bytes = packet[:4], packet[4:]
    if int.from_bytes(fcs_bytes, 'little') != crc16_x25(body):
        raise ValueError('checksum mismatch')
    version, sequence, millivolts = struct.unpack('<BBh', body)
    if version != 1:
        raise ValueError('unsupported version')
    _integer_in_range(millivolts, -5000, 5000, 'millivolts')
    return {'version': version, 'sequence': sequence,
            'millivolts': millivolts, 'unit': 'mV'}


def _raises(expected, function, *args):
    try:
        function(*args)
    except expected:
        return True
    return False


def run_example():
    packet = build_packet(7, 1250)
    body = packet[:4]
    negative_body = build_packet(7, -1250)[:4]
    fixtures = [b'', b'123456789', bytes(range(256)), bytes(16),
                bytes([255]) * 13, body]
    flipped_rejections = []
    for bit in range(48):
        damaged = bytearray(packet)
        damaged[bit // 8] ^= 1 << (bit % 8)
        flipped_rejections.append(_raises(ValueError, parse_packet, bytes(damaged)))
    invalid_inputs = ('123', [1, 2], bytearray(b'12'), memoryview(b'12'), None)
    invalid_states = (True, 1.5, float('nan'), None, -1, 65536)
    wrong_version = append_fcs(bytes([2, 7]) + (1250).to_bytes(2, 'little', signed=True))
    wrong_range = append_fcs(bytes([1, 7]) + (5001).to_bytes(2, 'little', signed=True))
    changed_body = bytes([1, 8]) + (1251).to_bytes(2, 'little', signed=True)
    changed_packet = append_fcs(changed_body)
    check_bytes = crc16_x25(b'123456789').to_bytes(2, 'little')
    checks = {
        'published_ascii_check_906e': crc16_x25(b'123456789') == 0x906E,
        'check_fcs_low_byte_first': check_bytes == bytes.fromhex('6e90'),
        'empty_bytes_check_zero': crc16_x25(b'') == 0x0000,
        'independent_normal_orientation': all(
            crc16_x25(data) == reference_normal(data)
            for data in [bytes([v]) for v in range(256)] + fixtures),
        'hand_fixed_layout_four_bytes': body == bytes.fromhex('0107e204')
            and struct.calcsize('<BBh') == 4,
        'hand_roundtrip_fields_and_unit': parse_packet(packet) == {
            'version': 1, 'sequence': 7, 'millivolts': 1250, 'unit': 'mV'},
        'signed_negative_twos_complement': negative_body == bytes.fromhex('01071efb')
            and parse_packet(build_packet(7, -1250))['millivolts'] == -1250,
        'raw_good_residue_before_xorout': raw_update(packet) == 0xF0B8
            and crc16_x25(packet) == 0x0F47,
        'all_48_fixture_single_bit_changes_rejected': all(flipped_rejections),
        'swapped_fcs_bytes_rejected': packet[4] != packet[5]
            and _raises(ValueError, parse_packet, body + packet[4:][::-1]),
        'exact_packet_length_rejects_short_and_extra': all(
            _raises(ValueError, parse_packet, packet[:n]) for n in range(6))
            and _raises(ValueError, parse_packet, packet + b'\x00'),
        'valid_crc_does_not_override_version': raw_update(wrong_version) == 0xF0B8
            and _raises(ValueError, parse_packet, wrong_version),
        'valid_crc_does_not_override_measurement_range': raw_update(wrong_range) == 0xF0B8
            and _raises(ValueError, parse_packet, wrong_range),
        'streaming_all_splits_finalize_once': all(
            (raw_update(data[split:], raw_update(data[:split])) ^ 0xFFFF) == crc16_x25(data)
            for data in fixtures for split in range(len(data) + 1)),
        'exact_bytes_and_register_contract': all(
            _raises(TypeError, raw_update, data) for data in invalid_inputs)
            and all(_raises((TypeError, ValueError), raw_update, b'', state)
                    for state in invalid_states)
            and all(_raises(TypeError, parse_packet, data) for data in invalid_inputs),
        'builder_rejects_type_and_range': all(
            _raises((TypeError, ValueError), build_packet, seq, 0)
            for seq in (True, 1.0, float('nan'), None, -1, 256))
            and all(_raises((TypeError, ValueError), build_packet, 0, mv)
                    for mv in (False, 1.0, float('nan'), None, -5001, 5001)),
        'closed_endpoints_roundtrip': all(
            parse_packet(build_packet(seq, mv)) == {
                'version': 1, 'sequence': seq, 'millivolts': mv, 'unit': 'mV'}
            for seq in (0, 255) for mv in (-5000, 5000)),
        'recomputed_crc_is_not_authentication': changed_packet != packet
            and parse_packet(changed_packet) == {
                'version': 1, 'sequence': 8, 'millivolts': 1251, 'unit': 'mV'}
            and packet == build_packet(7, 1250),
    }
    assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values())
    return {
        'runtime': {'python': platform.python_version(), 'libraries': ['stdlib only']},
        'scope': 'Offline synthetic six-byte sensor packet, CRC-16/X-25; no PPP/device/network/authentication test.',
        'model': {'width': 16, 'poly_normal': '0x1021', 'poly_reflected': '0x8408',
                  'init': '0xFFFF', 'refin': True, 'refout': True, 'xorout': '0xFFFF',
                  'fcs_byte_order': 'little', 'trailing_zero_augmentation': False},
        'metrics': {'ascii_check': f'0x{crc16_x25(b"123456789"):04X}',
                    'check_fcs_hex': check_bytes.hex(), 'body_hex': body.hex(),
                    'body_crc': f'0x{crc16_x25(body):04X}', 'packet_hex': packet.hex(),
                    'parsed': parse_packet(packet), 'negative_body_hex': negative_body.hex(),
                    'raw_packet_residue': f'0x{raw_update(packet):04X}',
                    'finalized_whole_packet_crc': f'0x{crc16_x25(packet):04X}',
                    'single_bit_mutations_rejected': sum(flipped_rejections),
                    'reference_single_byte_inputs': 256, 'reference_extra_fixtures': len(fixtures),
                    'changed_packet_hex': changed_packet.hex(),
                    'changed_parsed': parse_packet(changed_packet)},
        'checks': checks,
        'limitations': [
            'Finite fixtures and two implementations are evidence, not proof for all byte strings.',
            'Only this one fixture has all 48 single-bit mutations executed; no arbitrary error-rate claim.',
            'No malicious-sender authentication, replay check, loss detection, cryptographic integrity, PPP interoperability or hardware trial.',
            'Integer mV are authored packet values, not measured accuracy or analog voltage validation.',
        ],
    }


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

کد با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. فقط bytes دقیق برای بسته و int دقیق برای مقدارها پذیرفته می‌شود؛ bool، رشته، float و NaN رد می‌شوند. ابتدا نوع و طول شش بایت، سپس CRC و بعد نسخه و محدودهٔ مقدار بررسی می‌شوند. ذخیره‌پذیری یک عدد در int16 به معنای مجاز بودن آن در قرارداد حسگر ما نیست. خطاها نیز با تبدیل بی‌صدا به صفر پنهان نمی‌شوند.

۴. خروجی واقعی و هجده گروه بررسیِ قابل بازتولید

آزمون تغییر یک بیت و مقایسه با گیرنده
تصویر مفهومی: آزمون تغییر یک بیت و مقایسه با گیرنده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای بدنهٔ معلوم، دو پیاده‌سازی CRC برابر 0xF06D می‌دهند؛ بنابراین بستهٔ کامل 0107e2046df0 است. این مقدار CRC، محاسبهٔ ثبت‌شدهٔ مثال است؛ پاسخ دستی مستقل، چیدمان بدنه و مقدار فیلدهاست. بردار خارجی 0x906E هم جدا بررسی می‌شود. مخلوط کردن این دو نوع شاهد، مقایسه با پاسخ مستقل را کم‌اثر می‌کند.

{
  "runtime": {
    "python": "3.14.5",
    "libraries": [
      "stdlib only"
    ]
  },
  "scope": "Offline synthetic six-byte sensor packet, CRC-16/X-25; no PPP/device/network/authentication test.",
  "model": {
    "width": 16,
    "poly_normal": "0x1021",
    "poly_reflected": "0x8408",
    "init": "0xFFFF",
    "refin": true,
    "refout": true,
    "xorout": "0xFFFF",
    "fcs_byte_order": "little",
    "trailing_zero_augmentation": false
  },
  "metrics": {
    "ascii_check": "0x906E",
    "check_fcs_hex": "6e90",
    "body_hex": "0107e204",
    "body_crc": "0xF06D",
    "packet_hex": "0107e2046df0",
    "parsed": {
      "version": 1,
      "sequence": 7,
      "millivolts": 1250,
      "unit": "mV"
    },
    "negative_body_hex": "01071efb",
    "raw_packet_residue": "0xF0B8",
    "finalized_whole_packet_crc": "0x0F47",
    "single_bit_mutations_rejected": 48,
    "reference_single_byte_inputs": 256,
    "reference_extra_fixtures": 6,
    "changed_packet_hex": "0108e30472a3",
    "changed_parsed": {
      "version": 1,
      "sequence": 8,
      "millivolts": 1251,
      "unit": "mV"
    }
  },
  "checks": {
    "published_ascii_check_906e": true,
    "check_fcs_low_byte_first": true,
    "empty_bytes_check_zero": true,
    "independent_normal_orientation": true,
    "hand_fixed_layout_four_bytes": true,
    "hand_roundtrip_fields_and_unit": true,
    "signed_negative_twos_complement": true,
    "raw_good_residue_before_xorout": true,
    "all_48_fixture_single_bit_changes_rejected": true,
    "swapped_fcs_bytes_rejected": true,
    "exact_packet_length_rejects_short_and_extra": true,
    "valid_crc_does_not_override_version": true,
    "valid_crc_does_not_override_measurement_range": true,
    "streaming_all_splits_finalize_once": true,
    "exact_bytes_and_register_contract": true,
    "builder_rejects_type_and_range": true,
    "closed_endpoints_roundtrip": true,
    "recomputed_crc_is_not_authentication": true
  },
  "limitations": [
    "Finite fixtures and two implementations are evidence, not proof for all byte strings.",
    "Only this one fixture has all 48 single-bit mutations executed; no arbitrary error-rate claim.",
    "No malicious-sender authentication, replay check, loss detection, cryptographic integrity, PPP interoperability or hardware trial.",
    "Integer mV are authored packet values, not measured accuracy or analog voltage validation."
  ]
}

هر یک از ۴۸ بیت همین بسته، جداگانه تغییر کرده و گیرنده همهٔ ۴۸ نسخه را رد کرده است. این یک گروه بررسی است؛ نرخ خرابی شبکه، احتمال خطای تشخیص‌نداده یا تضمین همهٔ الگوهای چندبیتی از آن استخراج نمی‌شود. جابه‌جایی دو بایت FCS، حذف بایت و افزودن بایت نیز بررسی شده‌اند. آزمونِ طول ثابت جای بازسازی مرز پیام در جریان دریافت را نمی‌گیرد.

در یک گروه دیگر، پیام در همهٔ محل‌های تقسیمِ داده‌های محدود دو تکه می‌شود. ثبات خام ادامه می‌یابد و XOR فقط یک بار در پایان اعمال می‌شود؛ محاسبهٔ CRC نهاییِ جداگانه برای هر تکه و چسباندن جواب‌ها همین معنا را ندارد. نسخهٔ نامعتبر و مقدار ۵۰۰۱ میلی‌ولت نیز با CRC دوباره‌محاسبه‌شده ساخته می‌شوند و همچنان رد می‌شوند. صحت بررسیِ بایت‌ها، قواعد کاربرد را حذف نمی‌کند.

مرز این تمرین با قاب‌بندی پیام حسگر روی TCP، قرارداد شش‌بایتیِ ازقبل‌جداشده است. برای معنای فیلدها نیز قرارداد خطای دادهٔ JSON حسگر مکمل مناسبی است. پذیرش هر دو بستهٔ درست، اصالت فرستنده را ثابت نمی‌کند: تغییر شماره و مقدار و محاسبهٔ مجدد CRC در این مثال پذیرفته می‌شود. هیچ کلید محرمانه‌ای برای جلوگیری از این کار نداریم.

۵. چهار پرسش پاسخ‌دار برای معیار پذیرش روشن

مرز بررسی خطای CRC با اصالت فرستنده
تصویر مفهومی: مرز بررسی خطای CRC با اصالت فرستنده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: مدل و ترتیب خروجی را چگونه مستقل بررسی کنیم؟ پاسخ: نه بایت ASCII شناخته‌شده باید 0x906E بدهد و FCS کم‌ارزش‌اول آن 6e90 باشد. موفقیت رفت‌وبرگشتِ سازنده و گیرنده به‌تنهایی کافی نیست.

پرسش دوم: چرا ثبات 0xF0B8 با خروجی 0x0F47 تفاوت دارد؟ پاسخ: اولی ثبات خام پس از خواندن کل بسته است؛ دومی پس از XOR پایانی محاسبه می‌شود. هر آزمون باید مرحلهٔ مورد مقایسه را نام ببرد.

پرسش سوم: ۵۰۰۱ میلی‌ولت با FCS درست پذیرفته می‌شود؟ پاسخ: خیر؛ با وجود ذخیره‌شدن در دو بایت علامت‌دار، از محدودهٔ کاربرد خارج است. مقدار ۵۰۰۰ مجاز است و واحد هم میلی‌ولت تعریف شده؛ تبدیل از ولت انجام نمی‌دهیم.

پرسش چهارم: CRC درست یعنی فرستنده معتبر است؟ پاسخ: خیر؛ تغییر عمدی بدنه و ساخت FCS تازه در مثال پذیرفته شد. بررسی خطای بایت، احراز هویت و مقابله با بازپخش یکی نیستند. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ این هجده گروه، آزمون سخت‌افزار یا انطباق کامل PPP نیستند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.