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:
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)
|
||||
Reference in New Issue
Block a user