feat: EDC 服务 — Python/uvloop 实现,UDP/TCP 异步网络服务

This commit is contained in:
wangfq
2026-05-27 10:23:15 +08:00
commit a10d176f68
11 changed files with 1076 additions and 0 deletions

268
src/models.py Normal file
View File

@@ -0,0 +1,268 @@
"""数据库模型 — 连接池管理与表结构初始化"""
import logging
import aiomysql
from src.config import (
MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB,
MYSQL_POOL_MIN, MYSQL_POOL_MAX,
)
logger = logging.getLogger(__name__)
_pool: aiomysql.Pool | None = None
async def init_pool() -> aiomysql.Pool:
"""初始化 MySQL 连接池并建表"""
global _pool
_pool = await aiomysql.create_pool(
host=MYSQL_HOST,
port=MYSQL_PORT,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
db=MYSQL_DB,
minsize=MYSQL_POOL_MIN,
maxsize=MYSQL_POOL_MAX,
autocommit=True,
)
await _create_tables(_pool)
logger.info("MySQL 连接池已初始化")
return _pool
async def get_pool() -> aiomysql.Pool:
"""获取连接池"""
assert _pool is not None, "数据库连接池未初始化"
return _pool
async def close_pool():
"""关闭连接池"""
global _pool
if _pool:
_pool.close()
await _pool.wait_closed()
_pool = None
# ─── DDL ───────────────────────────────────────────────────────────
async def _create_tables(pool: aiomysql.Pool):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
# 1. 联网终端信息表
await cur.execute("""
CREATE TABLE IF NOT EXISTS `dnt_info` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`serial` VARCHAR(45) UNIQUE NOT NULL COMMENT '设备唯一编码 Device_id',
`name` VARCHAR(45) DEFAULT '',
`ip` VARCHAR(45) DEFAULT '',
`port` INT DEFAULT 0,
`mac` VARCHAR(45) DEFAULT '',
`subnet` VARCHAR(45) DEFAULT '',
`gateway` VARCHAR(45) DEFAULT '',
`msgport` INT DEFAULT 0,
`version` VARCHAR(45) DEFAULT '',
`dtype` VARCHAR(5) DEFAULT '30' COMMENT '设备类型',
`poll_duration` INT DEFAULT 0,
`reset_duration` INT DEFAULT 0,
`state` TINYINT DEFAULT 0 COMMENT '0 offline, 1 online',
`last_login` DATETIME NULL,
`last_off` DATETIME NULL,
`online_total` INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
# 2. 车检器测试参数信息表
await cur.execute("""
CREATE TABLE IF NOT EXISTS `tb_loop_test_info` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(45) DEFAULT '',
`dev_model` VARCHAR(24) DEFAULT '',
`model_code` TINYINT DEFAULT 0,
`hard_ver` VARCHAR(10) DEFAULT '',
`soft_ver` VARCHAR(10) DEFAULT '',
`relay_exist` TINYINT DEFAULT 0,
`relay_pluse` TINYINT DEFAULT 0,
`sens_min` INT DEFAULT 0,
`sens_max` INT DEFAULT 0,
`freq_min` INT DEFAULT 0,
`freq_max` INT DEFAULT 0,
`peak_min` INT DEFAULT 0,
`peak_max` INT DEFAULT 0,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
# 3. 设备测试状态表
await cur.execute("""
CREATE TABLE IF NOT EXISTS `tb_state_tst` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`dnt_id` INT NOT NULL COMMENT 'FK → dnt_info.id',
`dpg430_addr` TINYINT DEFAULT 0,
`pcnum` VARCHAR(10) DEFAULT '' COMMENT '批次号',
`serialnum` INT DEFAULT 0 COMMENT '流水号',
`sub_type` TINYINT DEFAULT 0 COMMENT '1 DLD110, 2 PD132',
`str_type` VARCHAR(30) DEFAULT '',
`iffinish` VARCHAR(5) DEFAULT '' COMMENT '是否完成',
`fault_info` VARCHAR(100) DEFAULT '',
`relay_out` VARCHAR(24) DEFAULT '',
`ppvalue` FLOAT DEFAULT 0 COMMENT '峰峰值',
`idle_freq` FLOAT DEFAULT 0 COMMENT '开始工作频率',
`enter_freq` FLOAT DEFAULT 0,
`exit_freq` FLOAT DEFAULT 0,
`enter_dist` INT DEFAULT 0,
`exit_dist` INT DEFAULT 0,
`enter_speed` INT DEFAULT 0,
`exit_speed` INT DEFAULT 0,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_dnt_id` (`dnt_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
# 4. 采集表模板(不直接创建表,设备注册时按此结构动态建表)
logger.info("数据库表初始化完成")
# ─── 设备采集表 CRUD ────────────────────────────────────────────────
COLLECT_TABLE_PREFIX = "tb_collect_"
def collect_table_name(device_id: str) -> str:
"""根据 Device_id 生成采集表名"""
return f"{COLLECT_TABLE_PREFIX}{device_id}"
async def ensure_collect_table(device_id: str):
"""确保设备采集表存在"""
table = collect_table_name(device_id)
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(f"""
CREATE TABLE IF NOT EXISTS `{table}` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`dat_type` TINYINT DEFAULT 0 COMMENT '0心跳 1流量 2探头 3其他 4时间戳 7 RS485 8串口上报 9配置返回 11异常',
`raw_data` VARCHAR(380) DEFAULT '',
`state` TINYINT DEFAULT 0 COMMENT '0 未处理, 1 已处理',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
""")
logger.info(f"设备采集表 {table} 已就绪")
async def insert_collect_data(device_id: str, dat_type: int, raw_data: str):
"""向设备采集表插入原始数据"""
table = collect_table_name(device_id)
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
f"INSERT INTO `{table}` (dat_type, raw_data) VALUES (%s, %s)",
(dat_type, raw_data),
)
async def fetch_unparsed(device_id: str) -> list[dict]:
"""获取未处理的记录 (state=0, dat_type=8)"""
table = collect_table_name(device_id)
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute(
f"SELECT id, raw_data FROM `{table}` WHERE state = 0 AND dat_type = 8 LIMIT 100"
)
return await cur.fetchall()
async def mark_parsed(device_id: str, record_id: int):
"""标记记录为已处理"""
table = collect_table_name(device_id)
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
f"UPDATE `{table}` SET state = 1 WHERE id = %s", (record_id,)
)
# ─── dnt_info CRUD ─────────────────────────────────────────────────
async def get_dnt_by_serial(serial: str) -> dict | None:
"""根据 Device_id (serial) 查询终端"""
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT * FROM dnt_info WHERE serial = %s", (serial,))
return await cur.fetchone()
async def upsert_dnt(serial: str, ip: str, port: int, mac: str,
subnet: str, gateway: str, msgport: int,
version: str, dtype: str = "30") -> int:
"""插入或更新终端信息,返回 dnt_info.id"""
existing = await get_dnt_by_serial(serial)
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
if existing:
# 已有记录:更新 IP / 网关 / 上线时间
if (existing["ip"] != ip or existing["gateway"] != gateway
or existing["port"] != port or existing["subnet"] != subnet):
await cur.execute(
"""UPDATE dnt_info SET ip=%s, port=%s, subnet=%s, gateway=%s,
mac=%s, msgport=%s, version=%s, last_login=NOW(), state=1
WHERE serial=%s""",
(ip, port, subnet, gateway, mac, msgport, version, serial),
)
else:
await cur.execute(
"UPDATE dnt_info SET last_login=NOW(), state=1 WHERE serial=%s",
(serial,),
)
return existing["id"]
else:
await cur.execute(
"""INSERT INTO dnt_info
(serial, ip, port, mac, subnet, gateway, msgport, version, dtype,
last_login, state)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW(), 1)""",
(serial, ip, port, mac, subnet, gateway, msgport, version, dtype),
)
return cur.lastrowid
async def insert_test_result(dnt_id: int, dpg430_addr: int, pcnum: str,
serialnum: int, sub_type: int, str_type: str,
iffinish: str, fault_info: str, relay_out: str,
ppvalue: float, idle_freq: float, enter_freq: float,
exit_freq: float, enter_dist: int, exit_dist: int,
enter_speed: int, exit_speed: int):
"""插入测试结果到 tb_state_tst"""
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"""INSERT INTO tb_state_tst
(dnt_id, dpg430_addr, pcnum, serialnum, sub_type, str_type,
iffinish, fault_info, relay_out, ppvalue, idle_freq,
enter_freq, exit_freq, enter_dist, exit_dist, enter_speed, exit_speed)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
(dnt_id, dpg430_addr, pcnum, serialnum, sub_type, str_type,
iffinish, fault_info, relay_out, ppvalue, idle_freq,
enter_freq, exit_freq, enter_dist, exit_dist, enter_speed, exit_speed),
)
async def set_device_offline(serial: str):
"""标记设备离线"""
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(
"UPDATE dnt_info SET state=0, last_off=NOW() WHERE serial=%s",
(serial,),
)