feat(config): 添加配置管理和MQTT模拟服务功能

- 实现了应用配置的数据类结构(MqttConfig, TmsConfig, AppConfig)
- 创建了配置加载和解析功能,支持从YAML文件读取配置
- 添加了TDengine数据库配置和连接池管理
- 实现了MQTT客户端依赖注入和服务构建
- 创建了钻孔实时数据的ORM映射和SQL构建功能
- 实现了TDengine Writer用于数据写入超级表
- 添加了MQTT模拟服务,支持发布、订阅和数据转发功能
- 创建了随机数据发送器用于测试
- 实现了消息持久化到本地文件功能
- 配置了数据库连接池和SQL执行功能
This commit is contained in:
2026-03-12 09:58:00 +08:00
commit d5d1cb0b7d
19 changed files with 1224 additions and 0 deletions

14
db/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
from db.config import TDengineConfig, load_tdengine_config, parse_taos_url
from db.orm import DrillingRealtimeORM
from db.pool import TaosConnectionPool, create_taos_pool
from db.writer import TDengineWriter
__all__ = [
"DrillingRealtimeORM",
"TDengineConfig",
"TDengineWriter",
"TaosConnectionPool",
"create_taos_pool",
"load_tdengine_config",
"parse_taos_url",
]

65
db/config.py Normal file
View File

@@ -0,0 +1,65 @@
from dataclasses import dataclass
from urllib.parse import urlparse
from config import get_value
@dataclass(frozen=True)
class TDengineConfig:
url: str = ""
username: str = ""
password: str = ""
database: str = ""
stable: str = "drilling_realtime_st"
device_code: str = "GJ-304-0088"
pool_size: int = 2
timeout: int = 10
@property
def enabled(self):
return bool(self.base_url and self.database and self.username)
@property
def base_url(self):
base_url, _ = parse_taos_url(self.url)
return base_url
def parse_taos_url(jdbc_url):
if not jdbc_url:
return "", ""
raw = str(jdbc_url).strip()
if raw.lower().startswith("jdbc:taos-rs://"):
raw = "http://" + raw[len("jdbc:TAOS-RS://") :]
elif "://" not in raw:
raw = "http://" + raw
parsed = urlparse(raw)
base_url = f"{parsed.scheme or 'http'}://{parsed.hostname or '127.0.0.1'}:{parsed.port or 6041}"
database = (parsed.path or "").strip("/")
return base_url, database
def _resolve_raw_config(cfg_or_app):
raw = getattr(cfg_or_app, "raw", None)
return raw if isinstance(raw, dict) else cfg_or_app
def load_tdengine_config(cfg_or_app, default_device_code="GJ-304-0088"):
cfg = _resolve_raw_config(cfg_or_app)
url = get_value(cfg, ("tdengine", "url"), ("tdengine-url",), default="")
_, database_from_url = parse_taos_url(url)
return TDengineConfig(
url=url,
username=get_value(cfg, ("tdengine", "username"), ("tdengine-username",), default=""),
password=get_value(cfg, ("tdengine", "password"), ("tdengine-password",), default=""),
database=get_value(cfg, ("tdengine", "database"), ("tdengine-database",), default=database_from_url),
stable=get_value(cfg, ("tdengine", "stable"), ("tdengine-stable",), default="drilling_realtime_st"),
device_code=get_value(
cfg,
("tdengine", "device-code"),
("tdengine", "equipment-sn"),
default=default_device_code,
),
pool_size=int(get_value(cfg, ("tdengine", "pool-size"), ("tdengine-pool-size",), default=2)),
timeout=int(get_value(cfg, ("tdengine", "timeout"), ("tdengine-timeout",), default=10)),
)

122
db/orm.py Normal file
View File

@@ -0,0 +1,122 @@
import re
import time
DB_COLUMNS = [
"ts",
"stknum",
"recid",
"seqid",
"actual_date",
"actual_time",
"actcod",
"deptbitm",
"deptbitv",
"deptmeas",
"deptvert",
"blkpos",
"ropa",
"hkla",
"hklx",
"woba",
"wobx",
"torqa",
"torqx",
"rpma",
"sppa",
"chkp",
"spm1",
"spm2",
"spm3",
"tvolact",
"tvolcact",
"mfop",
"mfoa",
"mfia",
"mdoa",
"mdia",
"mtoa",
"mtia",
"mcoa",
"mcia",
"stkc",
"lagstks",
"deptretm",
"gasa",
"space1",
"space2",
"space3",
"space4",
"space5",
]
INT_COLUMNS = {"stknum", "recid", "seqid", "actcod", "rpma", "spm1", "spm2", "spm3", "mfop", "stkc", "lagstks"}
def sanitize_identifier(value, fallback):
cleaned = re.sub(r"[^A-Za-z0-9_]", "_", str(value or ""))
if not cleaned:
cleaned = fallback
if cleaned[0].isdigit():
cleaned = f"t_{cleaned}"
return cleaned.lower()
def sql_quote(value):
return "'" + str(value).replace("'", "''") + "'"
def to_int(value, default=0):
try:
return int(value)
except Exception:
return default
def to_float(value, default=0.0):
try:
return float(value)
except Exception:
return default
class DrillingRealtimeORM:
def __init__(self, database, stable="drilling_realtime_st", default_device_code="GJ-304-0088"):
self.database = database
self.stable = stable
self.default_device_code = default_device_code or "GJ-304-0088"
def build_insert_sql(self, payload):
if not isinstance(payload, dict):
raise ValueError("payload is not JSON object")
meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else {}
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
equipment_code = (
str(self.default_device_code).strip()
or str(meta.get("equipment_code", "")).strip()
or str(meta.get("equipment_sn", "")).strip()
or "GJ-304-0088"
)
table_name = sanitize_identifier(
f"drilling_realtime_{equipment_code}",
"drilling_realtime_default",
)
values = []
for col in DB_COLUMNS:
if col == "ts":
raw = data.get("ts", data.get("record_time", int(time.time() * 1000)))
values.append(str(to_int(raw, int(time.time() * 1000))))
elif col in INT_COLUMNS:
values.append(str(to_int(data.get(col, 0), 0)))
else:
values.append(str(to_float(data.get(col, 0), 0.0)))
columns_sql = ", ".join([f"`{column}`" for column in DB_COLUMNS])
values_sql = ", ".join(values)
return (
f"INSERT INTO `{self.database}`.`{table_name}` USING `{self.database}`.`{self.stable}` "
f"TAGS ({sql_quote(equipment_code)}) ({columns_sql}) VALUES ({values_sql})"
)

60
db/pool.py Normal file
View File

@@ -0,0 +1,60 @@
import base64
from queue import LifoQueue
from urllib.request import Request, urlopen
class TaosConnection:
def __init__(self, base_url, username, password, timeout=10):
self.base_url = base_url
self.username = username or ""
self.password = password or ""
self.timeout = timeout
def execute(self, sql):
auth = f"{self.username}:{self.password}"
auth_header = base64.b64encode(auth.encode("utf-8")).decode("ascii")
req = Request(
url=f"{self.base_url}/rest/sql",
data=sql.encode("utf-8"),
headers={
"Authorization": f"Basic {auth_header}",
"Content-Type": "text/plain",
},
method="POST",
)
with urlopen(req, timeout=self.timeout) as resp:
body = resp.read().decode("utf-8", errors="replace")
if resp.status != 200:
raise RuntimeError(f"HTTP {resp.status} {body}")
return body
class TaosConnectionPool:
def __init__(self, base_url, username, password, pool_size=2, timeout=10):
self.base_url = base_url
self.username = username
self.password = password
self.pool_size = max(int(pool_size or 1), 1)
self.timeout = timeout
self._pool = LifoQueue(maxsize=self.pool_size)
for _ in range(self.pool_size):
self._pool.put(TaosConnection(base_url, username, password, timeout=timeout))
def execute(self, sql):
conn = self._pool.get()
try:
return conn.execute(sql)
finally:
self._pool.put(conn)
def create_taos_pool(config):
if not config.enabled:
return None
return TaosConnectionPool(
config.base_url,
config.username,
config.password,
pool_size=config.pool_size,
timeout=config.timeout,
)

20
db/writer.py Normal file
View File

@@ -0,0 +1,20 @@
from db.orm import DrillingRealtimeORM
from db.pool import create_taos_pool
class TDengineWriter:
def __init__(self, config, pool=None, orm=None):
self.config = config
self.pool = pool if pool is not None else create_taos_pool(config)
self.orm = orm if orm is not None else DrillingRealtimeORM(
config.database,
stable=config.stable,
default_device_code=config.device_code,
)
self.enabled = bool(config.enabled and self.pool)
def write_payload(self, payload):
if not self.enabled:
return None
sql = self.orm.build_insert_sql(payload)
return self.pool.execute(sql)