Initial commit: migrate badge-admin from /tmp to /home/gpsystem
via HAPI (https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
20
app/protocol/__init__.py
Normal file
20
app/protocol/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
KKS Bluetooth Badge Protocol
|
||||
|
||||
Provides packet parsing, building, and CRC computation for the
|
||||
KKS Bluetooth badge communication protocol over TCP.
|
||||
"""
|
||||
|
||||
from .constants import * # noqa: F401,F403
|
||||
from .crc import crc_itu, verify_crc
|
||||
from .parser import PacketParser
|
||||
from .builder import PacketBuilder
|
||||
|
||||
__all__ = [
|
||||
# CRC
|
||||
"crc_itu",
|
||||
"verify_crc",
|
||||
# Classes
|
||||
"PacketParser",
|
||||
"PacketBuilder",
|
||||
]
|
||||
BIN
app/protocol/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/protocol/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/protocol/__pycache__/builder.cpython-312.pyc
Normal file
BIN
app/protocol/__pycache__/builder.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/protocol/__pycache__/constants.cpython-312.pyc
Normal file
BIN
app/protocol/__pycache__/constants.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/protocol/__pycache__/crc.cpython-312.pyc
Normal file
BIN
app/protocol/__pycache__/crc.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/protocol/__pycache__/parser.cpython-312.pyc
Normal file
BIN
app/protocol/__pycache__/parser.cpython-312.pyc
Normal file
Binary file not shown.
331
app/protocol/builder.py
Normal file
331
app/protocol/builder.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
KKS Bluetooth Badge Protocol Packet Builder
|
||||
|
||||
Constructs server response packets for the KKS badge protocol.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from .constants import (
|
||||
PROTO_HEARTBEAT,
|
||||
PROTO_HEARTBEAT_EXT,
|
||||
PROTO_LBS_ADDRESS_REQ,
|
||||
PROTO_LBS_MULTI_REPLY,
|
||||
PROTO_LOGIN,
|
||||
PROTO_MESSAGE,
|
||||
PROTO_ONLINE_CMD,
|
||||
PROTO_TIME_SYNC,
|
||||
PROTO_TIME_SYNC_2,
|
||||
PROTO_ADDRESS_REPLY_EN,
|
||||
START_MARKER_LONG,
|
||||
START_MARKER_SHORT,
|
||||
STOP_MARKER,
|
||||
)
|
||||
from .crc import crc_itu
|
||||
|
||||
|
||||
class PacketBuilder:
|
||||
"""Builds server response packets for the KKS badge protocol."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Core builder
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def build_response(
|
||||
protocol_number: int,
|
||||
serial_number: int,
|
||||
info_content: bytes = b"",
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a complete response packet.
|
||||
|
||||
Packet layout (short form, 0x7878):
|
||||
START(2) + LENGTH(1) + PROTO(1) + INFO(N) + SERIAL(2) + CRC(2) + STOP(2)
|
||||
LENGTH = 1(proto) + N(info) + 2(serial) + 2(crc)
|
||||
|
||||
If the payload exceeds 255 bytes the long form (0x7979, 2-byte
|
||||
length) is used automatically.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
protocol_number : int
|
||||
Protocol number byte.
|
||||
serial_number : int
|
||||
Packet serial number (16-bit).
|
||||
info_content : bytes
|
||||
Information content (may be empty).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The fully assembled packet.
|
||||
"""
|
||||
proto_byte = struct.pack("B", protocol_number)
|
||||
serial_bytes = struct.pack("!H", serial_number)
|
||||
|
||||
# Payload for length calculation: proto + info + serial + crc
|
||||
payload_len = 1 + len(info_content) + 2 + 2 # proto + info + serial + crc
|
||||
|
||||
if payload_len > 0xFF:
|
||||
# Long packet
|
||||
length_bytes = struct.pack("!H", payload_len)
|
||||
start_marker = START_MARKER_LONG
|
||||
else:
|
||||
length_bytes = struct.pack("B", payload_len)
|
||||
start_marker = START_MARKER_SHORT
|
||||
|
||||
# CRC is computed over: length_bytes + proto + info + serial
|
||||
crc_input = length_bytes + proto_byte + info_content + serial_bytes
|
||||
crc_value = crc_itu(crc_input)
|
||||
crc_bytes = struct.pack("!H", crc_value)
|
||||
|
||||
return (
|
||||
start_marker
|
||||
+ length_bytes
|
||||
+ proto_byte
|
||||
+ info_content
|
||||
+ serial_bytes
|
||||
+ crc_bytes
|
||||
+ STOP_MARKER
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Specific response builders
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def build_login_response(self, serial_number: int) -> bytes:
|
||||
"""
|
||||
Build a login response (0x01).
|
||||
|
||||
The server responds with an empty info content to acknowledge login.
|
||||
"""
|
||||
return self.build_response(PROTO_LOGIN, serial_number)
|
||||
|
||||
def build_heartbeat_response(
|
||||
self,
|
||||
serial_number: int,
|
||||
protocol: int = PROTO_HEARTBEAT,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a heartbeat response.
|
||||
|
||||
Works for both standard heartbeat (0x13) and extended heartbeat (0x36).
|
||||
"""
|
||||
return self.build_response(protocol, serial_number)
|
||||
|
||||
def build_time_sync_response(
|
||||
self,
|
||||
serial_number: int,
|
||||
protocol: int = PROTO_TIME_SYNC,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a time sync response (0x1F).
|
||||
|
||||
Returns the current UTC time as a 4-byte Unix timestamp.
|
||||
"""
|
||||
utc_now = int(time.time())
|
||||
info = struct.pack("!I", utc_now)
|
||||
return self.build_response(protocol, serial_number, info)
|
||||
|
||||
def build_time_sync_8a_response(self, serial_number: int) -> bytes:
|
||||
"""
|
||||
Build a Time Sync 2 response (0x8A).
|
||||
|
||||
Returns the current UTC time as YY MM DD HH MM SS (6 bytes).
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
info = struct.pack(
|
||||
"BBBBBB",
|
||||
now.year - 2000,
|
||||
now.month,
|
||||
now.day,
|
||||
now.hour,
|
||||
now.minute,
|
||||
now.second,
|
||||
)
|
||||
return self.build_response(PROTO_TIME_SYNC_2, serial_number, info)
|
||||
|
||||
def build_lbs_multi_response(self, serial_number: int) -> bytes:
|
||||
"""
|
||||
Build an LBS Multi Reply response (0x2E).
|
||||
|
||||
The server acknowledges with an empty info content.
|
||||
"""
|
||||
return self.build_response(PROTO_LBS_MULTI_REPLY, serial_number)
|
||||
|
||||
def build_online_command(
|
||||
self,
|
||||
serial_number: int,
|
||||
server_flag: int,
|
||||
command: str,
|
||||
language: int = 0x0001,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build an online command packet (0x80).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
serial_number : int
|
||||
Packet serial number.
|
||||
server_flag : int
|
||||
Server flag bits (32-bit).
|
||||
command : str
|
||||
The command string to send (ASCII).
|
||||
language : int
|
||||
Language code (default 0x0001 = Chinese).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
Complete packet.
|
||||
"""
|
||||
cmd_bytes = command.encode("ascii")
|
||||
# inner_len = server_flag(4) + cmd_content(N)
|
||||
inner_len = 4 + len(cmd_bytes)
|
||||
|
||||
info = struct.pack("B", inner_len) # 1 byte inner length
|
||||
info += struct.pack("!I", server_flag) # 4 bytes server flag
|
||||
info += cmd_bytes # N bytes command
|
||||
info += struct.pack("!H", language) # 2 bytes language
|
||||
|
||||
return self.build_response(PROTO_ONLINE_CMD, serial_number, info)
|
||||
|
||||
def build_message(
|
||||
self,
|
||||
serial_number: int,
|
||||
server_flag: int,
|
||||
message_text: str,
|
||||
language: int = 0x0001,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a message packet (0x82).
|
||||
|
||||
The message is encoded in UTF-16 Big-Endian.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
serial_number : int
|
||||
Packet serial number.
|
||||
server_flag : int
|
||||
Server flag bits (32-bit).
|
||||
message_text : str
|
||||
The message string to send.
|
||||
language : int
|
||||
Language code (default 0x0001 = Chinese).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
Complete packet.
|
||||
"""
|
||||
msg_bytes = message_text.encode("utf-16-be")
|
||||
# inner_len = server_flag(4) + msg_content(N)
|
||||
inner_len = 4 + len(msg_bytes)
|
||||
|
||||
info = struct.pack("B", inner_len) # 1 byte inner length
|
||||
info += struct.pack("!I", server_flag) # 4 bytes server flag
|
||||
info += msg_bytes # N bytes message (UTF16BE)
|
||||
info += struct.pack("!H", language) # 2 bytes language
|
||||
|
||||
return self.build_response(PROTO_MESSAGE, serial_number, info)
|
||||
|
||||
def build_address_reply_cn(
|
||||
self,
|
||||
serial_number: int,
|
||||
server_flag: int,
|
||||
address: str,
|
||||
phone: str = "",
|
||||
protocol: int = PROTO_LBS_ADDRESS_REQ,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build a Chinese address reply packet.
|
||||
|
||||
Used as a response to protocol 0x17 (LBS Address Request)
|
||||
or similar address query protocols.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
serial_number : int
|
||||
Packet serial number.
|
||||
server_flag : int
|
||||
Server flag bits (32-bit).
|
||||
address : str
|
||||
Address string (encoded as UTF-16 Big-Endian).
|
||||
phone : str
|
||||
Phone number string (BCD encoded, even length, padded with 'F').
|
||||
protocol : int
|
||||
Protocol number to respond with (default 0x17).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
Complete packet.
|
||||
"""
|
||||
addr_bytes = address.encode("utf-16-be")
|
||||
addr_len = len(addr_bytes)
|
||||
|
||||
info = struct.pack("!I", server_flag) # 4 bytes server flag
|
||||
info += struct.pack("!H", addr_len) # 2 bytes address length
|
||||
info += addr_bytes # N bytes address
|
||||
|
||||
if phone:
|
||||
phone_padded = phone if len(phone) % 2 == 0 else phone + "F"
|
||||
phone_bcd = bytes.fromhex(phone_padded)
|
||||
info += struct.pack("B", len(phone_bcd)) # 1 byte phone length
|
||||
info += phone_bcd # N bytes phone BCD
|
||||
else:
|
||||
info += struct.pack("B", 0) # 0 phone length
|
||||
|
||||
return self.build_response(protocol, serial_number, info)
|
||||
|
||||
def build_address_reply_en(
|
||||
self,
|
||||
serial_number: int,
|
||||
server_flag: int,
|
||||
address: str,
|
||||
phone: str = "",
|
||||
protocol: int = PROTO_ADDRESS_REPLY_EN,
|
||||
) -> bytes:
|
||||
"""
|
||||
Build an English address reply packet (0x97).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
serial_number : int
|
||||
Packet serial number.
|
||||
server_flag : int
|
||||
Server flag bits (32-bit).
|
||||
address : str
|
||||
Address string (ASCII/UTF-8 encoded).
|
||||
phone : str
|
||||
Phone number string (BCD encoded, even length, padded with 'F').
|
||||
protocol : int
|
||||
Protocol number to respond with (default 0x97).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
Complete packet.
|
||||
"""
|
||||
addr_bytes = address.encode("utf-8")
|
||||
addr_len = len(addr_bytes)
|
||||
|
||||
info = struct.pack("!I", server_flag) # 4 bytes server flag
|
||||
info += struct.pack("!H", addr_len) # 2 bytes address length
|
||||
info += addr_bytes # N bytes address
|
||||
|
||||
if phone:
|
||||
phone_padded = phone if len(phone) % 2 == 0 else phone + "F"
|
||||
phone_bcd = bytes.fromhex(phone_padded)
|
||||
info += struct.pack("B", len(phone_bcd)) # 1 byte phone length
|
||||
info += phone_bcd # N bytes phone BCD
|
||||
else:
|
||||
info += struct.pack("B", 0) # 0 phone length
|
||||
|
||||
return self.build_response(protocol, serial_number, info)
|
||||
172
app/protocol/constants.py
Normal file
172
app/protocol/constants.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
KKS Bluetooth Badge Protocol Constants
|
||||
|
||||
Defines all protocol markers, protocol numbers, alarm types,
|
||||
signal strength levels, data report modes, and related mappings.
|
||||
"""
|
||||
|
||||
from typing import Dict, FrozenSet
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Start / Stop Markers
|
||||
# ---------------------------------------------------------------------------
|
||||
START_MARKER_SHORT: bytes = b'\x78\x78' # 1-byte packet length field
|
||||
START_MARKER_LONG: bytes = b'\x79\x79' # 2-byte packet length field
|
||||
STOP_MARKER: bytes = b'\x0D\x0A'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol Numbers
|
||||
# ---------------------------------------------------------------------------
|
||||
PROTO_LOGIN: int = 0x01
|
||||
PROTO_HEARTBEAT: int = 0x13
|
||||
PROTO_LBS_ADDRESS_REQ: int = 0x17
|
||||
PROTO_ADDRESS_QUERY: int = 0x1A
|
||||
PROTO_TIME_SYNC: int = 0x1F
|
||||
PROTO_GPS: int = 0x22
|
||||
PROTO_LBS_MULTI: int = 0x28
|
||||
PROTO_LBS_MULTI_REPLY: int = 0x2E
|
||||
PROTO_WIFI: int = 0x2C
|
||||
PROTO_HEARTBEAT_EXT: int = 0x36
|
||||
PROTO_ONLINE_CMD: int = 0x80
|
||||
PROTO_ONLINE_CMD_REPLY: int = 0x81
|
||||
PROTO_MESSAGE: int = 0x82
|
||||
PROTO_TIME_SYNC_2: int = 0x8A
|
||||
PROTO_GENERAL_INFO: int = 0x94
|
||||
PROTO_ADDRESS_REPLY_EN: int = 0x97
|
||||
PROTO_GPS_4G: int = 0xA0
|
||||
PROTO_LBS_4G: int = 0xA1
|
||||
PROTO_WIFI_4G: int = 0xA2
|
||||
PROTO_ALARM_SINGLE_FENCE: int = 0xA3
|
||||
PROTO_ALARM_MULTI_FENCE: int = 0xA4
|
||||
PROTO_ALARM_LBS_4G: int = 0xA5
|
||||
PROTO_LBS_4G_ADDRESS_REQ: int = 0xA7
|
||||
PROTO_ALARM_ACK: int = 0x26
|
||||
PROTO_ALARM_WIFI: int = 0xA9
|
||||
PROTO_ATTENDANCE: int = 0xB0
|
||||
PROTO_ATTENDANCE_4G: int = 0xB1
|
||||
PROTO_BT_PUNCH: int = 0xB2
|
||||
PROTO_BT_LOCATION: int = 0xB3
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Alarm Types (bit-pattern -> name)
|
||||
# ---------------------------------------------------------------------------
|
||||
ALARM_TYPES: Dict[int, str] = {
|
||||
0x00: "normal",
|
||||
0x01: "sos",
|
||||
0x02: "power_cut",
|
||||
0x03: "vibration",
|
||||
0x04: "enter_fence",
|
||||
0x05: "exit_fence",
|
||||
0x06: "over_speed",
|
||||
0x09: "displacement",
|
||||
0x0A: "enter_gps_dead_zone",
|
||||
0x0B: "exit_gps_dead_zone",
|
||||
0x0C: "power_on",
|
||||
0x0D: "gps_first_fix",
|
||||
0x0E: "low_battery",
|
||||
0x0F: "low_battery_protection",
|
||||
0x10: "sim_change",
|
||||
0x11: "power_off",
|
||||
0x12: "airplane_mode",
|
||||
0x13: "remove",
|
||||
0x14: "door",
|
||||
0x15: "shutdown",
|
||||
0x16: "voice_alarm",
|
||||
0x17: "fake_base_station",
|
||||
0x18: "cover_open",
|
||||
0x19: "internal_low_battery",
|
||||
0xFE: "acc_on",
|
||||
0xFF: "acc_off",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GSM Signal Strength Levels
|
||||
# ---------------------------------------------------------------------------
|
||||
GSM_SIGNAL_LEVELS: Dict[int, str] = {
|
||||
0x00: "No Signal",
|
||||
0x01: "Very Weak",
|
||||
0x02: "Weak",
|
||||
0x03: "Good",
|
||||
0x04: "Strong",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data Report Mode (0x00 - 0x0F)
|
||||
# ---------------------------------------------------------------------------
|
||||
DATA_REPORT_MODES: Dict[int, str] = {
|
||||
0x00: "Timing Upload", # 定时上报
|
||||
0x01: "Distance Upload", # 定距上报
|
||||
0x02: "Turn Point Upload", # 拐点上传
|
||||
0x03: "ACC Status Changed", # ACC状态改变上传
|
||||
0x04: "Last Point After Stop", # 运动→静止补传最后定位点
|
||||
0x05: "Reconnect Upload", # 断网重连上报最后有效点
|
||||
0x06: "Ephemeris Force Upload", # 星历更新强制上传GPS点
|
||||
0x07: "Button Upload", # 按键上传定位点
|
||||
0x08: "Power On Upload", # 开机上报位置信息
|
||||
0x09: "Unused", # 未使用
|
||||
0x0A: "Static Update", # 设备静止后上报(时间更新)
|
||||
0x0B: "WiFi Parsed Upload", # WIFI解析经纬度上传
|
||||
0x0C: "LJDW Upload", # 立即定位指令上报
|
||||
0x0D: "Static Last Point", # 设备静止后上报最后经纬度
|
||||
0x0E: "GPSDUP Upload", # 静止状态定时上传
|
||||
0x0F: "Exit Tracking Mode", # 退出追踪模式
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol Numbers That Require a Server Response
|
||||
# ---------------------------------------------------------------------------
|
||||
PROTOCOLS_REQUIRING_RESPONSE: FrozenSet[int] = frozenset({
|
||||
PROTO_LOGIN,
|
||||
PROTO_HEARTBEAT,
|
||||
PROTO_LBS_ADDRESS_REQ,
|
||||
PROTO_ADDRESS_QUERY,
|
||||
PROTO_TIME_SYNC,
|
||||
PROTO_LBS_MULTI,
|
||||
PROTO_HEARTBEAT_EXT,
|
||||
PROTO_TIME_SYNC_2,
|
||||
# PROTO_GENERAL_INFO (0x94) does NOT require response per protocol doc
|
||||
PROTO_ALARM_SINGLE_FENCE,
|
||||
PROTO_ALARM_MULTI_FENCE,
|
||||
PROTO_ALARM_LBS_4G,
|
||||
PROTO_LBS_4G_ADDRESS_REQ,
|
||||
PROTO_ALARM_WIFI,
|
||||
PROTO_ATTENDANCE,
|
||||
PROTO_ATTENDANCE_4G,
|
||||
PROTO_BT_PUNCH,
|
||||
# Note: PROTO_BT_LOCATION (0xB3) does NOT require a response per protocol spec
|
||||
})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Protocol Number -> Human-Readable Name
|
||||
# ---------------------------------------------------------------------------
|
||||
PROTOCOL_NAMES: Dict[int, str] = {
|
||||
PROTO_LOGIN: "Login",
|
||||
PROTO_HEARTBEAT: "Heartbeat",
|
||||
PROTO_LBS_ADDRESS_REQ: "LBS Address Request",
|
||||
PROTO_ADDRESS_QUERY: "Address Query",
|
||||
PROTO_TIME_SYNC: "Time Sync",
|
||||
PROTO_ALARM_ACK: "Alarm ACK",
|
||||
PROTO_GPS: "GPS",
|
||||
PROTO_LBS_MULTI: "LBS Multi",
|
||||
PROTO_LBS_MULTI_REPLY: "LBS Multi Reply",
|
||||
PROTO_WIFI: "WIFI",
|
||||
PROTO_HEARTBEAT_EXT: "Heartbeat Extended",
|
||||
PROTO_ONLINE_CMD: "Online Command",
|
||||
PROTO_ONLINE_CMD_REPLY: "Online Command Reply",
|
||||
PROTO_MESSAGE: "Message",
|
||||
PROTO_TIME_SYNC_2: "Time Sync 2",
|
||||
PROTO_GENERAL_INFO: "General Info",
|
||||
PROTO_ADDRESS_REPLY_EN: "Address Reply (EN)",
|
||||
PROTO_GPS_4G: "GPS 4G",
|
||||
PROTO_LBS_4G: "LBS 4G",
|
||||
PROTO_WIFI_4G: "WIFI 4G",
|
||||
PROTO_ALARM_SINGLE_FENCE: "Alarm Single Fence",
|
||||
PROTO_ALARM_MULTI_FENCE: "Alarm Multi Fence",
|
||||
PROTO_ALARM_LBS_4G: "Alarm LBS 4G",
|
||||
PROTO_LBS_4G_ADDRESS_REQ: "LBS 4G Address Request",
|
||||
PROTO_ALARM_WIFI: "Alarm WIFI",
|
||||
PROTO_ATTENDANCE: "Attendance",
|
||||
PROTO_ATTENDANCE_4G: "Attendance 4G",
|
||||
PROTO_BT_PUNCH: "BT Punch",
|
||||
PROTO_BT_LOCATION: "BT Location",
|
||||
}
|
||||
76
app/protocol/crc.py
Normal file
76
app/protocol/crc.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
CRC-ITU Implementation for KKS Badge Protocol
|
||||
|
||||
Uses CRC-16/X-25 (reflected CRC-CCITT):
|
||||
Polynomial: 0x8408 (reflected 0x1021)
|
||||
Initial value: 0xFFFF
|
||||
Final XOR: 0xFFFF
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-computed CRC lookup table (256 entries, reflected polynomial 0x8408)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CRC_TABLE: List[int] = []
|
||||
|
||||
|
||||
def _generate_crc_table() -> List[int]:
|
||||
"""Generate the CRC-16/X-25 lookup table for reflected polynomial 0x8408."""
|
||||
table: List[int] = []
|
||||
for i in range(256):
|
||||
crc = i
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0x8408
|
||||
else:
|
||||
crc >>= 1
|
||||
table.append(crc)
|
||||
return table
|
||||
|
||||
|
||||
_CRC_TABLE = _generate_crc_table()
|
||||
|
||||
|
||||
def crc_itu(data: bytes) -> int:
|
||||
"""
|
||||
Compute the CRC-ITU checksum for the given data.
|
||||
|
||||
Uses the CRC-16/X-25 algorithm (reflected CRC-CCITT with final XOR).
|
||||
For a KKS protocol packet this should be the bytes from (and including)
|
||||
the packet-length field through the serial-number field.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : bytes
|
||||
The data to compute the CRC over.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
16-bit CRC value.
|
||||
"""
|
||||
crc: int = 0xFFFF
|
||||
for byte in data:
|
||||
crc = (crc >> 8) ^ _CRC_TABLE[(crc ^ byte) & 0xFF]
|
||||
return crc ^ 0xFFFF
|
||||
|
||||
|
||||
def verify_crc(data: bytes, expected_crc: int) -> bool:
|
||||
"""
|
||||
Verify that *data* produces the *expected_crc*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : bytes
|
||||
The data slice to check (same range used when computing the CRC).
|
||||
expected_crc : int
|
||||
The 16-bit CRC value to compare against.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the computed CRC matches *expected_crc*.
|
||||
"""
|
||||
return crc_itu(data) == (expected_crc & 0xFFFF)
|
||||
1142
app/protocol/parser.py
Normal file
1142
app/protocol/parser.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user