我的修改
This commit is contained in:
parent
9268d572c7
commit
10c6a99ad8
3
.gitignore
vendored
3
.gitignore
vendored
@ -30,6 +30,8 @@ backend/data/
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!backend/.env.example
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
@ -47,4 +49,3 @@ pnpm-debug.log*
|
||||
*.swp
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
|
||||
27
backend/.env.example
Normal file
27
backend/.env.example
Normal file
@ -0,0 +1,27 @@
|
||||
EMCP_DATA_DIR=/userdata/emcp/data
|
||||
EMCP_ALARM_DB_PATH=/userdata/emcp/data/alarm.db
|
||||
EMCP_BACKUP_DIR=/userdata/emcp/data/backups
|
||||
|
||||
# true: use MockDeviceClient
|
||||
# false: use real Modbus client
|
||||
EMCP_USE_MOCK_DEVICE=true
|
||||
|
||||
# mock | rtu | tcp
|
||||
EMCP_MODBUS_TRANSPORT=rtu
|
||||
EMCP_MODBUS_SLAVE_ID=1
|
||||
EMCP_MODBUS_TIMEOUT_SECONDS=1.0
|
||||
|
||||
# float decode options
|
||||
EMCP_MODBUS_FLOAT_BYTE_ORDER=big
|
||||
EMCP_MODBUS_FLOAT_WORD_ORDER=big
|
||||
|
||||
# RTU settings
|
||||
EMCP_MODBUS_SERIAL_DEVICE=/dev/ttyS1
|
||||
EMCP_MODBUS_SERIAL_BAUD=115200
|
||||
EMCP_MODBUS_SERIAL_PARITY=N
|
||||
EMCP_MODBUS_SERIAL_DATA_BITS=8
|
||||
EMCP_MODBUS_SERIAL_STOP_BITS=1
|
||||
|
||||
# TCP settings
|
||||
EMCP_MODBUS_TCP_HOST=127.0.0.1
|
||||
EMCP_MODBUS_TCP_PORT=502
|
||||
@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from random import Random
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional, Sequence
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
from app.schemas.platform import (
|
||||
AlarmEvent,
|
||||
@ -57,6 +60,204 @@ class DeviceClient(ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
VALUE_GROUP_FIELDS = [
|
||||
"Ua",
|
||||
"Ub",
|
||||
"Uc",
|
||||
"Ia",
|
||||
"Ib",
|
||||
"Ic",
|
||||
"Pa",
|
||||
"Pb",
|
||||
"Pc",
|
||||
"Pt",
|
||||
"Qa",
|
||||
"Qb",
|
||||
"Qc",
|
||||
"Qt",
|
||||
"Sa",
|
||||
"Sb",
|
||||
"Sc",
|
||||
"St",
|
||||
"PFa",
|
||||
"PFb",
|
||||
"PFc",
|
||||
"PFt",
|
||||
"Uab",
|
||||
"Ubc",
|
||||
"Uca",
|
||||
"frq",
|
||||
]
|
||||
LINE_START_REGISTERS = [0x0000, 0x0034, 0x0068, 0x009C]
|
||||
LINE_REGISTER_COUNT = 0x34
|
||||
AI_INPUT_START_REGISTER = 0x00D0
|
||||
AI_INPUT_COUNT = 8
|
||||
DI_INPUT_REGISTER = 0x00E3
|
||||
EVENT_FLAG_REGISTER = 0x00E4
|
||||
EVENT_HOLDING_START_REGISTER = 0x0000
|
||||
UART_CONFIG_START_REGISTER = 0x0004
|
||||
UART_CONFIG_REGISTER_COUNT = 16
|
||||
LINE_ALARM_START_REGISTER = 0x0014
|
||||
LINE_ALARM_REGISTERS_PER_LINE = 14
|
||||
AI_CHANNEL_START_REGISTER = 0x004C
|
||||
AO_CHANNEL_START_REGISTER = 0x006A
|
||||
AI_ALARM_START_REGISTER = 0x0088
|
||||
ANALOG_BLOCK_REGISTER_COUNT = 30
|
||||
DO_STATE_REGISTER = 0x00A6
|
||||
TIME_SYNC_START_REGISTER = 0x0000
|
||||
|
||||
|
||||
def _clamp_byte(value: Any) -> int:
|
||||
try:
|
||||
number = int(round(float(value)))
|
||||
except (TypeError, ValueError):
|
||||
number = 0
|
||||
return max(0, min(255, number))
|
||||
|
||||
|
||||
def _normalize_text(value: str) -> str:
|
||||
return value.strip().upper().replace(" ", "")
|
||||
|
||||
|
||||
def _serial_parity_flag(value: str) -> str:
|
||||
normalized = _normalize_text(value)
|
||||
return {"NONE": "N", "N": "N", "ODD": "O", "O": "O", "EVEN": "E", "E": "E"}.get(normalized, "N")
|
||||
|
||||
|
||||
class ModbusTransport(ABC):
|
||||
@abstractmethod
|
||||
def read_input_registers(self, address: int, count: int) -> List[int]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read_holding_registers(self, address: int, count: int) -> List[int]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read_discrete_inputs(self, address: int, count: int) -> List[bool]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read_coils(self, address: int, count: int) -> List[bool]:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write_registers(self, address: int, values: Sequence[int]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write_coil(self, address: int, value: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PymodbusTransport(ModbusTransport):
|
||||
def __init__(self) -> None:
|
||||
self._client: Any = None
|
||||
|
||||
def _build_client(self) -> Any:
|
||||
try:
|
||||
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
||||
except ImportError as exc:
|
||||
raise RuntimeError("未安装 pymodbus,无法启用 Modbus 采集") from exc
|
||||
|
||||
transport = settings.modbus_transport.strip().lower()
|
||||
if transport == "tcp":
|
||||
return ModbusTcpClient(
|
||||
host=settings.modbus_tcp_host,
|
||||
port=settings.modbus_tcp_port,
|
||||
timeout=settings.modbus_timeout_seconds,
|
||||
)
|
||||
|
||||
try:
|
||||
from pymodbus import FramerType
|
||||
|
||||
return ModbusSerialClient(
|
||||
port=settings.modbus_serial_device,
|
||||
framer=FramerType.RTU,
|
||||
baudrate=settings.modbus_serial_baud,
|
||||
parity=_serial_parity_flag(settings.modbus_serial_parity),
|
||||
stopbits=settings.modbus_serial_stop_bits,
|
||||
bytesize=settings.modbus_serial_data_bits,
|
||||
timeout=settings.modbus_timeout_seconds,
|
||||
)
|
||||
except Exception:
|
||||
return ModbusSerialClient(
|
||||
method="rtu",
|
||||
port=settings.modbus_serial_device,
|
||||
baudrate=settings.modbus_serial_baud,
|
||||
parity=_serial_parity_flag(settings.modbus_serial_parity),
|
||||
stopbits=settings.modbus_serial_stop_bits,
|
||||
bytesize=settings.modbus_serial_data_bits,
|
||||
timeout=settings.modbus_timeout_seconds,
|
||||
)
|
||||
|
||||
def _ensure_client(self) -> Any:
|
||||
if self._client is None:
|
||||
self._client = self._build_client()
|
||||
connect = getattr(self._client, "connect", None)
|
||||
if callable(connect):
|
||||
result = connect()
|
||||
if result is False:
|
||||
raise RuntimeError("Modbus 连接失败")
|
||||
return self._client
|
||||
|
||||
def _call(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
client = self._ensure_client()
|
||||
method = getattr(client, method_name)
|
||||
try:
|
||||
return method(*args, slave=settings.modbus_slave_id, **kwargs)
|
||||
except TypeError:
|
||||
return method(*args, unit=settings.modbus_slave_id, **kwargs)
|
||||
|
||||
def _check_response(self, response: Any, action: str) -> Any:
|
||||
if response is None:
|
||||
raise RuntimeError(f"Modbus {action} 返回为空")
|
||||
is_error = getattr(response, "isError", None)
|
||||
if callable(is_error) and response.isError():
|
||||
raise RuntimeError(f"Modbus {action} 返回错误: {response}")
|
||||
return response
|
||||
|
||||
def read_input_registers(self, address: int, count: int) -> List[int]:
|
||||
response = self._check_response(
|
||||
self._call("read_input_registers", address, count),
|
||||
f"读输入寄存器[{hex(address)}:{count}]",
|
||||
)
|
||||
return list(getattr(response, "registers", []) or [])
|
||||
|
||||
def read_holding_registers(self, address: int, count: int) -> List[int]:
|
||||
response = self._check_response(
|
||||
self._call("read_holding_registers", address, count),
|
||||
f"读保持寄存器[{hex(address)}:{count}]",
|
||||
)
|
||||
return list(getattr(response, "registers", []) or [])
|
||||
|
||||
def read_discrete_inputs(self, address: int, count: int) -> List[bool]:
|
||||
response = self._check_response(
|
||||
self._call("read_discrete_inputs", address, count),
|
||||
f"读离散输入[{hex(address)}:{count}]",
|
||||
)
|
||||
return [bool(value) for value in (getattr(response, "bits", []) or [])[:count]]
|
||||
|
||||
def read_coils(self, address: int, count: int) -> List[bool]:
|
||||
response = self._check_response(
|
||||
self._call("read_coils", address, count),
|
||||
f"读线圈[{hex(address)}:{count}]",
|
||||
)
|
||||
return [bool(value) for value in (getattr(response, "bits", []) or [])[:count]]
|
||||
|
||||
def write_registers(self, address: int, values: Sequence[int]) -> None:
|
||||
self._check_response(
|
||||
self._call("write_registers", address, list(values)),
|
||||
f"写保持寄存器[{hex(address)}:{len(values)}]",
|
||||
)
|
||||
|
||||
def write_coil(self, address: int, value: bool) -> None:
|
||||
self._check_response(
|
||||
self._call("write_coil", address, bool(value)),
|
||||
f"写线圈[{hex(address)}]",
|
||||
)
|
||||
|
||||
|
||||
class MockDeviceClient(DeviceClient):
|
||||
def __init__(self) -> None:
|
||||
self._random = Random(3568)
|
||||
@ -153,5 +354,347 @@ class MockDeviceClient(DeviceClient):
|
||||
return {"control_status": f"执行成功: 开关{payload.ch}{action_text}"}
|
||||
|
||||
|
||||
class CDeviceClient(MockDeviceClient):
|
||||
"""真实 C 程序接入前先复用 mock 行为,后续在此类中替换协议实现。"""
|
||||
class CDeviceClient(DeviceClient):
|
||||
"""基于 Modbus 协议的真实设备客户端。"""
|
||||
|
||||
def __init__(self, transport: Optional[ModbusTransport] = None) -> None:
|
||||
self.transport = transport or PymodbusTransport()
|
||||
self._do_state = {index: 0 for index in range(1, 13)}
|
||||
self._last_event_signature: Optional[tuple[int, int, int, int]] = None
|
||||
|
||||
def _registers_to_float(self, registers: Sequence[int]) -> float:
|
||||
ordered = list(registers)
|
||||
if settings.modbus_float_word_order.strip().lower() == "little":
|
||||
ordered.reverse()
|
||||
|
||||
raw = bytearray()
|
||||
for register in ordered:
|
||||
high = (int(register) >> 8) & 0xFF
|
||||
low = int(register) & 0xFF
|
||||
if settings.modbus_float_byte_order.strip().lower() == "little":
|
||||
raw.extend([low, high])
|
||||
else:
|
||||
raw.extend([high, low])
|
||||
return float(struct.unpack(">f", bytes(raw))[0])
|
||||
|
||||
def _float_to_registers(self, value: float) -> List[int]:
|
||||
raw = struct.pack(">f", float(value))
|
||||
words = [bytearray(raw[0:2]), bytearray(raw[2:4])]
|
||||
if settings.modbus_float_byte_order.strip().lower() == "little":
|
||||
words = [bytearray(reversed(word)) for word in words]
|
||||
registers = [(word[0] << 8) | word[1] for word in words]
|
||||
if settings.modbus_float_word_order.strip().lower() == "little":
|
||||
registers.reverse()
|
||||
return registers
|
||||
|
||||
def _bytes_to_registers(self, payload: bytes) -> List[int]:
|
||||
if len(payload) % 2:
|
||||
payload += b"\x00"
|
||||
return [(payload[index] << 8) | payload[index + 1] for index in range(0, len(payload), 2)]
|
||||
|
||||
def _build_value_group(self, registers: Sequence[int]) -> ValueGroup:
|
||||
values: Dict[str, float] = {}
|
||||
for index, field_name in enumerate(VALUE_GROUP_FIELDS):
|
||||
start = index * 2
|
||||
values[field_name] = self._registers_to_float(registers[start : start + 2])
|
||||
return ValueGroup(**values)
|
||||
|
||||
def _read_line_data(self, line_no: int, start_register: int) -> LineData:
|
||||
registers = self.transport.read_input_registers(start_register, LINE_REGISTER_COUNT)
|
||||
value_group = self._build_value_group(registers)
|
||||
return LineData(line_no=line_no, pri_val=value_group, sec_val=value_group)
|
||||
|
||||
def _read_ai_collect(self) -> Dict[str, float]:
|
||||
registers = self.transport.read_input_registers(AI_INPUT_START_REGISTER, AI_INPUT_COUNT * 2)
|
||||
values = {
|
||||
f"ai{index + 1}": self._registers_to_float(registers[index * 2 : index * 2 + 2])
|
||||
for index in range(AI_INPUT_COUNT)
|
||||
}
|
||||
for index in range(AI_INPUT_COUNT + 1, 13):
|
||||
values[f"ai{index}"] = 0.0
|
||||
return values
|
||||
|
||||
def _read_di_state(self) -> Dict[str, int]:
|
||||
try:
|
||||
bits = self.transport.read_discrete_inputs(0x0000, 12)
|
||||
return {f"di{index + 1}": int(bits[index]) for index in range(min(12, len(bits)))}
|
||||
except Exception:
|
||||
register = self.transport.read_input_registers(DI_INPUT_REGISTER, 1)[0]
|
||||
return {f"di{index}": int(bool(register & (1 << (index - 1)))) for index in range(1, 13)}
|
||||
|
||||
def _read_do_state(self) -> Dict[str, int]:
|
||||
try:
|
||||
bits = self.transport.read_coils(0x0000, 12)
|
||||
if bits:
|
||||
for index, bit in enumerate(bits[:12], start=1):
|
||||
self._do_state[index] = int(bool(bit))
|
||||
except Exception:
|
||||
pass
|
||||
return {f"do{index}": self._do_state.get(index, 0) for index in range(1, 13)}
|
||||
|
||||
def _build_status(self) -> DeviceStatus:
|
||||
transport = settings.modbus_transport.strip().lower()
|
||||
if transport == "tcp":
|
||||
return DeviceStatus(self_check="正常", net1="正常", net2="正常", uart1="断开", uart2="断开")
|
||||
return DeviceStatus(self_check="正常", net1="断开", net2="断开", uart1="正常", uart2="正常")
|
||||
|
||||
def _category_map(self, category: str) -> str:
|
||||
normalized = category.strip().upper()
|
||||
return {
|
||||
"UA": "UA",
|
||||
"UB": "UB",
|
||||
"UC": "UC",
|
||||
"UAB": "UAB",
|
||||
"UBC": "UBC",
|
||||
"UCA": "UCA",
|
||||
"P": "P",
|
||||
"Q": "Q",
|
||||
"F": "F",
|
||||
"FRQ": "F",
|
||||
"频率": "F",
|
||||
"功率": "P",
|
||||
}.get(normalized, normalized)
|
||||
|
||||
def _signal_type_code(self, signal_type: str) -> int:
|
||||
normalized = _normalize_text(signal_type)
|
||||
return 0 if normalized in {"4-20MA", "4~20MA", "4-20MA"} else 1
|
||||
|
||||
def _measure_type_code(self, measure_type: str) -> int:
|
||||
normalized = self._category_map(measure_type)
|
||||
return {
|
||||
"UA": 0,
|
||||
"UB": 1,
|
||||
"UC": 2,
|
||||
"UAB": 3,
|
||||
"UBC": 4,
|
||||
"UCA": 5,
|
||||
"P": 6,
|
||||
"Q": 7,
|
||||
"F": 8,
|
||||
}.get(normalized, 0)
|
||||
|
||||
def _output_node_code(self, output_node: str) -> int:
|
||||
digits = "".join(character for character in output_node if character.isdigit())
|
||||
return _clamp_byte(digits or 0)
|
||||
|
||||
def _parity_code(self, parity: str) -> int:
|
||||
normalized = _normalize_text(parity)
|
||||
return {"NONE": 0, "N": 0, "ODD": 1, "O": 1, "EVEN": 2, "E": 2}.get(normalized, 0)
|
||||
|
||||
def _data_bits_code(self, data_bits: int) -> int:
|
||||
return 0 if int(data_bits) == 8 else _clamp_byte(data_bits)
|
||||
|
||||
def _stop_bits_code(self, stop_bits: int) -> int:
|
||||
return 1 if int(stop_bits) >= 2 else 0
|
||||
|
||||
def _protocol_code(self, protocol: str) -> int:
|
||||
return 1 if "MODBUS" in _normalize_text(protocol) else 0
|
||||
|
||||
def _line_rule_payload(self, limit: Any, delay: Any, output_node: str, enabled: bool) -> bytes:
|
||||
return bytes(
|
||||
[
|
||||
_clamp_byte(limit),
|
||||
_clamp_byte(delay),
|
||||
self._output_node_code(output_node),
|
||||
1 if enabled else 0,
|
||||
]
|
||||
)
|
||||
|
||||
def read_realtime_data(self) -> RealtimeData:
|
||||
line_list = [
|
||||
self._read_line_data(line_no=index + 1, start_register=start_register)
|
||||
for index, start_register in enumerate(LINE_START_REGISTERS)
|
||||
]
|
||||
switch = self._read_di_state()
|
||||
switch.update(self._read_do_state())
|
||||
return RealtimeData(line_list=line_list, switch=switch, ai_collect=self._read_ai_collect())
|
||||
|
||||
def read_device_status(self) -> DeviceStatus:
|
||||
self.transport.read_input_registers(EVENT_FLAG_REGISTER, 1)
|
||||
return self._build_status()
|
||||
|
||||
def read_alarm_events(self) -> List[AlarmEvent]:
|
||||
event_flag = self.transport.read_input_registers(EVENT_FLAG_REGISTER, 1)[0]
|
||||
if event_flag == 0:
|
||||
return []
|
||||
|
||||
registers = self.transport.read_holding_registers(EVENT_HOLDING_START_REGISTER, 4)
|
||||
signature = tuple(int(value) for value in registers)
|
||||
if signature == self._last_event_signature:
|
||||
return []
|
||||
self._last_event_signature = signature
|
||||
|
||||
timestamp = int(registers[0]) | (int(registers[1]) << 16)
|
||||
milliseconds = int(registers[2]) & 0xFFFF
|
||||
line_and_type = int(registers[3]) & 0xFFFF
|
||||
line_no = (line_and_type >> 8) & 0xFF
|
||||
event_type = line_and_type & 0xFF
|
||||
|
||||
event_name = {
|
||||
1: "PT断线",
|
||||
2: "CT断线",
|
||||
}.get(event_type, f"事件{event_type}")
|
||||
event_time = datetime.fromtimestamp(timestamp).replace(microsecond=min(milliseconds, 999) * 1000)
|
||||
line_text = "总路" if line_no == 0 else f"线路{line_no}"
|
||||
return [
|
||||
AlarmEvent(
|
||||
alarm_type="modbus_event",
|
||||
time=event_time,
|
||||
no="" if line_no == 0 else str(line_no),
|
||||
type=event_name,
|
||||
content=f"{line_text}{event_name}",
|
||||
level="中",
|
||||
)
|
||||
]
|
||||
|
||||
def send_device_config(self, payload: DeviceConfigIn) -> Dict[str, Any]:
|
||||
uart_map = {item.port.upper(): item for item in payload.uart}
|
||||
raw = bytearray()
|
||||
for port_name in ("COM1", "COM2", "COM3", "COM4"):
|
||||
item = uart_map.get(port_name)
|
||||
baud = item.baud if item is not None else 0
|
||||
parity = self._parity_code(item.parity) if item is not None else 0
|
||||
data_bits = self._data_bits_code(item.data_bits) if item is not None else 0
|
||||
stop_bits = self._stop_bits_code(item.stop_bits) if item is not None else 0
|
||||
protocol = self._protocol_code(item.protocol) if item is not None else 0
|
||||
raw.extend(struct.pack("<I4B", int(baud), parity, data_bits, stop_bits, protocol))
|
||||
|
||||
registers = self._bytes_to_registers(bytes(raw))
|
||||
self.transport.write_registers(UART_CONFIG_START_REGISTER, registers)
|
||||
return {
|
||||
"send_status": "成功",
|
||||
"target": "device",
|
||||
"modbus_written": {"uart_registers": len(registers)},
|
||||
"ignored_fields": ["password", "hardware_version", "software_version", "net"],
|
||||
}
|
||||
|
||||
def send_channel_config(self, payload: ChannelConfigIn) -> Dict[str, Any]:
|
||||
def build_channel_payload(items: List[Any]) -> List[int]:
|
||||
raw = bytearray()
|
||||
channel_map = {int(item.ch): item for item in items}
|
||||
for channel_no in range(1, 13):
|
||||
item = channel_map.get(channel_no)
|
||||
if item is None:
|
||||
raw.extend(b"\x00" * 5)
|
||||
continue
|
||||
raw.extend(
|
||||
bytes(
|
||||
[
|
||||
self._signal_type_code(item.singal_type),
|
||||
_clamp_byte(item.line_no),
|
||||
self._measure_type_code(item.type),
|
||||
_clamp_byte(item.limit_high),
|
||||
_clamp_byte(item.limit_low),
|
||||
]
|
||||
)
|
||||
)
|
||||
return self._bytes_to_registers(bytes(raw))
|
||||
|
||||
ai_registers = build_channel_payload(payload.ai_channel)
|
||||
ao_registers = build_channel_payload(payload.ao_channel)
|
||||
self.transport.write_registers(AI_CHANNEL_START_REGISTER, ai_registers)
|
||||
self.transport.write_registers(AO_CHANNEL_START_REGISTER, ao_registers)
|
||||
return {
|
||||
"send_status": "成功",
|
||||
"target": "channel",
|
||||
"modbus_written": {
|
||||
"ai_registers": len(ai_registers),
|
||||
"ao_registers": len(ao_registers),
|
||||
},
|
||||
}
|
||||
|
||||
def send_line_alarm_setting(self, payload: LineAlarmSettingIn) -> Dict[str, Any]:
|
||||
over_limit_map = {rule.category: rule for rule in payload.over_limit_alarm}
|
||||
fault_map = {rule.category: rule for rule in payload.fault_alarm}
|
||||
|
||||
ordered_rules = [
|
||||
over_limit_map.get("电压"),
|
||||
over_limit_map.get("电流"),
|
||||
over_limit_map.get("差流"),
|
||||
over_limit_map.get("功率"),
|
||||
over_limit_map.get("频率"),
|
||||
fault_map.get("PT断线"),
|
||||
fault_map.get("CT断线"),
|
||||
]
|
||||
|
||||
raw = bytearray()
|
||||
for rule in ordered_rules:
|
||||
if rule is None:
|
||||
raw.extend(b"\x00" * 4)
|
||||
continue
|
||||
raw.extend(self._line_rule_payload(rule.limit, rule.delay, rule.output_node, rule.enabled))
|
||||
|
||||
start_register = LINE_ALARM_START_REGISTER + max(0, payload.line_no - 1) * LINE_ALARM_REGISTERS_PER_LINE
|
||||
registers = self._bytes_to_registers(bytes(raw))
|
||||
self.transport.write_registers(start_register, registers)
|
||||
return {
|
||||
"send_status": "成功",
|
||||
"target": "line_alarm",
|
||||
"line_no": payload.line_no,
|
||||
"modbus_written": {"start_register": start_register, "registers": len(registers)},
|
||||
}
|
||||
|
||||
def send_ai_alarm_setting(self, payload: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
raw = bytearray()
|
||||
item_map = {int(item.get("channel_no", 0)): item for item in payload}
|
||||
for channel_no in range(1, 13):
|
||||
item = item_map.get(channel_no)
|
||||
if item is None:
|
||||
raw.extend(b"\x00" * 5)
|
||||
continue
|
||||
raw.extend(
|
||||
bytes(
|
||||
[
|
||||
_clamp_byte(item.get("limit_high", 0)),
|
||||
_clamp_byte(item.get("limit_low", 0)),
|
||||
_clamp_byte(item.get("delay", 0)),
|
||||
self._output_node_code(str(item.get("output_node", ""))),
|
||||
1 if bool(item.get("enabled", False)) else 0,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
registers = self._bytes_to_registers(bytes(raw))
|
||||
self.transport.write_registers(AI_ALARM_START_REGISTER, registers)
|
||||
return {
|
||||
"send_status": "成功",
|
||||
"target": "ai_alarm",
|
||||
"items": len(payload),
|
||||
"modbus_written": {"registers": len(registers)},
|
||||
}
|
||||
|
||||
def send_system_config(self, payload: SystemConfigIn) -> Dict[str, Any]:
|
||||
time_text = str(payload.time_sync).strip()
|
||||
if time_text and time_text.lower() not in {"auto", "manual"}:
|
||||
timestamp = int(datetime.fromisoformat(time_text).timestamp())
|
||||
else:
|
||||
timestamp = int(datetime.now().timestamp())
|
||||
|
||||
registers = [timestamp & 0xFFFF, (timestamp >> 16) & 0xFFFF]
|
||||
self.transport.write_registers(TIME_SYNC_START_REGISTER, registers)
|
||||
return {
|
||||
"send_status": "成功",
|
||||
"target": "system",
|
||||
"brightness": payload.brightness,
|
||||
"screen_saver": payload.screen_saver,
|
||||
"modbus_written": {"time_registers": len(registers)},
|
||||
"ignored_fields": ["brightness", "screen_saver"],
|
||||
}
|
||||
|
||||
def send_switch_control(self, payload: SwitchControlIn) -> Dict[str, Any]:
|
||||
coil_address = max(0, payload.ch - 1)
|
||||
coil_value = bool(payload.action)
|
||||
self.transport.write_coil(coil_address, coil_value)
|
||||
self._do_state[payload.ch] = int(coil_value)
|
||||
|
||||
bitmask = 0
|
||||
for index, state in self._do_state.items():
|
||||
if state:
|
||||
bitmask |= 1 << (index - 1)
|
||||
self.transport.write_registers(DO_STATE_REGISTER, [bitmask & 0xFFFF])
|
||||
|
||||
action_text = "合" if payload.action == 1 else "分"
|
||||
return {
|
||||
"control_status": f"执行成功: 开关{payload.ch}{action_text}",
|
||||
"modbus_written": {"coil_address": coil_address, "state_register": DO_STATE_REGISTER},
|
||||
}
|
||||
|
||||
@ -15,6 +15,18 @@ class Settings(BaseSettings):
|
||||
poll_interval_seconds: float = 0.5
|
||||
auth_password: str = "admin123"
|
||||
use_mock_device: bool = True
|
||||
modbus_transport: str = "rtu"
|
||||
modbus_slave_id: int = 1
|
||||
modbus_tcp_host: str = "127.0.0.1"
|
||||
modbus_tcp_port: int = 502
|
||||
modbus_serial_device: str = "/dev/ttyS1"
|
||||
modbus_serial_baud: int = 115200
|
||||
modbus_serial_parity: str = "N"
|
||||
modbus_serial_data_bits: int = 8
|
||||
modbus_serial_stop_bits: int = 1
|
||||
modbus_timeout_seconds: float = 1.0
|
||||
modbus_float_byte_order: str = "big"
|
||||
modbus_float_word_order: str = "big"
|
||||
config_dir: Path = BASE_DIR / "config"
|
||||
data_dir: Path = BASE_DIR / "data"
|
||||
alarm_db_path: Path = BASE_DIR / "data" / "alarm.db"
|
||||
|
||||
@ -9,6 +9,7 @@ dependencies = [
|
||||
"uvicorn[standard]>=0.30,<1.0",
|
||||
"pydantic>=2.7,<3.0",
|
||||
"pydantic-settings>=2.3,<3.0",
|
||||
"pymodbus>=3.6,<4.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
226
backend/tests/test_modbus_client.py
Normal file
226
backend/tests/test_modbus_client.py
Normal file
@ -0,0 +1,226 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Sequence, Tuple
|
||||
|
||||
from app.adapters.device_client import (
|
||||
AI_ALARM_START_REGISTER,
|
||||
AI_CHANNEL_START_REGISTER,
|
||||
AI_INPUT_COUNT,
|
||||
AI_INPUT_START_REGISTER,
|
||||
AO_CHANNEL_START_REGISTER,
|
||||
CDeviceClient,
|
||||
DI_INPUT_REGISTER,
|
||||
DO_STATE_REGISTER,
|
||||
EVENT_FLAG_REGISTER,
|
||||
EVENT_HOLDING_START_REGISTER,
|
||||
LINE_ALARM_REGISTERS_PER_LINE,
|
||||
LINE_ALARM_START_REGISTER,
|
||||
LINE_START_REGISTERS,
|
||||
ModbusTransport,
|
||||
TIME_SYNC_START_REGISTER,
|
||||
UART_CONFIG_REGISTER_COUNT,
|
||||
UART_CONFIG_START_REGISTER,
|
||||
VALUE_GROUP_FIELDS,
|
||||
)
|
||||
from app.schemas.platform import (
|
||||
AiAlarmSettingIn,
|
||||
AlarmRule,
|
||||
ChannelConfigIn,
|
||||
ChannelItem,
|
||||
DeviceConfigIn,
|
||||
LineAlarmSettingIn,
|
||||
NetConfigItem,
|
||||
SoftwareVersion,
|
||||
HardwareVersion,
|
||||
SwitchControlIn,
|
||||
SystemConfigIn,
|
||||
UartConfigItem,
|
||||
)
|
||||
|
||||
|
||||
class FakeTransport(ModbusTransport):
|
||||
def __init__(self) -> None:
|
||||
self.input_registers: Dict[int, int] = {}
|
||||
self.holding_registers: Dict[int, int] = {}
|
||||
self.discrete_inputs: Dict[int, bool] = {}
|
||||
self.coils: Dict[int, bool] = {}
|
||||
self.written_registers: List[Tuple[int, List[int]]] = []
|
||||
self.written_coils: List[Tuple[int, bool]] = []
|
||||
|
||||
def set_input_block(self, address: int, values: Sequence[int]) -> None:
|
||||
for offset, value in enumerate(values):
|
||||
self.input_registers[address + offset] = int(value)
|
||||
|
||||
def set_holding_block(self, address: int, values: Sequence[int]) -> None:
|
||||
for offset, value in enumerate(values):
|
||||
self.holding_registers[address + offset] = int(value)
|
||||
|
||||
def set_discrete_block(self, address: int, values: Sequence[bool]) -> None:
|
||||
for offset, value in enumerate(values):
|
||||
self.discrete_inputs[address + offset] = bool(value)
|
||||
|
||||
def set_coil_block(self, address: int, values: Sequence[bool]) -> None:
|
||||
for offset, value in enumerate(values):
|
||||
self.coils[address + offset] = bool(value)
|
||||
|
||||
def read_input_registers(self, address: int, count: int) -> List[int]:
|
||||
return [self.input_registers.get(address + offset, 0) for offset in range(count)]
|
||||
|
||||
def read_holding_registers(self, address: int, count: int) -> List[int]:
|
||||
return [self.holding_registers.get(address + offset, 0) for offset in range(count)]
|
||||
|
||||
def read_discrete_inputs(self, address: int, count: int) -> List[bool]:
|
||||
return [self.discrete_inputs.get(address + offset, False) for offset in range(count)]
|
||||
|
||||
def read_coils(self, address: int, count: int) -> List[bool]:
|
||||
return [self.coils.get(address + offset, False) for offset in range(count)]
|
||||
|
||||
def write_registers(self, address: int, values: Sequence[int]) -> None:
|
||||
block = [int(value) for value in values]
|
||||
self.written_registers.append((address, block))
|
||||
for offset, value in enumerate(block):
|
||||
self.holding_registers[address + offset] = value
|
||||
|
||||
def write_coil(self, address: int, value: bool) -> None:
|
||||
self.written_coils.append((address, bool(value)))
|
||||
self.coils[address] = bool(value)
|
||||
|
||||
|
||||
def test_modbus_client_reads_upstream_data() -> None:
|
||||
transport = FakeTransport()
|
||||
client = CDeviceClient(transport=transport)
|
||||
|
||||
for line_index, start_register in enumerate(LINE_START_REGISTERS, start=1):
|
||||
values: List[int] = []
|
||||
base = float(line_index * 100)
|
||||
for field_offset, _field_name in enumerate(VALUE_GROUP_FIELDS, start=1):
|
||||
values.extend(client._float_to_registers(base + field_offset))
|
||||
transport.set_input_block(start_register, values)
|
||||
|
||||
ai_values: List[int] = []
|
||||
for index in range(AI_INPUT_COUNT):
|
||||
ai_values.extend(client._float_to_registers(1.25 + index))
|
||||
transport.set_input_block(AI_INPUT_START_REGISTER, ai_values)
|
||||
transport.set_discrete_block(0x0000, [True, False, True, False, True, False, True, False, True, False, True, False])
|
||||
transport.set_coil_block(0x0000, [False, True, False, True, False, True, False, True, False, True, False, True])
|
||||
transport.set_input_block(DI_INPUT_REGISTER, [0x0555])
|
||||
|
||||
realtime = client.read_realtime_data()
|
||||
|
||||
assert len(realtime.line_list) == 4
|
||||
assert realtime.line_list[0].sec_val.Ua == 101.0
|
||||
assert realtime.line_list[1].sec_val.Pt == 210.0
|
||||
assert realtime.line_list[3].sec_val.frq == 426.0
|
||||
assert realtime.ai_collect["ai1"] == 1.25
|
||||
assert realtime.ai_collect["ai8"] == 8.25
|
||||
assert realtime.ai_collect["ai9"] == 0.0
|
||||
assert realtime.switch["di1"] == 1
|
||||
assert realtime.switch["di2"] == 0
|
||||
assert realtime.switch["do1"] == 0
|
||||
assert realtime.switch["do2"] == 1
|
||||
|
||||
|
||||
def test_modbus_client_reads_alarm_event_once() -> None:
|
||||
transport = FakeTransport()
|
||||
client = CDeviceClient(transport=transport)
|
||||
timestamp = int(datetime(2026, 5, 25, 12, 0, 0).timestamp())
|
||||
|
||||
transport.set_input_block(EVENT_FLAG_REGISTER, [1])
|
||||
transport.set_holding_block(
|
||||
EVENT_HOLDING_START_REGISTER,
|
||||
[
|
||||
timestamp & 0xFFFF,
|
||||
(timestamp >> 16) & 0xFFFF,
|
||||
123,
|
||||
(2 << 8) | 1,
|
||||
],
|
||||
)
|
||||
|
||||
alarms = client.read_alarm_events()
|
||||
duplicate = client.read_alarm_events()
|
||||
|
||||
assert len(alarms) == 1
|
||||
assert alarms[0].no == "2"
|
||||
assert alarms[0].type == "PT断线"
|
||||
assert "线路2PT断线" in alarms[0].content
|
||||
assert duplicate == []
|
||||
|
||||
|
||||
def test_modbus_client_writes_downstream_registers() -> None:
|
||||
transport = FakeTransport()
|
||||
client = CDeviceClient(transport=transport)
|
||||
|
||||
device_payload = DeviceConfigIn(
|
||||
password="",
|
||||
hardware_version=HardwareVersion(),
|
||||
software_version=SoftwareVersion(),
|
||||
net=[NetConfigItem(nic="网卡一", ip="192.168.1.10", mask="255.255.255.0", gateway="192.168.1.1", protocol="Modbus TCP")],
|
||||
uart=[
|
||||
UartConfigItem(port="COM1", baud=9600, parity="NONE", data_bits=8, stop_bits=1, protocol="Modbus RTU"),
|
||||
UartConfigItem(port="COM2", baud=115200, parity="EVEN", data_bits=8, stop_bits=2, protocol="Modbus RTU"),
|
||||
],
|
||||
)
|
||||
channel_payload = ChannelConfigIn(
|
||||
ai_channel=[ChannelItem(ch=1, singal_type="4-20mA", line_no=1, type="UA", limit_low=0, limit_high=20)],
|
||||
ao_channel=[ChannelItem(ch=2, singal_type="1~5V", line_no=2, type="Q", limit_low=1, limit_high=8)],
|
||||
)
|
||||
line_alarm_payload = LineAlarmSettingIn(
|
||||
line_no=2,
|
||||
over_limit_alarm=[
|
||||
AlarmRule(category="电压", limit=180, delay=5, output_node="开出1", enabled=True),
|
||||
AlarmRule(category="电流", limit=200, delay=6, output_node="开出2", enabled=False),
|
||||
AlarmRule(category="差流", limit=20, delay=7, output_node="开出3", enabled=True),
|
||||
AlarmRule(category="功率", limit=30, delay=8, output_node="开出4", enabled=False),
|
||||
AlarmRule(category="频率", limit=50, delay=9, output_node="开出5", enabled=True),
|
||||
],
|
||||
fault_alarm=[
|
||||
AlarmRule(category="PT断线", delay=10, output_node="开出6", enabled=True),
|
||||
AlarmRule(category="CT断线", delay=11, output_node="开出7", enabled=False),
|
||||
],
|
||||
)
|
||||
ai_alarm_payload = [
|
||||
AiAlarmSettingIn(
|
||||
channel_no=1,
|
||||
singal_type="4-20mA",
|
||||
limit_low=0,
|
||||
limit_high=20,
|
||||
delay=3,
|
||||
output_node="开出1",
|
||||
enabled=True,
|
||||
).model_dump()
|
||||
]
|
||||
system_payload = SystemConfigIn(time_sync="2026-05-25 12:34:56", brightness=80, screen_saver=60)
|
||||
switch_payload = SwitchControlIn(ch=3, action=1)
|
||||
|
||||
device_result = client.send_device_config(device_payload)
|
||||
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)
|
||||
system_result = client.send_system_config(system_payload)
|
||||
control_result = client.send_switch_control(switch_payload)
|
||||
|
||||
register_map = {address: values for address, values in transport.written_registers}
|
||||
|
||||
assert UART_CONFIG_START_REGISTER in register_map
|
||||
assert len(register_map[UART_CONFIG_START_REGISTER]) == UART_CONFIG_REGISTER_COUNT
|
||||
assert AI_CHANNEL_START_REGISTER in register_map
|
||||
assert len(register_map[AI_CHANNEL_START_REGISTER]) == 30
|
||||
assert AO_CHANNEL_START_REGISTER in register_map
|
||||
assert len(register_map[AO_CHANNEL_START_REGISTER]) == 30
|
||||
assert LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE in register_map
|
||||
assert len(register_map[LINE_ALARM_START_REGISTER + LINE_ALARM_REGISTERS_PER_LINE]) == 14
|
||||
assert AI_ALARM_START_REGISTER in register_map
|
||||
assert len(register_map[AI_ALARM_START_REGISTER]) == 30
|
||||
assert TIME_SYNC_START_REGISTER in register_map
|
||||
assert len(register_map[TIME_SYNC_START_REGISTER]) == 2
|
||||
assert DO_STATE_REGISTER in register_map
|
||||
assert register_map[DO_STATE_REGISTER][0] == 0b100
|
||||
assert transport.written_coils == [(2, True)]
|
||||
|
||||
assert device_result["send_status"] == "成功"
|
||||
assert channel_result["modbus_written"]["ai_registers"] == 30
|
||||
assert line_result["line_no"] == 2
|
||||
assert ai_alarm_result["items"] == 1
|
||||
assert system_result["send_status"] == "成功"
|
||||
assert "执行成功" in control_result["control_status"]
|
||||
205
document/gui_1.h
Normal file
205
document/gui_1.h
Normal file
@ -0,0 +1,205 @@
|
||||
#ifndef __GUI_H
|
||||
#define __GUI_H
|
||||
|
||||
#include "time.h"
|
||||
|
||||
#define LINE_TOTAL_NUM 4 //总测量线路数量
|
||||
#define ANALOG_CH_NUM 12 //模拟量通道数
|
||||
|
||||
|
||||
/***********************************************************************小核数据 -> 大核****************************************************************************/
|
||||
|
||||
//实时量
|
||||
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
|
||||
{
|
||||
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断线,其它事件依次编号
|
||||
}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]; //模拟量输入
|
||||
b_code_t b_code; //B码对时
|
||||
uint16_t a_in; //数字量,开入
|
||||
|
||||
uint16_t evt_f; //事件标志,读取到时间标志后,再读取保持寄存器里的事件内容
|
||||
|
||||
}gui_in_04reg_t;//读输入寄存器数据
|
||||
|
||||
typedef struct
|
||||
{
|
||||
event_t event;
|
||||
|
||||
}gui_in_03reg_t;//读保持寄存器事件数据
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t a_in; //数字量,开入
|
||||
|
||||
}gui_in_02reg_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
|
||||
{
|
||||
uint32_t timestamp; //时间戳,设置时间
|
||||
|
||||
|
||||
com_param_t com_param[4]; //串口参数设置: 小核串口,FPGA串口
|
||||
line_type_t line_param[4]; //线路定值设置: 4路定值一起下发
|
||||
analog_ch_param_t a_in_param[12]; //12路模拟输入通道设置
|
||||
analog_ch_param_t a_out_param[12]; //12路模拟输出通道设置
|
||||
ai_alarm_param_t ai_alarm_param[12];//12路模拟输入 报警参数设置
|
||||
|
||||
}gui_out_03reg_t;//放入保持寄存器中的参数
|
||||
|
||||
typedef struct
|
||||
{
|
||||
|
||||
uint16_t a_in; //数字量,开出
|
||||
|
||||
}gui_out_01reg_t;//放入线圈寄存器中的数
|
||||
|
||||
|
||||
#endif
|
||||
@ -229,7 +229,7 @@ EMCP_BACKUP_DIR=/userdata/emcp/data/backups
|
||||
|
||||
```bash
|
||||
cd /userdata/emcp/backend
|
||||
python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log
|
||||
nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log >/dev/null 2>&1 &
|
||||
```
|
||||
|
||||
验证方式:
|
||||
@ -405,6 +405,7 @@ sudo chown -R linaro:linaro /userdata/emcp/backend/config
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable emcp-backend.service
|
||||
sudo systemctl disable emcp-backend.service
|
||||
sudo systemctl enable emcp-frontend.service
|
||||
sudo systemctl start emcp-backend.service
|
||||
sudo systemctl start emcp-frontend.service
|
||||
|
||||
212
document/显示界面modbus协议寄存器定义.md
Normal file
212
document/显示界面modbus协议寄存器定义.md
Normal file
@ -0,0 +1,212 @@
|
||||
# Modbus 协议寄存器定义文档
|
||||
|
||||
## 一、协议基本配置
|
||||
|
||||
- **协议类型**:Modbus RTU
|
||||
- **从机地址**:1
|
||||
- **串口配置**:
|
||||
- 波特率:115200
|
||||
- 数据位:8
|
||||
- 校验位:无
|
||||
- 停止位:1
|
||||
|
||||
---
|
||||
|
||||
## 二、读数据(功能码:04)
|
||||
|
||||
### 1. 实时显示数据
|
||||
|
||||
- **寄存器类型**:输入寄存器
|
||||
- **功能码**:0x04
|
||||
- **数据格式**:单精度浮点数(32位,占2个寄存器)
|
||||
|
||||
#### 线路 #1
|
||||
- **寄存器地址范围**:0x0000 ~ 0x0032
|
||||
|
||||
| 寄存器地址 | 参数名 | 寄存器地址 | 参数名 | 寄存器地址 | 参数名 |
|
||||
| ---------- | ------ | ---------- | ------ | ---------- | ------ |
|
||||
| 0x0000 | Ua | 0x0012 | Pt | 0x0024 | PFa |
|
||||
| 0x0002 | Ub | 0x0014 | Qa | 0x0026 | PFb |
|
||||
| 0x0004 | Uc | 0x0016 | Qb | 0x0028 | PFc |
|
||||
| 0x0006 | Ia | 0x0018 | Qc | 0x002A | PFt |
|
||||
| 0x0008 | Ib | 0x001A | Qt | 0x002C | Uab |
|
||||
| 0x000A | Ic | 0x001C | Sa | 0x002E | Ubc |
|
||||
| 0x000C | Pa | 0x001E | Sb | 0x0030 | Uca |
|
||||
| 0x000E | Pb | 0x0020 | Sc | 0x0032 | Frq |
|
||||
| 0x0010 | Pc | 0x0022 | St | - | - |
|
||||
|
||||
#### 线路 #2
|
||||
- **起始地址**:0x0034
|
||||
- **结束地址**:0x0066
|
||||
- **参数顺序**:同线路 #1
|
||||
|
||||
#### 线路 #3
|
||||
- **起始地址**:0x0068
|
||||
- **结束地址**:0x009A
|
||||
- **参数顺序**:同线路 #1
|
||||
|
||||
#### 线路 #4
|
||||
- **起始地址**:0x009C
|
||||
- **结束地址**:0x00CE
|
||||
- **参数顺序**:同线路 #1
|
||||
|
||||
---
|
||||
|
||||
### 2. 模拟量输入(AI)
|
||||
|
||||
- **寄存器类型**:输入寄存器
|
||||
- **功能码**:0x04
|
||||
- **数据格式**:单精度浮点数(32位)
|
||||
|
||||
| 寄存器地址 | 输入编号 |
|
||||
| ---------- | -------- |
|
||||
| 0x00D0 | Ai1 |
|
||||
| 0x00D2 | Ai2 |
|
||||
| 0x00D4 | Ai3 |
|
||||
| 0x00D6 | Ai4 |
|
||||
| 0x00D8 | Ai5 |
|
||||
| 0x00DA | Ai6 |
|
||||
| 0x00DC | Ai7 |
|
||||
| 0x00DE | Ai8 |
|
||||
|
||||
---
|
||||
|
||||
### 3. B码对时
|
||||
|
||||
- **寄存器类型**:输入寄存器
|
||||
- **功能码**:0x04
|
||||
- **数据格式**:无符号16位整型
|
||||
|
||||
| 寄存器地址 | 位分配 | 内容 |
|
||||
| ---------- | ------------------------- | ----- |
|
||||
| 0x00E0 | [15:8] = 秒<br>[7:0] = 分 | 秒/分 |
|
||||
| 0x00E1 | [15:8] = 时<br>[7:0] = 日 | 时/日 |
|
||||
| 0x00E2 | [15:8] = 月<br>[7:0] = 年 | 月/年 |
|
||||
|
||||
---
|
||||
|
||||
### 4. 数字量输入(开入)
|
||||
|
||||
- **寄存器类型**:输入寄存器
|
||||
- **功能码**:0x04
|
||||
- **寄存器地址**:0x00E3
|
||||
- **说明**:共16路,每个bit位表示一路开入量
|
||||
|
||||
---
|
||||
|
||||
### 5. 事件总标记
|
||||
|
||||
- **寄存器类型**:输入寄存器
|
||||
- **功能码**:0x04
|
||||
- **寄存器地址**:0x00E4
|
||||
- **说明**:当该位被置位时,表示有新事件,需要读取事件内容
|
||||
|
||||
---
|
||||
|
||||
## 三、读事件(功能码:03)
|
||||
|
||||
- **触发条件**:事件总标记(0x00E4)被置位
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x03
|
||||
- **寄存器地址范围**:0x0000 ~ 0x0003
|
||||
|
||||
| 寄存器地址 | 数据名 | 格式 | 说明 |
|
||||
| ---------- | ---------------- | ---------- | ---------------------------- |
|
||||
| 0x0000 | 时间戳(低16位) | 32位无符号 | 占用0x0000~0x0001 |
|
||||
| 0x0001 | 时间戳(高16位) | | |
|
||||
| 0x0002 | 毫秒计时 | 16位无符号 | |
|
||||
| 0x0003 | 线路号+事件类型 | 16位无符号 | 高字节线路号,低字节事件类型 |
|
||||
|
||||
---
|
||||
|
||||
## 四、读输入数字量(开入)(功能码:02)
|
||||
|
||||
- **寄存器类型**:离散输入
|
||||
- **功能码**:0x02
|
||||
|
||||
| 寄存器地址 | 说明 |
|
||||
| ---------- | --------------------- |
|
||||
| 0x0000 | 16路开入量,每bit一路 |
|
||||
| 0x0001 | 保留(16路) |
|
||||
|
||||
---
|
||||
|
||||
## 五、写数据(功能码:10,写多个保持寄存器)
|
||||
|
||||
### 1. 时间设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x0000
|
||||
- **寄存器长度**:2
|
||||
- **说明**:写入时间戳(32位)
|
||||
|
||||
---
|
||||
|
||||
### 2. 串口设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x0004
|
||||
- **寄存器长度**:16
|
||||
- **说明**:4路 × 4(8字节数据/2) = 16个寄存器
|
||||
|
||||
---
|
||||
|
||||
### 3. 线路定值设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x0014
|
||||
- **寄存器长度**:56
|
||||
- **说明**:4路 × 7类别 × 2(4字节数据/2) = 56个寄存器
|
||||
|
||||
---
|
||||
|
||||
### 4. AI通道设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x004C
|
||||
- **寄存器长度**:30
|
||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
||||
|
||||
---
|
||||
|
||||
### 5. AO通道设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x006A
|
||||
- **寄存器长度**:30
|
||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
||||
|
||||
---
|
||||
|
||||
### 6. AI报警设置
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x0088
|
||||
- **寄存器长度**:30
|
||||
- **说明**:12路 × 5字节数据 / 2 = 30个寄存器(整体下发)
|
||||
|
||||
---
|
||||
|
||||
### 7. 开出(写继电器输出)
|
||||
|
||||
- **寄存器类型**:保持寄存器
|
||||
- **功能码**:0x10
|
||||
- **起始地址**:0x00A6
|
||||
- **寄存器长度**:1
|
||||
- **数据格式**:16位,每一位表示一路开出量
|
||||
|
||||
---
|
||||
|
||||
## 六、写开出(线圈)(功能码:05)
|
||||
|
||||
- **寄存器类型**:线圈
|
||||
- **功能码**:0x05
|
||||
- **起始地址**:0x0000
|
||||
- **寄存器长度**:12
|
||||
- **说明**:单路线圈写操作(12路分别操作)
|
||||
BIN
frontend/dist.zip
Normal file
BIN
frontend/dist.zip
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export const http = axios.create({
|
||||
baseURL: 'http://192.168.1.135:8000/api',
|
||||
baseURL: 'http://127.0.0.1:8000/api',
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
11
scripts/emcp-browser-launcher.desktop
Normal file
11
scripts/emcp-browser-launcher.desktop
Normal file
@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=EMCP Browser
|
||||
Comment=Launch EMCP browser
|
||||
Exec=/userdata/emcp/scripts/start-emcp-browser.sh
|
||||
Path=/userdata/emcp/scripts
|
||||
Icon=web-browser
|
||||
Terminal=false
|
||||
StartupNotify=false
|
||||
Categories=Utility;
|
||||
@ -1,14 +1,3 @@
|
||||
#!/bin/bash
|
||||
LOG_FILE="/userdata/emcp/data/browser-autostart.log"
|
||||
mkdir -p /userdata/emcp/data
|
||||
exec >>"$LOG_FILE" 2>&1
|
||||
|
||||
echo "==== $(date '+%F %T') start-emcp-browser ===="
|
||||
echo "USER=$(whoami)"
|
||||
echo "DISPLAY=${DISPLAY:-}"
|
||||
echo "XDG_SESSION_TYPE=${XDG_SESSION_TYPE:-}"
|
||||
echo "DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-}"
|
||||
|
||||
URL="${EMCP_BROWSER_URL:-http://127.0.0.1:5173/}"
|
||||
BACKEND_HEALTH_URL="${EMCP_BACKEND_HEALTH_URL:-http://127.0.0.1:8000/}"
|
||||
FRONTEND_HEALTH_URL="${EMCP_FRONTEND_HEALTH_URL:-$URL}"
|
||||
@ -73,4 +62,4 @@ chromium \
|
||||
--disable-infobars \
|
||||
--disk-cache-size=10485760 \
|
||||
--user-data-dir="$BROWSER_PROFILE_DIR/chromium" \
|
||||
"$URL" >/dev/null 2>&1 &
|
||||
"$URL" >/dev/null 2>&1 &
|
||||
Loading…
Reference in New Issue
Block a user