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:
2026-03-17 01:14:40 +00:00
commit 8a18a5ff16
61 changed files with 13106 additions and 0 deletions

20
app/protocol/__init__.py Normal file
View 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",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

331
app/protocol/builder.py Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff