修改了事件处理逻辑

This commit is contained in:
root 2026-06-26 17:48:42 +08:00
parent 6937c5bfdd
commit 00fe92c90e
14 changed files with 391 additions and 337 deletions

View File

@ -15,11 +15,13 @@ from app.schemas.platform import (
DeviceStatus,
LineAlarmSettingIn,
LineData,
NetConfigItem,
RealtimeData,
SystemConfigIn,
SwitchControlIn,
TimeSyncConfigIn,
ValueGroup,
UartConfigItem,
)
@ -56,6 +58,10 @@ class DeviceClient(ABC):
def read_time_sync_config(self) -> Dict[str, str]:
raise NotImplementedError
@abstractmethod
def read_net_config(self, nic: str) -> Dict[str, Any]:
raise NotImplementedError
@abstractmethod
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
raise NotImplementedError
@ -80,6 +86,10 @@ class DeviceClient(ABC):
def send_time_sync_config(self, payload: TimeSyncConfigIn) -> Dict[str, Any]:
raise NotImplementedError
@abstractmethod
def send_net_config(self, payload: NetConfigItem) -> Dict[str, Any]:
raise NotImplementedError
@abstractmethod
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
raise NotImplementedError
@ -131,6 +141,9 @@ AO_CHANNEL_START_REGISTER = 0x0072
AI_ALARM_START_REGISTER = 0x0090
ANALOG_BLOCK_REGISTER_COUNT = 30
DO_STATE_REGISTER = 0x00A6
NET_CONFIG_START_REGISTER = 0x00AE
NET_CONFIG_REGISTER_COUNT = 28
NET_ITEM_REGISTER_COUNT = 7
TIME_SYNC_START_REGISTER = 0x0000
@ -329,6 +342,12 @@ class MockDeviceClient(DeviceClient):
}
]
self._system_config = {"time_sync": "2026-05-25 12:34:56", "brightness": 80, "screen_saver": 60}
self._net_config = [
{"nic": "网卡一", "ip": "192.168.1.10", "mask": "255.255.255.0", "gateway": "192.168.1.1", "protocol": "Modbus TCP"},
{"nic": "网卡二", "ip": "192.168.2.10", "mask": "255.255.255.0", "gateway": "192.168.2.1", "protocol": "Modbus TCP"},
{"nic": "网卡三", "ip": "", "mask": "", "gateway": "", "protocol": ""},
{"nic": "网卡四", "ip": "", "mask": "", "gateway": "", "protocol": ""},
]
def _value_group(self, base_u: float, base_i: float, base_p: float) -> ValueGroup:
delta = self._tick % 5
@ -386,14 +405,15 @@ class MockDeviceClient(DeviceClient):
def read_alarm_events(self) -> List[AlarmEvent]:
if self._tick % 6 != 0:
return []
line_code = (self._tick // 6) % 4 + 1
return [
AlarmEvent(
alarm_type="line_alarm",
time=datetime.now(),
no=str((self._tick // 6) % 4 + 1),
type="电压",
content="模拟告警:线路电压越限",
level="",
event_time=datetime.now(),
line_code=line_code,
event_type=3,
event_code=1,
event_value=220,
content=f"线路{line_code}电压越限,动作值=220",
)
]
@ -428,6 +448,12 @@ class MockDeviceClient(DeviceClient):
def read_time_sync_config(self) -> Dict[str, str]:
return {"time_sync": str(self._system_config.get("time_sync", ""))}
def read_net_config(self, nic: str) -> Dict[str, Any]:
for item in self._net_config:
if item["nic"] == nic:
return dict(item)
return {"nic": nic, "ip": "", "mask": "", "gateway": "", "protocol": ""}
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
return {"send_status": "成功", "target": "device", "items": len(payload.net) + len(payload.uart)}
@ -455,6 +481,17 @@ class MockDeviceClient(DeviceClient):
self._system_config["time_sync"] = payload.time_sync
return {"send_status": "成功", "target": "time_sync", "time_sync": payload.time_sync}
def send_net_config(self, payload: NetConfigItem) -> Dict[str, Any]:
updated = False
for index, item in enumerate(self._net_config):
if item["nic"] == payload.nic:
self._net_config[index] = payload.model_dump()
updated = True
break
if not updated:
self._net_config.append(payload.model_dump())
return {"send_status": "成功", "target": "net", "nic": payload.nic, "modbus_written": {"net_registers": NET_CONFIG_REGISTER_COUNT}}
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
action_text = "" if payload.action == 1 else ""
return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"}
@ -607,6 +644,67 @@ class CDeviceClient(DeviceClient):
def _protocol_code(self, protocol: str) -> int:
return 1 if "MODBUS" in _normalize_text(protocol) else 0
def _decode_net_protocol(self, code: int) -> str:
return "Modbus TCP" if int(code) == 1 else ""
def _net_nic_names(self) -> List[str]:
return ["网卡一", "网卡二", "网卡三", "网卡四"]
def _net_index(self, nic: str) -> int:
normalized = nic.strip()
nic_names = self._net_nic_names()
if normalized in nic_names:
return nic_names.index(normalized)
digits = "".join(character for character in normalized if character.isdigit())
if digits:
index = max(1, min(4, int(digits))) - 1
return index
chinese_map = {"": 0, "": 1, "": 2, "": 3, "1": 0, "2": 1, "3": 2, "4": 3}
for key, index in chinese_map.items():
if key in normalized:
return index
return 0
def _ip_text_to_bytes(self, value: str) -> bytes:
parts = [part.strip() for part in str(value).split(".") if part.strip() != ""]
numbers = []
for part in parts[:4]:
try:
numbers.append(max(0, min(255, int(part))))
except ValueError:
numbers.append(0)
while len(numbers) < 4:
numbers.append(0)
return bytes(numbers[:4])
def _ip_bytes_to_text(self, value: bytes) -> str:
if len(value) < 4:
value = value + b"\x00" * (4 - len(value))
if all(int(part) == 0 for part in value[:4]):
return ""
return ".".join(str(int(part)) for part in value[:4])
def _decode_net_item(self, nic: str, payload: bytes) -> Dict[str, Any]:
chunk = payload[:14] + b"\x00" * max(0, 14 - len(payload))
return {
"nic": nic,
"ip": self._ip_bytes_to_text(chunk[0:4]),
"mask": self._ip_bytes_to_text(chunk[4:8]),
"gateway": self._ip_bytes_to_text(chunk[8:12]),
"protocol": self._decode_net_protocol(chunk[12]),
}
def _encode_net_item(self, item: NetConfigItem) -> bytes:
return b"".join(
[
self._ip_text_to_bytes(item.ip),
self._ip_text_to_bytes(item.mask),
self._ip_text_to_bytes(item.gateway),
bytes([self._protocol_code(item.protocol)]),
b"\x00",
]
)
def _line_rule_payload(self, limit: Any, delay: Any, output_node: str, enabled: bool) -> bytes:
return bytes(
[
@ -789,12 +887,12 @@ class CDeviceClient(DeviceClient):
line_text = "装置" if line_no == 0 else f"线路{line_no}"
alarms.append(
AlarmEvent(
alarm_type="modbus_event",
time=event_time,
no="" if line_no == 0 else str(line_no),
type=event_name,
event_time=event_time,
line_code=line_no,
event_type=event_type,
event_code=event_code,
event_value=action_value,
content=f"{line_text}{event_name},动作值={action_value}",
level=self._decode_event_level(event_type),
)
)
@ -817,9 +915,6 @@ class CDeviceClient(DeviceClient):
return f"AI越限{event_code}"
return f"事件{event_type}-{event_code}"
def _decode_event_level(self, event_type: int) -> str:
return {1: "严重", 2: "", 3: "", 4: "一般", 5: "一般"}.get(event_type, "一般")
def read_channel_config(self) -> Dict[str, Any]:
ai_registers = self.transport.read_holding_registers(AI_CHANNEL_START_REGISTER, ANALOG_BLOCK_REGISTER_COUNT)
ao_registers = self.transport.read_holding_registers(AO_CHANNEL_START_REGISTER, ANALOG_BLOCK_REGISTER_COUNT)
@ -849,6 +944,13 @@ class CDeviceClient(DeviceClient):
timestamp = int(registers[0]) | (int(registers[1]) << 16)
return {"time_sync": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")}
def read_net_config(self, nic: str) -> Dict[str, Any]:
registers = self.transport.read_holding_registers(NET_CONFIG_START_REGISTER, NET_CONFIG_REGISTER_COUNT)
payload = self._registers_to_bytes(registers)
index = self._net_index(nic)
start = index * 14
return self._decode_net_item(self._net_nic_names()[index], payload[start : start + 14])
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
uart_map = {item.port.upper(): item for item in payload.uart}
raw = bytearray()
@ -979,6 +1081,24 @@ class CDeviceClient(DeviceClient):
"modbus_written": {"time_registers": len(registers)},
}
def send_net_config(self, payload: NetConfigItem) -> Dict[str, Any]:
registers = self.transport.read_holding_registers(NET_CONFIG_START_REGISTER, NET_CONFIG_REGISTER_COUNT)
raw = bytearray(self._registers_to_bytes(registers))
expected_size = NET_CONFIG_REGISTER_COUNT * 2
if len(raw) < expected_size:
raw.extend(b"\x00" * (expected_size - len(raw)))
index = self._net_index(payload.nic)
start = index * 14
raw[start : start + 14] = self._encode_net_item(payload)
encoded = self._bytes_to_registers(bytes(raw))
self.transport.write_registers(NET_CONFIG_START_REGISTER, encoded)
return {
"send_status": "成功",
"target": "net",
"nic": self._net_nic_names()[index],
"modbus_written": {"start_register": NET_CONFIG_START_REGISTER, "registers": len(encoded)},
}
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
coil_address = max(0, payload.ch - 1)
coil_value = bool(payload.action)

View File

@ -14,16 +14,16 @@ router = APIRouter(prefix="/alarm", tags=["alarm"])
def list_alarms(
page: int = Query(default=1, ge=1),
size: int = Query(default=20, ge=1, le=100),
no: Optional[str] = Query(default=None),
type: Optional[str] = Query(default=None),
line_code: Optional[int] = Query(default=None, ge=0),
event_type: Optional[int] = Query(default=None, ge=0),
start_time: Optional[datetime] = Query(default=None),
end_time: Optional[datetime] = Query(default=None),
) -> dict:
data = platform_service.list_alarms(
page=page,
size=size,
no=no,
alarm_type=type,
line_code=line_code,
event_type=event_type,
start_time=start_time,
end_time=end_time,
)

View File

@ -15,19 +15,78 @@ def get_connection(db_path: Optional[Path] = None) -> sqlite3.Connection:
return connection
def _alarm_event_columns(connection: sqlite3.Connection) -> list[str]:
rows = connection.execute("PRAGMA table_info(alarm_event)").fetchall()
return [str(row["name"]) for row in rows]
def _ensure_alarm_event_schema(connection: sqlite3.Connection) -> None:
expected_columns = [
"id",
"event_time",
"line_code",
"event_type",
"event_code",
"event_value",
"content",
]
current_columns = _alarm_event_columns(connection)
if not current_columns:
return
if current_columns == expected_columns:
return
connection.execute("ALTER TABLE alarm_event RENAME TO alarm_event_legacy")
connection.execute(
"""
CREATE TABLE alarm_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_time TEXT NOT NULL,
line_code INTEGER NOT NULL DEFAULT 0,
event_type INTEGER NOT NULL DEFAULT 0,
event_code INTEGER NOT NULL DEFAULT 0,
event_value INTEGER NOT NULL DEFAULT 0,
content TEXT NOT NULL
)
"""
)
legacy_columns = set(current_columns)
if {"time", "content"}.issubset(legacy_columns):
connection.execute(
"""
INSERT INTO alarm_event (event_time, line_code, event_type, event_code, event_value, content)
SELECT
time,
CASE
WHEN typeof(no) IN ('integer', 'real') THEN CAST(no AS INTEGER)
WHEN trim(COALESCE(no, '')) GLOB '[0-9]*' AND trim(COALESCE(no, '')) <> '' THEN CAST(trim(no) AS INTEGER)
ELSE 0
END,
0,
0,
0,
content
FROM alarm_event_legacy
"""
)
connection.execute("DROP TABLE alarm_event_legacy")
def init_db() -> None:
with get_connection() as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS alarm_event (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alarm_type TEXT NOT NULL,
time TEXT NOT NULL,
no TEXT,
type TEXT,
content TEXT NOT NULL,
level TEXT NOT NULL
event_time TEXT NOT NULL,
line_code INTEGER NOT NULL DEFAULT 0,
event_type INTEGER NOT NULL DEFAULT 0,
event_code INTEGER NOT NULL DEFAULT 0,
event_value INTEGER NOT NULL DEFAULT 0,
content TEXT NOT NULL
)
"""
)
_ensure_alarm_event_schema(connection)
connection.commit()

View File

@ -12,16 +12,16 @@ class AlarmRepository:
with get_connection() as connection:
cursor = connection.execute(
"""
INSERT INTO alarm_event (alarm_type, time, no, type, content, level)
INSERT INTO alarm_event (event_time, line_code, event_type, event_code, event_value, content)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
alarm.alarm_type,
alarm.time.isoformat(sep=" ", timespec="seconds"),
alarm.no,
alarm.type,
alarm.event_time.isoformat(sep=" ", timespec="seconds"),
alarm.line_code,
alarm.event_type,
alarm.event_code,
alarm.event_value,
alarm.content,
alarm.level,
),
)
connection.commit()
@ -31,8 +31,8 @@ class AlarmRepository:
self,
page: int = 1,
size: int = 20,
no: str = "",
alarm_type: str = "",
line_code: Optional[int] = None,
event_type: Optional[int] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
@ -40,17 +40,17 @@ class AlarmRepository:
conditions = []
params: List[Any] = []
if no:
conditions.append("no = ?")
params.append(no)
if alarm_type:
conditions.append("type = ?")
params.append(alarm_type)
if line_code is not None:
conditions.append("line_code = ?")
params.append(line_code)
if event_type is not None:
conditions.append("event_type = ?")
params.append(event_type)
if start_time is not None:
conditions.append("time >= ?")
conditions.append("event_time >= ?")
params.append(start_time.isoformat(sep=" ", timespec="seconds"))
if end_time is not None:
conditions.append("time <= ?")
conditions.append("event_time <= ?")
params.append(end_time.isoformat(sep=" ", timespec="seconds"))
where_clause = ""
@ -60,7 +60,7 @@ class AlarmRepository:
with get_connection() as connection:
cursor = connection.execute(
f"""
SELECT id, alarm_type, time, no, type, content, level
SELECT id, event_time, line_code, event_type, event_code, event_value, content
FROM alarm_event
{where_clause}
ORDER BY id DESC

View File

@ -165,12 +165,12 @@ class DevicePasswordUpdateIn(BaseModel):
class AlarmEvent(BaseModel):
id: Optional[int] = None
alarm_type: str
time: datetime
no: str = ""
type: str = ""
event_time: datetime
line_code: int = 0
event_type: int = 0
event_code: int = 0
event_value: int = 0
content: str
level: str = "一般"
class SwitchControlIn(BaseModel):

View File

@ -350,17 +350,10 @@ class PlatformService:
return data
def get_net_config(self, nic: str) -> Dict[str, Any]:
device_config = self._load_device_config_with_defaults()
item = self._find_keyed_item(device_config["net"], "nic", nic)
if item is not None:
return item
return {"nic": nic, "ip": "", "mask": "", "gateway": "", "protocol": ""}
return self.device_client.read_net_config(nic)
def save_net_config(self, payload: NetConfigItem) -> Dict[str, Any]:
device_config = self._load_device_config_with_defaults()
device_config["net"] = self._upsert_keyed_item(device_config["net"], payload.model_dump(), "nic")
path = self.config_repo.write_device_config(device_config)
return {"save_path": f"/config/{path.name}", "target": "net", "nic": payload.nic, "send_status": "成功"}
return self.device_client.send_net_config(payload)
def get_uart_config(self, port: str) -> Dict[str, Any]:
device_config = self._load_device_config_with_defaults()
@ -456,16 +449,16 @@ class PlatformService:
self,
page: int,
size: int,
no: str = "",
alarm_type: str = "",
line_code: Optional[int] = None,
event_type: Optional[int] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
) -> List[Dict[str, Any]]:
return self.alarm_repo.list_alarms(
page=page,
size=size,
no=no,
alarm_type=alarm_type,
line_code=line_code,
event_type=event_type,
start_time=start_time,
end_time=end_time,
)

View File

@ -13,7 +13,7 @@
"net": [
{
"nic": "网卡一",
"ip": "192.168.1.10",
"ip": "192.168.1.11",
"mask": "255.255.255.1",
"gateway": "192.168.1.1",
"protocol": "Modbus TCP"
@ -29,7 +29,7 @@
"uart": [
{
"port": "COM1",
"baud": 19200,
"baud": 9600,
"parity": "NONE",
"data_bits": 8,
"stop_bits": 1,

View File

@ -74,8 +74,9 @@ def print_alarm_summary(client: CDeviceClient) -> None:
else:
for alarm in alarms:
print(
f"time={alarm.time}, no={alarm.no}, type={alarm.type}, "
f"level={alarm.level}, content={alarm.content}"
f"event_time={alarm.event_time}, line_code={alarm.line_code}, "
f"event_type={alarm.event_type}, event_code={alarm.event_code}, "
f"event_value={alarm.event_value}, content={alarm.content}"
)
print()

View File

@ -90,22 +90,22 @@ def test_alarm_list_filters(tmp_path) -> None:
repo = AlarmRepository()
repo.save_alarm(
AlarmEvent(
alarm_type="保护报警",
time=datetime(2026, 5, 16, 10, 0, 0),
no="L1",
type="voltage",
event_time=datetime(2026, 5, 16, 10, 0, 0),
line_code=1,
event_type=3,
event_code=1,
event_value=230,
content="A 相过压",
level="严重",
)
)
repo.save_alarm(
AlarmEvent(
alarm_type="状态报警",
time=datetime(2026, 5, 16, 11, 0, 0),
no="L2",
type="current",
event_time=datetime(2026, 5, 16, 11, 0, 0),
line_code=2,
event_type=5,
event_code=2,
event_value=456,
content="B 相过流",
level="一般",
)
)
@ -114,8 +114,8 @@ def test_alarm_list_filters(tmp_path) -> None:
params={
"page": 1,
"size": 20,
"no": "L2",
"type": "current",
"line_code": 2,
"event_type": 5,
"start_time": "2026-05-16T10:30:00",
"end_time": "2026-05-16T11:30:00",
},
@ -125,8 +125,10 @@ def test_alarm_list_filters(tmp_path) -> None:
payload = response.json()
assert payload["code"] == 200
assert len(payload["data"]) == 1
assert payload["data"][0]["no"] == "L2"
assert payload["data"][0]["type"] == "current"
assert payload["data"][0]["line_code"] == 2
assert payload["data"][0]["event_type"] == 5
assert payload["data"][0]["event_code"] == 2
assert payload["data"][0]["event_value"] == 456
def test_config_query_endpoints(tmp_path) -> None:
@ -181,6 +183,7 @@ def test_config_query_endpoints(tmp_path) -> None:
assert device_response.json()["data"]["password"] == ""
assert device_response.json()["data"]["net"][0]["ip"] == "192.168.1.10"
assert net_response.json()["data"]["nic"] == "网卡一"
assert net_response.json()["data"]["ip"] == "192.168.1.10"
assert uart_response.json()["data"]["port"] == "COM1"
assert channel_response.json()["data"]["ai_channel"][0]["singal_type"] == "4-20mA"
assert line_alarm_response.json()["data"]["line_no"] == 1
@ -239,10 +242,16 @@ def test_protocol_config_endpoints_read_and_write_device_only(tmp_path) -> None:
headers=headers,
json={"time_sync": "2026-05-25 13:00:00", "brightness": 88, "screen_saver": 90},
)
net_response = client.post(
"/api/config/device/net",
headers=headers,
json={"nic": "网卡二", "ip": "172.16.2.10", "mask": "255.255.255.0", "gateway": "172.16.2.1", "protocol": "Modbus TCP"},
)
query_channel = client.get("/api/config/channel", headers=headers)
query_line = client.get("/api/config/line_alarm_setting", headers=headers, params={"line_no": 3})
query_ai_alarm = client.get("/api/config/ai_alarm_setting", headers=headers)
query_system = client.get("/api/config/system", headers=headers)
query_net = client.get("/api/config/device/net", headers=headers, params={"nic": "网卡二"})
time_sync_response = client.post(
"/api/config/time-sync",
headers=headers,
@ -254,13 +263,17 @@ def test_protocol_config_endpoints_read_and_write_device_only(tmp_path) -> None:
assert line_response.status_code == 200
assert ai_alarm_response.status_code == 200
assert system_response.status_code == 200
assert net_response.status_code == 200
assert time_sync_response.status_code == 200
assert query_channel.json()["data"]["ai_channel"][0]["line_no"] == 3
assert query_line.json()["data"]["transformer_change"][0]["value"] == 20.0
assert query_ai_alarm.json()["data"][0]["delay"] == 30
assert query_system.json()["data"]["brightness"] == 88
assert query_net.json()["data"]["ip"] == "172.16.2.10"
assert query_net.json()["data"]["gateway"] == "172.16.2.1"
assert query_time_sync.json()["data"]["time_sync"] == "2026-05-25 14:00:00"
assert platform_service.config_repo.read_setting_config() == {}
assert platform_service.config_repo.read_device_config() == {}
finally:
platform_service.device_client = old_client
platform_service.config_repo = old_repo
@ -268,7 +281,9 @@ def test_protocol_config_endpoints_read_and_write_device_only(tmp_path) -> None:
def test_save_device_net_and_uart_by_key(tmp_path) -> None:
old_repo = platform_service.config_repo
old_client = platform_service.device_client
platform_service.config_repo = JsonConfigRepository(tmp_path)
platform_service.device_client = MockDeviceClient()
try:
platform_service.config_repo.write_device_config(
@ -338,15 +353,20 @@ def test_save_device_net_and_uart_by_key(tmp_path) -> None:
saved = platform_service.config_repo.read_device_config()
net_map = {item["nic"]: item for item in saved["net"]}
uart_map = {item["port"]: item for item in saved["uart"]}
net_query = client.get("/api/config/device/net", headers=headers, params={"nic": "网卡二"})
assert net_map["网卡一"]["ip"] == "172.16.1.10"
assert net_map["网卡二"]["ip"] == "10.10.10.2"
assert net_map["网卡二"]["protocol"] == "IEC104"
assert net_map["网卡二"]["ip"] == "192.168.1.56"
assert net_map["网卡二"]["protocol"] == "Modbus TCP"
assert uart_map["COM1"]["baud"] == 19200
assert uart_map["COM2"]["baud"] == 4800
assert uart_map["COM2"]["protocol"] == "DLT645"
assert net_query.status_code == 200
assert net_query.json()["data"]["ip"] == "10.10.10.2"
assert net_query.json()["data"]["gateway"] == "10.10.10.1"
assert saved["password"] == "hashed-password"
finally:
platform_service.device_client = old_client
platform_service.config_repo = old_repo

View File

@ -16,6 +16,8 @@ from app.adapters.device_client import (
EVENT_FLAG_REGISTER,
EVENT_INPUT_START_REGISTER,
EVENT_SUCCESS_REPLY_REGISTER,
NET_CONFIG_REGISTER_COUNT,
NET_CONFIG_START_REGISTER,
LINE_ALARM_REGISTERS_PER_LINE,
LINE_ALARM_START_REGISTER,
LINE_START_REGISTERS,
@ -145,8 +147,10 @@ def test_modbus_client_reads_alarm_event_once() -> None:
duplicate = client.read_alarm_events()
assert len(alarms) == 1
assert alarms[0].no == "2"
assert alarms[0].type == "PT断线"
assert alarms[0].line_code == 2
assert alarms[0].event_type == 1
assert alarms[0].event_code == 1
assert alarms[0].event_value == 456
assert "线路2PT断线" in alarms[0].content
assert duplicate == []
assert transport.written_registers[-1] == (EVENT_SUCCESS_REPLY_REGISTER, [1])
@ -190,6 +194,13 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
]
+ [0] * 55
)
net_payload_bytes = bytes(
[
192, 168, 1, 10, 255, 255, 255, 0, 192, 168, 1, 1, 1, 0,
10, 0, 0, 2, 255, 255, 255, 0, 10, 0, 0, 1, 1, 0,
]
+ [0] * 28
)
timestamp = int(datetime(2026, 5, 25, 12, 34, 56).timestamp())
transport.set_holding_block(AI_CHANNEL_START_REGISTER, ai_channel_payload)
@ -199,6 +210,7 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
client._bytes_to_registers(line_alarm_payload_bytes),
)
transport.set_holding_block(AI_ALARM_START_REGISTER, client._bytes_to_registers(ai_alarm_payload_bytes))
transport.set_holding_block(NET_CONFIG_START_REGISTER, client._bytes_to_registers(net_payload_bytes))
transport.set_holding_block(TIME_SYNC_START_REGISTER, [timestamp & 0xFFFF, (timestamp >> 16) & 0xFFFF])
device_payload = DeviceConfigIn(
@ -248,7 +260,9 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
read_ai_alarm = client.read_ai_alarm_setting()
read_system = client.read_system_config()
read_time_sync = client.read_time_sync_config()
read_net = client.read_net_config("网卡二")
device_result = client.send_device_config(device_payload)
net_result = client.send_net_config(NetConfigItem(nic="网卡一", ip="172.16.1.10", mask="255.255.0.0", gateway="172.16.0.1", protocol="Modbus TCP"))
channel_result = client.send_channel_config(channel_payload)
line_result = client.send_line_alarm_setting(line_alarm_payload)
ai_alarm_result = client.send_ai_alarm_setting(ai_alarm_payload)
@ -269,6 +283,9 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
assert read_ai_alarm[0]["delay"] == 3
assert read_system["time_sync"] == "2026-05-25 12:34:56"
assert read_time_sync["time_sync"] == "2026-05-25 12:34:56"
assert read_net["nic"] == "网卡二"
assert read_net["ip"] == "10.0.0.2"
assert read_net["gateway"] == "10.0.0.1"
assert AI_CHANNEL_START_REGISTER in register_map
assert len(register_map[AI_CHANNEL_START_REGISTER]) == 30
@ -278,6 +295,8 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
assert len(register_map[LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE]) == 16
assert AI_ALARM_START_REGISTER in register_map
assert len(register_map[AI_ALARM_START_REGISTER]) == 30
assert NET_CONFIG_START_REGISTER in register_map
assert len(register_map[NET_CONFIG_START_REGISTER]) == NET_CONFIG_REGISTER_COUNT
assert TIME_SYNC_START_REGISTER in register_map
assert len(register_map[TIME_SYNC_START_REGISTER]) == 2
assert DO_STATE_REGISTER in register_map
@ -285,6 +304,8 @@ def test_modbus_client_reads_and_writes_downstream_registers() -> None:
assert transport.written_coils == [(2, True)]
assert device_result["send_status"] == "成功"
assert net_result["target"] == "net"
assert net_result["nic"] == "网卡一"
assert channel_result["modbus_written"]["ai_registers"] == 30
assert line_result["line_no"] == 2
assert ai_alarm_result["items"] == 1

View File

@ -1,207 +0,0 @@
#ifndef __GUI_H
#define __GUI_H
#include "time.h"
#define LINE_TOTAL_NUM 4 //线路数量
#define ANALOG_CH_NUM 12 //AI、AO模拟量通道数
/*************上行数据*************************************************小核数据 -> 大核****************************************************************************/
//实时量
typedef struct
{
//三相电压
float Ua; //A相电压
float Ub; //B相电压
float Uc; //C相电压
//三相电流
float Ia; //A相电流
float Ib; //B相电流
float Ic; //C相电流
//有功功率
float Pa;
float Pb;
float Pc;
float Pt; //总有功功率
//无功功率
float Qa;
float Qb;
float Qc;
float Qt; //总无功功率
//视在功率
float Sa;
float Sb;
float Sc;
float St; //总视在功率
//功率因数
float PFa;
float PFb;
float PFc;
float PFt; //总功率因数
//线电压
float Uab;
float Ubc;
float Uca;
//频率
float frq;
}measure_unit_t;
//B码对时时间数据
typedef union
{
uint8_t b_code[6];
struct
{
uint8_t ascii_s_unit : 4;
uint8_t ascii_s_ten : 4;
uint8_t ascii_min_unit : 4;
uint8_t ascii_min_ten : 4;
uint8_t ascii_hour_unit : 4;
uint8_t ascii_hour_ten : 4;
uint8_t ascii_m_day_unit: 4;
uint8_t ascii_m_day_ten : 4;
uint8_t ascii_month_unit: 4; //4bit
uint8_t ascii_month_ten : 4; //4bit
uint8_t ascii_year_unit : 4; //4bit
uint8_t ascii_year_ten : 4; //4bit
}time;
}b_code_t;
//开入、开出
typedef struct
{
uint16_t in; //每一位对应一个通道状态 1 = 合, 0 = 分
uint16_t out; //每一位对应一个通道状态 1 = 合, 0 = 分
}d_io_t;
//模拟量:AI AO
typedef struct
{
float in[ANALOG_CH_NUM]; //模拟量输入
float out[ANALOG_CH_NUM]; //模拟量输出
}a_io_t;
//事件
typedef struct
{
struct tm t; //事件发生时间
uint32_t ms; //事件发生的毫秒时刻
uint8_t evt; //类型0 = 无事件, 1 = 事件发生
}event_t;
//波形数据
typedef struct
{ uint32_t stamp; //时标
int16_t ch[8]; //4条线路波形数据时8个通道ua、ub、uc, ia、ib、ic、频率系数、开入量
//AI采集通道波形数据时 8个通道对应各个输入点
}ch_data_t;
//波形数据
typedef struct
{
uint8_t line_index; //线路号 0 ~ 3 = 4路电压电流采样数据4 = AI采集数据
ch_data_t dot[64]; //64个点组成一个周波
}waveform_data_t;
//小核数据 -> 大核
typedef struct
{
measure_unit_t measure_unit[LINE_TOTAL_NUM];//总共4条线路按序排列
a_io_t d_io; //模拟量输入、输出
d_io_t a_io; //数字量,开入、开出
b_code_t b_code; //B码对时
event_t event[2]; //事件标志:event[0] = pt 断线, event[1] = CT断线
waveform_data_t waveform; //实时一周波数据
}gui_in_t;
/********下行数据******************************************************大核数据 -> 小核****************************************************************************/
//界面任一参数修改,即将整个参数表更新下发一次
//串口参数设置
typedef struct
{
uint32_t baud_tate; //波特率 9600 115200默认
uint8_t parity; //校验位 0 = 无校验, 1 = 奇校验, 2 = 偶校验
uint8_t data; //数据位 0 = 8位
uint8_t stop; //停止位 0 = 1位 1 = 2位
uint8_t protocol; //通讯协议 0 = 无协议1 = modbus
}com_param_t;
//模拟通道参数值
typedef struct
{
uint8_t sig_type;//信号类型 0 = 4~20ma信号 1 = 1~5V信号
uint8_t line_sub;//所属线路 1 = 线路1 2 = 线路2 3 =线路3 4 = 线路4 0 = 装置
uint8_t att; //类别 : 0 = UA, 1 = UB, 2 = UC, 3 = UAB, 4 = UBC, 5 = UCA, 6 = P, 7 = Q, 8 = F
uint8_t u_limit; //上限值 :
uint8_t d_limit; //下限值 :
}analog_ch_param_t;
//线路定值->开出设置
typedef struct
{
// uint8_t index; //线路号: 1 = 线路1, 2 = 线路2 3 = 线路3, 4 = 线路4
uint8_t limit; //限值:
uint8_t delay; //延时: [1 ~ 10秒]
uint8_t out_num; //输出节点: [1 = 开出1、 2 = 开出2、...]
uint8_t select; //投退: [0 = 不投, 1 = 投入]
}line_param_t;
typedef struct
{
line_param_t type[7]; //7个类别电压电流差流, 功率频率PT断线CT断线
}line_type_t;
//AI 报警-> 开出设置
typedef struct
{
uint8_t u_limit; //上限值 :
uint8_t d_limit; //下限值 :
uint8_t delay; //延时: [1 ~ 10秒]
uint8_t out_num; //输出节点: [1 = 开出1、 2 = 开出2、...]
uint8_t select; //投退: [0 = 不投, 1 = 投入]
}ai_alarm_param_t;
//大核数据 -> 小核
//设置模拟量,数字量输出
typedef struct
{
struct tm t; //时间设置
d_io_t a_io; //数字量,开入、开出
com_param_t com_param[4]; //串口参数设置
line_type_t line_param[4]; //线路定值设置: 4路定值一起下发
analog_ch_param_t a_in_param[12]; //12路AI模拟输入通道设置
analog_ch_param_t a_out_param[12]; //12路AO模拟输出通道设置
ai_alarm_param_t ai_alarm_param[12];//12路AI模拟输入报警参数设置
}gui_out_t;
#endif

View File

@ -1,12 +1,12 @@
#ifndef __GUI_H
#define __GUI_H
#include "time.h"
#include <rtthread.h>
#define LINE_TOTAL_NUM 4 //总测量线路数量
#define ANALOG_CH_NUM 12 //模拟量通道数
#define GUI_SIZE sizeof(gui_in_04reg_t)
/***********************************************************************小核数据 -> 大核****************************************************************************/
//实时量
@ -54,6 +54,8 @@ typedef struct
//频率
float frq;
// float rev1[10];//10个备用
}measure_unit_t;
//B码对时时间数据
@ -83,38 +85,36 @@ typedef union
typedef struct
{
uint32_t timestamp; //事件发生时间,时间戳
uin16_t ms; //事件发生的毫秒时刻
uint8_t line_num; //线路号0 = 其它1 = 1路2 =2路 3 = 3路 4 = 4路
uint8_t type; //事件类型1 = pt 断线, 2 = CT断线其它事件依次编号
uint16_t ms; //事件发生的毫秒时刻
uint8_t line_num; //高4位线路号 (0 = 装置1 = 线路12 =线路2 3 = 线路3 4 = 线路45= AI越限)
//低4位事件类型(1 = 保护、2 = 动作、3 = 报警、4 = 操作)
uint8_t code; /*事件代码保护类1 = pt 断线, 2 = CT断线其它事件依次编号
1 = 2 = 3 = ai4 = ao
*/
int16_t value; //动作值, 保护类、报警类和AI越限类→事件发生时刻的监测值、动作类→开入分/合状态0分、非0合
}event_t;
// //波形数据
// typedef struct
// { uint32_t stamp; //时标
// int16_t ch[8]; //4路波形数据时8个通道ua、ub、uc, ia、ib、ic、频率系数、开入量
// //AI波形数据时 8个通道对应各个输入点
// }ch_data_t;
// //波形数据
// typedef struct
// {
// uint8_t line_index; //线路号 0 ~ 3 = 4路电压电流采样数据4 = AI采集数据
// ch_data_t dot[64]; //64个点组成一个周波
// }waveform_data_t;
//小核数据 -> 大核
typedef struct
{
measure_unit_t measure_unit[LINE_TOTAL_NUM];//总共4条线路按序排列
float d_in[8]; //模拟量输入
float a_in[8]; //模拟量输入
b_code_t b_code; //B码对时
uint16_t a_in; //数字量,开入
uint16_t evt_f; //事件标志,读取到时间标志后,再读取保持寄存器里的事件内容
uint16_t d_in; //数字量,开入
uint16_t d_out; //开出,读开出状态
uint16_t evt_f; //事件个数读取到事件个数非0时立即读取输入寄存器里的事件内容
}gui_in_04reg_t;//读输入寄存器数据
union gui_t
{
uint8_t buf[GUI_SIZE];
gui_in_04reg_t sub;
};
typedef struct
{
event_t event;
@ -140,6 +140,15 @@ typedef struct
uint8_t protocol; //通讯协议 0 = 无协议1 = modbus
}com_param_t;
//网口
typedef struct
{
uint8_t id[4];
uint8_t mask[4];
uint8_t gateway[4];
uint8_t protocol; //协议
uint8_t rev;//保留
}net_t;
//模拟通道参数值
typedef struct
@ -160,11 +169,15 @@ typedef struct
uint8_t delay; //延时: [1 ~ 10秒]
uint8_t out_num; //输出节点: [1 = 开出1、 2 = 开出2、...]
uint8_t select; //投退: [0 = 不投, 1 = 投入]
}line_param_t;
typedef struct
{
line_param_t type[7]; //7个类别电压电流差流, 功率频率PT断线CT断线
uint16_t ct; //ct变比
uint16_t pt; //pt变比
}line_type_t;
@ -194,12 +207,17 @@ typedef struct
}gui_out_03reg_t;//放入保持寄存器中的参数
typedef struct
{
uint16_t a_in; //数字量,开出
}gui_out_01reg_t;//放入线圈寄存器中的数
uint16_t d_out; //数字量,开出
}gui_out_15reg_t;//放入线圈寄存器中的数
typedef struct
{
gui_out_03reg_t reg_03;
gui_out_15reg_t reg_15;
}param_set_t;
#endif

View File

@ -196,6 +196,7 @@
| AO通道设置 | `0x03 / 0x10` | 保持寄存器 | `0x0072` | 30 | 12路整体下发 |
| AI报警设置 | `0x03 / 0x10` | 保持寄存器 | `0x0090` | 30 | 12路整体下发 |
| 开出状态字写入 | `0x10` | 保持寄存器 | `0x00A6` | 1 | 16位状态字每 bit 一路开出 |
| 网口设置 | `0x03 / 0x10` | 保持寄存器 | `0x00AE` | 28 | 4路 × 14字节 / 2 = 28 |
### 5.2.1 时间设置 / 独立对时说明
@ -269,40 +270,68 @@
| 停止位 | `stop` |
| 通讯协议 | `protocol` |
### 5.4 常规配置 / 网设置
### 5.4 常规配置 / 网设置
#### 5.4.1 协议定义说明
#### 5.4.1 基本定义
| 项目 | 定义 |
| ---- | ---- |
| 配置项 | 网卡号、IP地址、子网掩码、默认网关、通讯协议 |
| 当前协议状态 | 本版 Modbus 协议**未定义对应寄存器** |
| Modbus 读写支持 | 不支持 |
| 处理方式 | 由上位机或系统本地配置逻辑单独处理,不纳入当前 Modbus 参数区 |
| 寄存器类型 | 保持寄存器 |
| 功能码 | `0x03 / 0x10` |
| 起始地址 | `0x00AE` |
| 寄存器长度 | `28` |
| 长度说明 | `(4(路) × 14byte数据) / 2 = 28` |
说明:
- 对应界面中的“常规配置 / 网卡设置”区域。
- 当前文档已定义“串口设置”的 Modbus 参数区,但**未定义网卡设置的寄存器地址、长度和编码方式**。
- 因此本版协议下,网卡参数**不能**按 `03H / 10H` 通过 Modbus 回读或下发。
- 若后续需要将网卡参数纳入 Modbus 管理,建议在协议后续版本中补充:
- 寄存器类型
- 功能码
- 起始地址
- 寄存器长度
- IP / 掩码 / 网关 / 协议字段编码规则
- 对应界面中的“常规配置 / 网口设置”区域。
- 共支持 `4` 路网口参数读写。
- 每一路网口参数占用 `14` 字节,即 `7` 个保持寄存器。
- 主站进入参数页面时应先使用 `03H` 回读当前网口参数,界面修改后再使用 `10H` 整组下发。
#### 5.4.2 界面字段范围
#### 5.4.2 单路网口参数组成
单路网口参数结构对应 `net_t`,总长度 `14` 字节:
```c
typedef struct
{
uint8_t id[4];
uint8_t mask[4];
uint8_t gateway[4];
uint8_t protocol;
uint8_t rev;
} net_t;
```
| 字段 | 类型 | 字节数 | 说明 |
| ---- | ---- | ------ | ---- |
| `id` | `uint8_t[4]` | 4 | IP 地址4 个字节 |
| `mask` | `uint8_t[4]` | 4 | 子网掩码4 个字节 |
| `gateway` | `uint8_t[4]` | 4 | 默认网关4 个字节 |
| `protocol` | `uint8_t` | 1 | 通讯协议 |
| `rev` | `uint8_t` | 1 | 保留 |
#### 5.4.3 寄存器分配
| 网口 | 寄存器范围 | 长度 | 说明 |
| ---- | ---------- | ---- | ---- |
| 网口1 | `0x00AE ~ 0x00B4` | 7 | 对应界面中的一组网口参数 |
| 网口2 | `0x00B5 ~ 0x00BB` | 7 | 对应界面中的一组网口参数 |
| 网口3 | `0x00BC ~ 0x00C2` | 7 | 预留/扩展网口参数 |
| 网口4 | `0x00C3 ~ 0x00C9` | 7 | 预留/扩展网口参数 |
#### 5.4.4 界面字段对应关系
对应界面中的典型字段如下:
| 界面字段 | 当前协议状态 |
| -------- | ------------ |
| 网卡号 | 未定义寄存器 |
| IP地址 | 未定义寄存器 |
| 子网掩码 | 未定义寄存器 |
| 默认网关 | 未定义寄存器 |
| 通讯协议 | 未定义寄存器 |
| 界面字段 | 协议字段 |
| -------- | -------- |
| 网卡号 | 由寄存器块位置区分,不单独占用字段 |
| IP地址 | `id[4]` |
| 子网掩码 | `mask[4]` |
| 默认网关 | `gateway[4]` |
| 通讯协议 | `protocol` |
---