feat(config): 添加配置管理和MQTT模拟服务功能
- 实现了应用配置的数据类结构(MqttConfig, TmsConfig, AppConfig) - 创建了配置加载和解析功能,支持从YAML文件读取配置 - 添加了TDengine数据库配置和连接池管理 - 实现了MQTT客户端依赖注入和服务构建 - 创建了钻孔实时数据的ORM映射和SQL构建功能 - 实现了TDengine Writer用于数据写入超级表 - 添加了MQTT模拟服务,支持发布、订阅和数据转发功能 - 创建了随机数据发送器用于测试 - 实现了消息持久化到本地文件功能 - 配置了数据库连接池和SQL执行功能
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.venv
|
||||||
|
.idea
|
||||||
|
|
||||||
|
|
||||||
|
.pytest_cache
|
||||||
|
.tox
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.pytest_cache/
|
||||||
|
.pytest_
|
||||||
|
|
||||||
|
.vscode
|
||||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# MQTT Mock Service
|
||||||
|
|
||||||
|
Simulates a service that:
|
||||||
|
- Subscribes to the ingest topic (server -> broker)
|
||||||
|
- Writes received messages to a local file (JSONL)
|
||||||
|
- Writes received messages to TDengine (super table)
|
||||||
|
- Forwards messages to another topic for downstream subscribers
|
||||||
|
- Optionally sends ack messages
|
||||||
|
|
||||||
|
Also includes:
|
||||||
|
- A random data sender (every 3 seconds by default)
|
||||||
|
- A simple subscriber that prints incoming payloads
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
```
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run (service mode)
|
||||||
|
```
|
||||||
|
python mqtt_mock.py --config config.yaml --mode listen
|
||||||
|
```
|
||||||
|
|
||||||
|
When TDengine config exists in `config.yaml`, each message on `pub-topic` is inserted into:
|
||||||
|
- Super table: `drilling_realtime_st`
|
||||||
|
- Sub table: auto-generated as `drilling_realtime_<device_code>`
|
||||||
|
- Tag value: fixed `device_code` from config (`GJ-304-0088`)
|
||||||
|
|
||||||
|
## Run (also publish test data)
|
||||||
|
```
|
||||||
|
python mqtt_mock.py --config config.yaml --mode both --interval 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run random sender (3s interval)
|
||||||
|
```
|
||||||
|
python mqtt_sender.py --config config.yaml --interval 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run subscriber
|
||||||
|
```
|
||||||
|
python mqtt_subscriber.py --config config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Payload format (server -> broker)
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"test_id": "550e8400e29b41d4a716446655440000",
|
||||||
|
"equipment_sn": "GJ-304-0088"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"record_time": 1751964764000,
|
||||||
|
"wellid": "",
|
||||||
|
"stknum": 0,
|
||||||
|
"recid": 0,
|
||||||
|
"seqid": 0,
|
||||||
|
"actual_date": 0,
|
||||||
|
"actual_time": 0,
|
||||||
|
"actcod": 0,
|
||||||
|
"deptbitm": 0,
|
||||||
|
"deptbitv": 0,
|
||||||
|
"deptmeas": 0,
|
||||||
|
"deptvert": 0,
|
||||||
|
"blkpos": 0,
|
||||||
|
"ropa": 0,
|
||||||
|
"hkla": 0,
|
||||||
|
"hklx": 0,
|
||||||
|
"woba": 0,
|
||||||
|
"wobx": 0,
|
||||||
|
"torqa": 0,
|
||||||
|
"torqx": 0,
|
||||||
|
"rpma": 0,
|
||||||
|
"sppa": 0,
|
||||||
|
"chkp": 0,
|
||||||
|
"spm1": 0,
|
||||||
|
"spm2": 0,
|
||||||
|
"spm3": 0,
|
||||||
|
"tvolact": 0,
|
||||||
|
"tvolcact": 0,
|
||||||
|
"mfop": 0,
|
||||||
|
"mfoa": 0,
|
||||||
|
"mfia": 0,
|
||||||
|
"mdoa": 0,
|
||||||
|
"mdia": 0,
|
||||||
|
"mtoa": 0,
|
||||||
|
"mtia": 0,
|
||||||
|
"mcoa": 0,
|
||||||
|
"mcia": 0,
|
||||||
|
"stkc": 0,
|
||||||
|
"lagstks": 0,
|
||||||
|
"deptretm": 0,
|
||||||
|
"gasa": 0,
|
||||||
|
"space1": 0,
|
||||||
|
"space2": 0,
|
||||||
|
"space3": 0,
|
||||||
|
"space4": 0,
|
||||||
|
"space5": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
- `pub-topic`: ingest topic from server
|
||||||
|
- `sub-topic`: forward topic for other services to subscribe
|
||||||
|
- `ack-topic`: optional ack topic
|
||||||
|
- `data-file`: local append-only file (JSON Lines)
|
||||||
|
- `equipment-sn`: default equipment_sn for simulated payloads
|
||||||
|
- `test-id`: default test_id for simulated payloads
|
||||||
|
- `tdengine.url`: e.g. `jdbc:TAOS-RS://192.168.1.87:6041/tms`
|
||||||
|
- `tdengine.username`: DB user
|
||||||
|
- `tdengine.password`: DB password
|
||||||
|
- `tdengine.database`: DB name (optional if URL already has `/tms`)
|
||||||
|
- `tdengine.stable`: default `drilling_realtime_st`
|
||||||
|
|
||||||
|
## Options
|
||||||
|
- `--mode`: publish | listen | both
|
||||||
|
- `--interval`: publish interval seconds
|
||||||
|
- `--count`: publish count (0 = forever)
|
||||||
|
- `--data-file`: override data-file in config
|
||||||
29
config.yaml
Normal file
29
config.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
mqtt:
|
||||||
|
broker: tcp://192.168.1.87:1883
|
||||||
|
client-id: tms-client-dev
|
||||||
|
mock-client-id: tms-client-dev-mock
|
||||||
|
sender-client-id: tms-client-dev-sender
|
||||||
|
subscriber-client-id: tms-client-dev-subscriber
|
||||||
|
username: test
|
||||||
|
password: S55HtwFZvhf67VpS
|
||||||
|
# Server publishes here; mock service subscribes and ingests
|
||||||
|
pub-topic: rules_gj_jxjs_wtt
|
||||||
|
# Mock service forwards here for other subscribers
|
||||||
|
|
||||||
|
tms:
|
||||||
|
device-code: GJ-304-0088
|
||||||
|
equipment-sn: GJ-304-0088
|
||||||
|
timeout: 10
|
||||||
|
keepalive: 20
|
||||||
|
server-ip: 192.168.1.41
|
||||||
|
server-port: 9929
|
||||||
|
|
||||||
|
tdengine:
|
||||||
|
url: jdbc:TAOS-RS://192.168.1.87:6041/tms
|
||||||
|
username: root
|
||||||
|
password: wDvfFffdwbm5U15K
|
||||||
|
# If omitted, database is parsed from url path: /tms
|
||||||
|
database: tms
|
||||||
|
stable: drilling_realtime_st
|
||||||
|
device-code: GJ-304-0088
|
||||||
|
|
||||||
113
config/__init__.py
Normal file
113
config/__init__.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MqttConfig:
|
||||||
|
broker: str
|
||||||
|
client_id: str
|
||||||
|
mock_client_id: str
|
||||||
|
sender_client_id: str
|
||||||
|
subscriber_client_id: str
|
||||||
|
username: str | None
|
||||||
|
password: str | None
|
||||||
|
pub_topic: str | None
|
||||||
|
sub_topic: str | None
|
||||||
|
ack_topic: str | None
|
||||||
|
data_file: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TmsConfig:
|
||||||
|
device_code: str
|
||||||
|
equipment_sn: str
|
||||||
|
timeout: int
|
||||||
|
keepalive: int
|
||||||
|
server_ip: str | None
|
||||||
|
server_port: int | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AppConfig:
|
||||||
|
mqtt: MqttConfig
|
||||||
|
tms: TmsConfig
|
||||||
|
raw: dict
|
||||||
|
|
||||||
|
|
||||||
|
def load_raw_config(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_value(cfg, *paths, default=None):
|
||||||
|
for path in paths:
|
||||||
|
current = cfg
|
||||||
|
found = True
|
||||||
|
for key in path:
|
||||||
|
if not isinstance(current, dict) or key not in current:
|
||||||
|
found = False
|
||||||
|
break
|
||||||
|
current = current[key]
|
||||||
|
if found and current is not None:
|
||||||
|
return current
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def load_app_config(path):
|
||||||
|
raw = load_raw_config(path)
|
||||||
|
base_client_id = get_value(raw, ("mqtt", "client-id"), ("client-id",), default="mqtt")
|
||||||
|
device_code = get_value(
|
||||||
|
raw,
|
||||||
|
("tms", "device-code"),
|
||||||
|
("tms", "equipment-sn"),
|
||||||
|
("device-code",),
|
||||||
|
("equipment-sn",),
|
||||||
|
default="GJ-304-0088",
|
||||||
|
)
|
||||||
|
equipment_sn = get_value(
|
||||||
|
raw,
|
||||||
|
("tms", "equipment-sn"),
|
||||||
|
("tms", "device-code"),
|
||||||
|
("equipment-sn",),
|
||||||
|
("device-code",),
|
||||||
|
default=device_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return AppConfig(
|
||||||
|
mqtt=MqttConfig(
|
||||||
|
broker=get_value(raw, ("mqtt", "broker"), ("broker",), default=""),
|
||||||
|
client_id=base_client_id,
|
||||||
|
mock_client_id=get_value(raw, ("mqtt", "mock-client-id"), ("mock-client-id",), default=f"{base_client_id}-mock"),
|
||||||
|
sender_client_id=get_value(raw, ("mqtt", "sender-client-id"), ("sender-client-id",), default=f"{base_client_id}-sender"),
|
||||||
|
subscriber_client_id=get_value(raw, ("mqtt", "subscriber-client-id"), ("subscriber-client-id",), default=f"{base_client_id}-subscriber"),
|
||||||
|
username=get_value(raw, ("mqtt", "username"), ("username",)),
|
||||||
|
password=get_value(raw, ("mqtt", "password"), ("password",)),
|
||||||
|
pub_topic=get_value(raw, ("mqtt", "pub-topic"), ("pub-topic",)),
|
||||||
|
sub_topic=get_value(raw, ("mqtt", "sub-topic"), ("sub-topic",)),
|
||||||
|
ack_topic=get_value(raw, ("mqtt", "ack-topic"), ("ack-topic",)),
|
||||||
|
data_file=get_value(raw, ("mqtt", "data-file"), ("data-file",), default=""),
|
||||||
|
),
|
||||||
|
tms=TmsConfig(
|
||||||
|
device_code=device_code,
|
||||||
|
equipment_sn=equipment_sn,
|
||||||
|
timeout=int(get_value(raw, ("tms", "timeout"), ("timeout",), default=10)),
|
||||||
|
keepalive=int(get_value(raw, ("tms", "keepalive"), ("keepalive",), default=20)),
|
||||||
|
server_ip=get_value(raw, ("tms", "server-ip"), ("server-ip",)),
|
||||||
|
server_port=get_value(raw, ("tms", "server-port"), ("server-port",)),
|
||||||
|
),
|
||||||
|
raw=raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
load_config = load_app_config
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AppConfig",
|
||||||
|
"MqttConfig",
|
||||||
|
"TmsConfig",
|
||||||
|
"get_value",
|
||||||
|
"load_app_config",
|
||||||
|
"load_config",
|
||||||
|
"load_raw_config",
|
||||||
|
]
|
||||||
41
config/dependencies.py
Normal file
41
config/dependencies.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from config import load_app_config
|
||||||
|
from db import TDengineWriter, load_tdengine_config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SenderDependencies:
|
||||||
|
config: object
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SubscriberDependencies:
|
||||||
|
config: object
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MockDependencies:
|
||||||
|
config: object
|
||||||
|
tdengine_config: object
|
||||||
|
tdengine_writer: object
|
||||||
|
data_file: str
|
||||||
|
|
||||||
|
|
||||||
|
def build_sender_dependencies(config_path):
|
||||||
|
return SenderDependencies(config=load_app_config(config_path))
|
||||||
|
|
||||||
|
|
||||||
|
def build_subscriber_dependencies(config_path):
|
||||||
|
return SubscriberDependencies(config=load_app_config(config_path))
|
||||||
|
|
||||||
|
|
||||||
|
def build_mock_dependencies(config_path, data_file_override=""):
|
||||||
|
app_config = load_app_config(config_path)
|
||||||
|
tdengine_config = load_tdengine_config(app_config, default_device_code=app_config.tms.device_code)
|
||||||
|
return MockDependencies(
|
||||||
|
config=app_config,
|
||||||
|
tdengine_config=tdengine_config,
|
||||||
|
tdengine_writer=TDengineWriter(tdengine_config),
|
||||||
|
data_file=data_file_override or app_config.mqtt.data_file,
|
||||||
|
)
|
||||||
3
config_utils.py
Normal file
3
config_utils.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from config import get_value, load_config
|
||||||
|
|
||||||
|
__all__ = ["get_value", "load_config"]
|
||||||
51
createTable.sql
Normal file
51
createTable.sql
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
DROP TABLE IF EXISTS drilling_realtime_st;
|
||||||
|
|
||||||
|
CREATE STABLE drilling_realtime_st (
|
||||||
|
ts TIMESTAMP,
|
||||||
|
stknum INT,
|
||||||
|
recid INT,
|
||||||
|
seqid INT,
|
||||||
|
actual_date FLOAT,
|
||||||
|
actual_time FLOAT,
|
||||||
|
actcod INT,
|
||||||
|
deptbitm FLOAT,
|
||||||
|
deptbitv FLOAT,
|
||||||
|
deptmeas FLOAT,
|
||||||
|
deptvert FLOAT,
|
||||||
|
blkpos FLOAT,
|
||||||
|
ropa FLOAT,
|
||||||
|
hkla FLOAT,
|
||||||
|
hklx FLOAT,
|
||||||
|
woba FLOAT,
|
||||||
|
wobx FLOAT,
|
||||||
|
torqa FLOAT,
|
||||||
|
torqx FLOAT,
|
||||||
|
rpma INT,
|
||||||
|
sppa FLOAT,
|
||||||
|
chkp FLOAT,
|
||||||
|
spm1 INT,
|
||||||
|
spm2 INT,
|
||||||
|
spm3 INT,
|
||||||
|
tvolact FLOAT,
|
||||||
|
tvolcact FLOAT,
|
||||||
|
mfop INT,
|
||||||
|
mfoa FLOAT,
|
||||||
|
mfia FLOAT,
|
||||||
|
mdoa FLOAT,
|
||||||
|
mdia FLOAT,
|
||||||
|
mtoa FLOAT,
|
||||||
|
mtia FLOAT,
|
||||||
|
mcoa FLOAT,
|
||||||
|
mcia FLOAT,
|
||||||
|
stkc INT,
|
||||||
|
lagstks INT,
|
||||||
|
deptretm FLOAT,
|
||||||
|
gasa FLOAT,
|
||||||
|
space1 FLOAT,
|
||||||
|
space2 FLOAT,
|
||||||
|
space3 FLOAT,
|
||||||
|
space4 FLOAT,
|
||||||
|
space5 FLOAT
|
||||||
|
) TAGS (
|
||||||
|
equipment_code VARCHAR(50)
|
||||||
|
);
|
||||||
14
db/__init__.py
Normal file
14
db/__init__.py
Normal 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
65
db/config.py
Normal 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
122
db/orm.py
Normal 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
60
db/pool.py
Normal 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
20
db/writer.py
Normal 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)
|
||||||
1
install.bat
Normal file
1
install.bat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pip install pyyaml paho-mqtt
|
||||||
10
main.py
Normal file
10
main.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.info("start app....")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
255
mqtt_mock.py
Normal file
255
mqtt_mock.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from config.dependencies import build_mock_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
DATA_KEYS = [
|
||||||
|
"ts",
|
||||||
|
"wellid",
|
||||||
|
"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",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_broker(broker):
|
||||||
|
if not broker:
|
||||||
|
raise ValueError("broker is required")
|
||||||
|
if "://" not in broker:
|
||||||
|
broker = "tcp://" + broker
|
||||||
|
parsed = urlparse(broker)
|
||||||
|
host = parsed.hostname or "localhost"
|
||||||
|
port = parsed.port or 1883
|
||||||
|
scheme = (parsed.scheme or "tcp").lower()
|
||||||
|
return scheme, host, port
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dir(path):
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
dir_path = os.path.dirname(path)
|
||||||
|
if dir_path:
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def build_server_payload(equipment_code):
|
||||||
|
data = {key: 0 for key in DATA_KEYS}
|
||||||
|
data["ts"] = int(time.time() * 1000)
|
||||||
|
data["wellid"] = ""
|
||||||
|
return {
|
||||||
|
"meta": {
|
||||||
|
"equipment_code": equipment_code,
|
||||||
|
"equipment_sn": equipment_code,
|
||||||
|
},
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_payload(raw_payload):
|
||||||
|
try:
|
||||||
|
return json.loads(raw_payload)
|
||||||
|
except Exception:
|
||||||
|
return raw_payload
|
||||||
|
|
||||||
|
|
||||||
|
def build_ack_payload(topic, raw_payload):
|
||||||
|
payload = {
|
||||||
|
"ack": True,
|
||||||
|
"topic": topic,
|
||||||
|
"ts": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
obj = json.loads(raw_payload)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
meta = obj.get("meta") if isinstance(obj.get("meta"), dict) else {}
|
||||||
|
if "equipment_code" in meta:
|
||||||
|
payload["equipment_code"] = meta["equipment_code"]
|
||||||
|
if "equipment_sn" in meta:
|
||||||
|
payload["equipment_sn"] = meta["equipment_sn"]
|
||||||
|
if "id" in obj:
|
||||||
|
payload["id"] = obj["id"]
|
||||||
|
payload["src"] = obj
|
||||||
|
except Exception:
|
||||||
|
payload["raw"] = raw_payload
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def write_message(path, topic, raw_payload):
|
||||||
|
ensure_dir(path)
|
||||||
|
record = {
|
||||||
|
"ts": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||||
|
"topic": topic,
|
||||||
|
"payload": parse_payload(raw_payload),
|
||||||
|
}
|
||||||
|
payload_obj = record["payload"]
|
||||||
|
if isinstance(payload_obj, dict):
|
||||||
|
data = payload_obj.get("data")
|
||||||
|
if isinstance(data, dict) and isinstance(data.get("ts"), (int, float)):
|
||||||
|
record["ts_iso"] = datetime.utcfromtimestamp(data["ts"] / 1000.0).isoformat(timespec="seconds") + "Z"
|
||||||
|
with open(path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(record, ensure_ascii=True))
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def run_mock_service(args, deps):
|
||||||
|
mqtt_config = deps.config.mqtt
|
||||||
|
tms_config = deps.config.tms
|
||||||
|
tdengine_config = deps.tdengine_config
|
||||||
|
tdengine_writer = deps.tdengine_writer
|
||||||
|
scheme, host, port = parse_broker(mqtt_config.broker)
|
||||||
|
|
||||||
|
print("MQTT mock config:")
|
||||||
|
print(f" broker: {scheme}://{host}:{port}")
|
||||||
|
print(f" client-id: {mqtt_config.mock_client_id}")
|
||||||
|
print(f" pub-topic (ingest): {mqtt_config.pub_topic}")
|
||||||
|
print(f" sub-topic (forward): {mqtt_config.sub_topic}")
|
||||||
|
print(f" ack-topic: {mqtt_config.ack_topic}")
|
||||||
|
print(f" data-file: {deps.data_file}")
|
||||||
|
print(f" device-code: {tms_config.device_code}")
|
||||||
|
print(f" tdengine enabled: {tdengine_writer.enabled}")
|
||||||
|
if tdengine_writer.enabled:
|
||||||
|
print(f" tdengine host: {tdengine_config.base_url}")
|
||||||
|
print(f" tdengine database: {tdengine_config.database}")
|
||||||
|
print(f" tdengine stable: {tdengine_config.stable}")
|
||||||
|
print(f" tdengine pool-size: {tdengine_config.pool_size}")
|
||||||
|
print(f" mode: {args.mode}")
|
||||||
|
|
||||||
|
client = mqtt.Client(client_id=mqtt_config.mock_client_id, clean_session=True)
|
||||||
|
if mqtt_config.username is not None:
|
||||||
|
client.username_pw_set(mqtt_config.username, mqtt_config.password)
|
||||||
|
|
||||||
|
if scheme in ("ssl", "tls", "mqtts"):
|
||||||
|
client.tls_set()
|
||||||
|
|
||||||
|
def on_connect(c, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected")
|
||||||
|
else:
|
||||||
|
print(f"Connect failed rc={rc}")
|
||||||
|
return
|
||||||
|
if args.mode in ("listen", "both") and mqtt_config.pub_topic:
|
||||||
|
c.subscribe(mqtt_config.pub_topic)
|
||||||
|
print(f"Subscribed: {mqtt_config.pub_topic}")
|
||||||
|
|
||||||
|
def on_disconnect(c, userdata, rc):
|
||||||
|
print(f"Disconnected callback rc={rc}")
|
||||||
|
|
||||||
|
def on_message(c, userdata, msg):
|
||||||
|
payload = msg.payload.decode("utf-8", errors="replace")
|
||||||
|
print(f"RX {msg.topic}: {payload}")
|
||||||
|
if msg.topic != mqtt_config.pub_topic:
|
||||||
|
return
|
||||||
|
|
||||||
|
if deps.data_file:
|
||||||
|
write_message(deps.data_file, msg.topic, payload)
|
||||||
|
print(f"Wrote to file: {deps.data_file}")
|
||||||
|
|
||||||
|
if tdengine_writer.enabled:
|
||||||
|
try:
|
||||||
|
tdengine_writer.write_payload(parse_payload(payload))
|
||||||
|
print("Wrote to TDengine")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Write TDengine failed: {exc}")
|
||||||
|
|
||||||
|
if mqtt_config.sub_topic:
|
||||||
|
c.publish(mqtt_config.sub_topic, payload)
|
||||||
|
print(f"Forwarded to {mqtt_config.sub_topic}")
|
||||||
|
|
||||||
|
if mqtt_config.ack_topic:
|
||||||
|
ack_payload = build_ack_payload(msg.topic, payload)
|
||||||
|
c.publish(mqtt_config.ack_topic, json.dumps(ack_payload, ensure_ascii=True))
|
||||||
|
print(f"TX {mqtt_config.ack_topic}: {ack_payload}")
|
||||||
|
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_disconnect = on_disconnect
|
||||||
|
client.on_message = on_message
|
||||||
|
client.connect(host, port, keepalive=tms_config.keepalive)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.mode in ("publish", "both"):
|
||||||
|
if not mqtt_config.pub_topic:
|
||||||
|
print("pub-topic is empty; nothing to publish")
|
||||||
|
else:
|
||||||
|
seq = 0
|
||||||
|
while True:
|
||||||
|
seq += 1
|
||||||
|
payload = build_server_payload(tms_config.device_code)
|
||||||
|
client.publish(mqtt_config.pub_topic, json.dumps(payload, ensure_ascii=True))
|
||||||
|
print(f"TX {mqtt_config.pub_topic}: {payload}")
|
||||||
|
if args.count and seq >= args.count:
|
||||||
|
break
|
||||||
|
time.sleep(args.interval)
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
client.loop_stop()
|
||||||
|
client.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="MQTT mock service")
|
||||||
|
ap.add_argument("--config", default="config.yaml", help="Path to config yaml")
|
||||||
|
ap.add_argument("--mode", choices=["publish", "listen", "both"], default="listen")
|
||||||
|
ap.add_argument("--interval", type=float, default=2.0, help="Publish interval (seconds)")
|
||||||
|
ap.add_argument("--count", type=int, default=0, help="Publish count (0 = forever)")
|
||||||
|
ap.add_argument("--data-file", default="", help="Override data-file in config")
|
||||||
|
args = ap.parse_args()
|
||||||
|
deps = build_mock_dependencies(args.config, data_file_override=args.data_file)
|
||||||
|
run_mock_service(args, deps)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
193
mqtt_sender.py
Normal file
193
mqtt_sender.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from config.dependencies import build_sender_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
DATA_KEYS = [
|
||||||
|
"ts",
|
||||||
|
"wellid",
|
||||||
|
"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",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_broker(broker):
|
||||||
|
if not broker:
|
||||||
|
raise ValueError("broker is required")
|
||||||
|
if "://" not in broker:
|
||||||
|
broker = "tcp://" + broker
|
||||||
|
parsed = urlparse(broker)
|
||||||
|
host = parsed.hostname or "localhost"
|
||||||
|
port = parsed.port or 1883
|
||||||
|
scheme = (parsed.scheme or "tcp").lower()
|
||||||
|
return scheme, host, port
|
||||||
|
|
||||||
|
|
||||||
|
def rand_int(a, b):
|
||||||
|
return random.randint(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
def build_random_payload(equipment_code):
|
||||||
|
data = {key: 0 for key in DATA_KEYS}
|
||||||
|
data["ts"] = int(time.time() * 1000)
|
||||||
|
data["wellid"] = random.choice(["", f"WELL-{rand_int(1, 9999):04d}"])
|
||||||
|
data["stknum"] = rand_int(0, 500)
|
||||||
|
data["recid"] = rand_int(0, 100000)
|
||||||
|
data["seqid"] = rand_int(0, 100000)
|
||||||
|
data["actual_date"] = int(datetime.utcnow().strftime("%Y%m%d"))
|
||||||
|
data["actual_time"] = int(datetime.utcnow().strftime("%H%M%S"))
|
||||||
|
data["actcod"] = rand_int(0, 9)
|
||||||
|
data["deptbitm"] = rand_int(0, 5000)
|
||||||
|
data["deptbitv"] = rand_int(0, 5000)
|
||||||
|
data["deptmeas"] = rand_int(0, 5000)
|
||||||
|
data["deptvert"] = rand_int(0, 5000)
|
||||||
|
data["blkpos"] = rand_int(0, 100)
|
||||||
|
data["ropa"] = rand_int(0, 200)
|
||||||
|
data["hkla"] = rand_int(0, 500)
|
||||||
|
data["hklx"] = rand_int(0, 500)
|
||||||
|
data["woba"] = rand_int(0, 200)
|
||||||
|
data["wobx"] = rand_int(0, 200)
|
||||||
|
data["torqa"] = rand_int(0, 200)
|
||||||
|
data["torqx"] = rand_int(0, 200)
|
||||||
|
data["rpma"] = rand_int(0, 300)
|
||||||
|
data["sppa"] = rand_int(0, 5000)
|
||||||
|
data["chkp"] = rand_int(0, 5000)
|
||||||
|
data["spm1"] = rand_int(0, 200)
|
||||||
|
data["spm2"] = rand_int(0, 200)
|
||||||
|
data["spm3"] = rand_int(0, 200)
|
||||||
|
data["tvolact"] = rand_int(0, 20000)
|
||||||
|
data["tvolcact"] = rand_int(0, 20000)
|
||||||
|
data["mfop"] = rand_int(0, 1000)
|
||||||
|
data["mfoa"] = rand_int(0, 1000)
|
||||||
|
data["mfia"] = rand_int(0, 1000)
|
||||||
|
data["mdoa"] = rand_int(0, 1000)
|
||||||
|
data["mdia"] = rand_int(0, 1000)
|
||||||
|
data["mtoa"] = rand_int(0, 1000)
|
||||||
|
data["mtia"] = rand_int(0, 1000)
|
||||||
|
data["mcoa"] = rand_int(0, 1000)
|
||||||
|
data["mcia"] = rand_int(0, 1000)
|
||||||
|
data["stkc"] = rand_int(0, 200)
|
||||||
|
data["lagstks"] = rand_int(0, 200)
|
||||||
|
data["deptretm"] = rand_int(0, 5000)
|
||||||
|
data["gasa"] = rand_int(0, 100)
|
||||||
|
data["space1"] = rand_int(0, 10)
|
||||||
|
data["space2"] = rand_int(0, 10)
|
||||||
|
data["space3"] = rand_int(0, 10)
|
||||||
|
data["space4"] = rand_int(0, 10)
|
||||||
|
data["space5"] = rand_int(0, 10)
|
||||||
|
return {
|
||||||
|
"meta": {
|
||||||
|
"equipment_code": equipment_code,
|
||||||
|
"equipment_sn": equipment_code,
|
||||||
|
},
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_sender(args, deps):
|
||||||
|
mqtt_config = deps.config.mqtt
|
||||||
|
tms_config = deps.config.tms
|
||||||
|
scheme, host, port = parse_broker(mqtt_config.broker)
|
||||||
|
|
||||||
|
print("MQTT sender config:")
|
||||||
|
print(f" broker: {scheme}://{host}:{port}")
|
||||||
|
print(f" client-id: {mqtt_config.sender_client_id}")
|
||||||
|
print(f" pub-topic: {mqtt_config.pub_topic}")
|
||||||
|
print(f" interval: {args.interval}s")
|
||||||
|
|
||||||
|
client = mqtt.Client(client_id=mqtt_config.sender_client_id, clean_session=True)
|
||||||
|
if mqtt_config.username is not None:
|
||||||
|
client.username_pw_set(mqtt_config.username, mqtt_config.password)
|
||||||
|
|
||||||
|
if scheme in ("ssl", "tls", "mqtts"):
|
||||||
|
client.tls_set()
|
||||||
|
|
||||||
|
def on_disconnect(c, userdata, rc):
|
||||||
|
print(f"Disconnected callback rc={rc}")
|
||||||
|
|
||||||
|
client.on_disconnect = on_disconnect
|
||||||
|
client.connect(host, port, keepalive=tms_config.keepalive)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not mqtt_config.pub_topic:
|
||||||
|
print("pub-topic is empty; nothing to publish")
|
||||||
|
return
|
||||||
|
seq = 0
|
||||||
|
while True:
|
||||||
|
seq += 1
|
||||||
|
payload = build_random_payload(tms_config.device_code)
|
||||||
|
client.publish(mqtt_config.pub_topic, json.dumps(payload, ensure_ascii=True))
|
||||||
|
print(f"TX {mqtt_config.pub_topic}: {payload}")
|
||||||
|
if args.count and seq >= args.count:
|
||||||
|
break
|
||||||
|
time.sleep(args.interval)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
client.loop_stop()
|
||||||
|
client.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="MQTT random data sender")
|
||||||
|
ap.add_argument("--config", default="config.yaml", help="Path to config yaml")
|
||||||
|
ap.add_argument("--interval", type=float, default=3.0, help="Publish interval (seconds)")
|
||||||
|
ap.add_argument("--count", type=int, default=0, help="Publish count (0 = forever)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
deps = build_sender_dependencies(args.config)
|
||||||
|
run_sender(args, deps)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
112
mqtt_subscriber.py
Normal file
112
mqtt_subscriber.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from config.dependencies import build_subscriber_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
def parse_broker(broker):
|
||||||
|
if not broker:
|
||||||
|
raise ValueError("broker is required")
|
||||||
|
if "://" not in broker:
|
||||||
|
broker = "tcp://" + broker
|
||||||
|
parsed = urlparse(broker)
|
||||||
|
host = parsed.hostname or "localhost"
|
||||||
|
port = parsed.port or 1883
|
||||||
|
scheme = (parsed.scheme or "tcp").lower()
|
||||||
|
return scheme, host, port
|
||||||
|
|
||||||
|
|
||||||
|
def print_flat_fields(title, value):
|
||||||
|
print(title)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for k, v in value.items():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
else:
|
||||||
|
print(f" {value}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_subscriber(args, deps):
|
||||||
|
mqtt_config = deps.config.mqtt
|
||||||
|
tms_config = deps.config.tms
|
||||||
|
topic = args.topic or mqtt_config.sub_topic or mqtt_config.pub_topic
|
||||||
|
if not topic:
|
||||||
|
raise ValueError("No topic to subscribe. Set sub-topic or pub-topic, or pass --topic")
|
||||||
|
|
||||||
|
scheme, host, port = parse_broker(mqtt_config.broker)
|
||||||
|
|
||||||
|
print("MQTT subscriber config:")
|
||||||
|
print(f" broker: {scheme}://{host}:{port}")
|
||||||
|
print(f" client-id: {mqtt_config.subscriber_client_id}")
|
||||||
|
print(f" topic: {topic}")
|
||||||
|
|
||||||
|
client = mqtt.Client(client_id=mqtt_config.subscriber_client_id, clean_session=True)
|
||||||
|
if mqtt_config.username is not None:
|
||||||
|
client.username_pw_set(mqtt_config.username, mqtt_config.password)
|
||||||
|
|
||||||
|
if scheme in ("ssl", "tls", "mqtts"):
|
||||||
|
client.tls_set()
|
||||||
|
|
||||||
|
def on_connect(c, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected")
|
||||||
|
c.subscribe(topic)
|
||||||
|
print(f"Subscribed: {topic}")
|
||||||
|
else:
|
||||||
|
print(f"Connect failed rc={rc}")
|
||||||
|
|
||||||
|
def on_disconnect(c, userdata, rc):
|
||||||
|
print(f"Disconnected callback rc={rc}")
|
||||||
|
|
||||||
|
def on_message(c, userdata, msg):
|
||||||
|
raw_payload = msg.payload.decode("utf-8", errors="replace")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"RX Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"Topic: {msg.topic}")
|
||||||
|
print(f"QoS: {msg.qos}, Retain: {msg.retain}, Bytes: {len(msg.payload)}")
|
||||||
|
try:
|
||||||
|
obj = json.loads(raw_payload)
|
||||||
|
print("Raw JSON:")
|
||||||
|
print(json.dumps(obj, ensure_ascii=False, indent=2))
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
print_flat_fields("meta:", obj.get("meta"))
|
||||||
|
print_flat_fields("data:", obj.get("data"))
|
||||||
|
extra_keys = [key for key in obj.keys() if key not in ("meta", "data")]
|
||||||
|
for key in extra_keys:
|
||||||
|
print_flat_fields(f"{key}:", obj.get(key))
|
||||||
|
except Exception:
|
||||||
|
print("Raw payload:")
|
||||||
|
print(raw_payload)
|
||||||
|
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_disconnect = on_disconnect
|
||||||
|
client.on_message = on_message
|
||||||
|
client.connect(host, port, keepalive=tms_config.keepalive)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
client.loop_stop()
|
||||||
|
client.disconnect()
|
||||||
|
print("Disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="MQTT subscriber")
|
||||||
|
ap.add_argument("--config", default="config.yaml", help="Path to config yaml")
|
||||||
|
ap.add_argument("--topic", default="", help="Override topic to subscribe")
|
||||||
|
args = ap.parse_args()
|
||||||
|
deps = build_subscriber_dependencies(args.config)
|
||||||
|
run_subscriber(args, deps)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
paho-mqtt>=1.6.0
|
||||||
|
PyYAML>=6.0
|
||||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
Reference in New Issue
Block a user