Skip to content

Commit ed7f956

Browse files
committed
增加IEC61850客户端自动发现功能
1 parent 1487d74 commit ed7f956

4 files changed

Lines changed: 165 additions & 16 deletions

File tree

src/device/core/device.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def initProtocol(self) -> None:
173173
self.protocol_handler = self._create_protocol_handler()
174174
self.protocol_handler.initialize(self._build_protocol_config())
175175

176+
# IEC61850 客户端: 注册测点发现回调
177+
if self.protocol_type == ProtocolType.Iec61850Client:
178+
self.protocol_handler.set_on_points_discovered(
179+
self._on_iec61850_points_discovered
180+
)
181+
176182
# 添加测点
177183
all_points = self.point_manager.get_all_points()
178184
self.protocol_handler.add_points(all_points)
@@ -228,6 +234,95 @@ def initIec61850Client(self) -> None:
228234
self.protocol_type = ProtocolType.Iec61850Client
229235
self.initProtocol()
230236

237+
def _on_iec61850_points_discovered(self, discovered_points: list) -> None:
238+
"""处理 IEC61850 客户端发现的测点,自动注册到系统
239+
240+
Args:
241+
discovered_points: 发现的测点列表,
242+
每个元素为 {"address": int, "frame_type": int, "ref": str}
243+
"""
244+
frame_type_names = {0: "遥测", 1: "遥信", 2: "遥控", 3: "遥调"}
245+
added_count = 0
246+
slave_id = 1 # IEC61850 默认使用从机地址 1
247+
248+
for dp in discovered_points:
249+
addr = dp["address"]
250+
ft = dp["frame_type"]
251+
ref = dp["ref"]
252+
253+
# 检查是否已存在(根据 address + frame_type 去重)
254+
existing = self.point_manager.find_point_by_address_and_type(addr, ft)
255+
if existing:
256+
continue
257+
258+
# 根据 frame_type 创建对应的 BasePoint 对象
259+
auto_code = str(addr)
260+
ft_label = frame_type_names.get(ft, str(ft))
261+
auto_name = str(addr)
262+
263+
point = None
264+
if ft == 0: # 遥测
265+
point = Yc(
266+
rtu_addr=str(slave_id),
267+
address=str(addr),
268+
func_code=3,
269+
name=auto_name,
270+
code=auto_code,
271+
value=0,
272+
frame_type=0,
273+
)
274+
elif ft == 1: # 遥信
275+
point = Yx(
276+
rtu_addr=str(slave_id),
277+
address=str(addr),
278+
func_code=1,
279+
name=auto_name,
280+
code=auto_code,
281+
value=0,
282+
frame_type=1,
283+
)
284+
elif ft == 2: # 遥控
285+
point = Yk(
286+
rtu_addr=str(slave_id),
287+
address=str(addr),
288+
func_code=5,
289+
name=auto_name,
290+
code=auto_code,
291+
value=0,
292+
frame_type=2,
293+
)
294+
elif ft == 3: # 遥调
295+
point = Yt(
296+
rtu_addr=str(slave_id),
297+
address=str(addr),
298+
func_code=6,
299+
name=auto_name,
300+
code=auto_code,
301+
value=0,
302+
frame_type=3,
303+
)
304+
305+
if point:
306+
# 添加到测点管理器
307+
self.point_manager.add_point(slave_id, point)
308+
309+
# 添加到模拟控制器
310+
self.simulation_controller.add_point(
311+
point, SimulateMethod.Random, 1
312+
)
313+
self.simulation_controller.set_point_status(point, True)
314+
315+
added_count += 1
316+
self.log.info(
317+
f"IEC61850 自动添加测点: {auto_code} ({auto_name}), "
318+
f"address={addr}, frame_type={ft}, ref={ref}"
319+
)
320+
321+
if added_count > 0:
322+
self.log.info(f"IEC61850 自动发现并添加了 {added_count} 个测点")
323+
else:
324+
self.log.info("IEC61850 未发现需要新增的测点(所有测点已存在)")
325+
231326
# ===== 设备启停 =====
232327

233328
async def start(self) -> bool:

src/device/core/point/point_manager.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ def get_points_by_type(self, frame_type: int) -> List[BasePoint]:
9999
result.extend(points)
100100
return result
101101

102+
def find_point_by_address_and_type(self, address: int, frame_type: int) -> Optional[BasePoint]:
103+
"""根据地址和帧类型查找测点
104+
105+
Args:
106+
address: 测点地址
107+
frame_type: 帧类型 (0=遥测, 1=遥信, 2=遥控, 3=遥调)
108+
109+
Returns:
110+
匹配的测点对象,未找到返回 None
111+
"""
112+
points = self.get_points_by_type(frame_type)
113+
for point in points:
114+
if point.address == address:
115+
return point
116+
return None
117+
102118
def get_all_points(self) -> List[BasePoint]:
103119
"""获取所有测点"""
104120
result: List[BasePoint] = []

src/device/protocol/iec61850_handler.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ def __init__(self, log=None):
151151
super().__init__()
152152
self._client = None
153153
self._log = log
154+
self._on_points_discovered = None # 测点发现回调
155+
156+
def set_on_points_discovered(self, callback):
157+
"""设置测点发现回调
158+
159+
Args:
160+
callback: 回调函数,签名为 callback(discovered_points: List[Dict])
161+
每个 dict 包含 {"address": int, "frame_type": int, "ref": str}
162+
"""
163+
self._on_points_discovered = callback
154164

155165
def initialize(self, config: Dict[str, Any]) -> None:
156166
"""初始化 IEC 61850 客户端
@@ -192,6 +202,17 @@ async def connect(self) -> bool:
192202
if self._client:
193203
is_connected = await self._client.connect()
194204
self._is_running = is_connected
205+
206+
# 连接成功后,通知上层发现的测点
207+
if is_connected and self._on_points_discovered:
208+
discovered = self._client.get_discovered_points()
209+
if discovered:
210+
try:
211+
self._on_points_discovered(discovered)
212+
except Exception as e:
213+
if self._log:
214+
self._log.error(f"处理发现的测点时出错: {e}")
215+
195216
return is_connected
196217
return False
197218
except Exception as e:

src/proto/iec61850/iec61850_client.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,18 @@ def _get_list_from_linked_list(self, linked_list) -> List[str]:
232232
iec61850.LinkedList_destroy(linked_list)
233233
return items
234234

235-
def discover_model(self):
236-
"""动态发现并映射服务端的数据模型"""
235+
def discover_model(self) -> List[Dict[str, Any]]:
236+
"""动态发现并映射服务端的数据模型
237+
238+
Returns:
239+
发现的测点列表,每个元素为 {"address": int, "frame_type": int, "ref": str}
240+
"""
237241
if not self._connection or not self._is_connected:
238-
return
242+
return []
239243

240244
log.info("开始 IEC 61850 动态模型发现...")
241245
start_time = time.time()
246+
discovered_points: List[Dict[str, Any]] = []
242247

243248
# 1. 获取逻辑设备列表
244249
result = iec61850.IedConnection_getLogicalDeviceList(self._connection)
@@ -247,12 +252,11 @@ def discover_model(self):
247252

248253
if error != iec61850.IED_ERROR_OK:
249254
log.error(f"发现模型失败: 无法获取逻辑设备列表 (错误码: {error})")
250-
return
255+
return []
251256

252257
lds = self._get_list_from_linked_list(ld_list)
253258
log.info(f"发现逻辑设备: {lds}")
254259

255-
discovered_count = 0
256260
for ld in lds:
257261
# 2. 获取逻辑节点列表 (使用 getLogicalDeviceDirectory)
258262
result = iec61850.IedConnection_getLogicalDeviceDirectory(self._connection, ld)
@@ -269,7 +273,7 @@ def discover_model(self):
269273
ln_ref = f"{ld}/{ln}"
270274

271275
# 3. 获取数据对象列表 (使用 getLogicalNodeDirectory)
272-
result = iec61850.IedConnection_getLogicalNodeDirectory(self._connection, ln_ref, 1) # 1 = ACSI_DIR_DO
276+
result = iec61850.IedConnection_getLogicalNodeDirectory(self._connection, ln_ref, 0) # 0 = ACSI_CLASS_DATA_OBJECT
273277
do_list = result[0] if isinstance(result, (list, tuple)) else result
274278
error = result[1] if isinstance(result, (list, tuple)) else 0
275279

@@ -284,33 +288,46 @@ def discover_model(self):
284288

285289
try:
286290
if ln == "MMXU1" and do.startswith("MV_"):
287-
addr = int(do[3:])
291+
addr = do[3:]
288292
ref = f"{full_do_ref}.mag.f"
289293
self._point_refs[(addr, 0)] = ref
290-
discovered_count += 1
294+
discovered_points.append({"address": addr, "frame_type": 0, "ref": ref})
291295
log.info(f"映射测点: ({addr}, 0) -> {ref}")
292296
elif ln == "GGIO1" and do.startswith("SPS_"):
293-
addr = int(do[4:])
297+
addr = do[4:]
294298
ref = f"{full_do_ref}.stVal"
295299
self._point_refs[(addr, 1)] = ref
296-
discovered_count += 1
300+
discovered_points.append({"address": addr, "frame_type": 1, "ref": ref})
297301
log.info(f"映射测点: ({addr}, 1) -> {ref}")
298302
elif ln == "GGIO1" and do.startswith("SPC_"):
299-
addr = int(do[4:])
303+
addr = do[4:]
300304
ref = f"{full_do_ref}.ctlVal"
301305
self._point_refs[(addr, 2)] = ref
302-
discovered_count += 1
306+
discovered_points.append({"address": addr, "frame_type": 2, "ref": ref})
303307
log.info(f"映射测点: ({addr}, 2) -> {ref}")
304308
elif ln == "GGIO2" and do.startswith("APC_"):
305-
addr = int(do[4:])
309+
addr = do[4:]
306310
ref = f"{full_do_ref}.ctlVal"
307311
self._point_refs[(addr, 3)] = ref
308-
discovered_count += 1
312+
discovered_points.append({"address": addr, "frame_type": 3, "ref": ref})
309313
log.info(f"映射测点: ({addr}, 3) -> {ref}")
310-
except ValueError:
314+
except Exception as e:
315+
log.error(f"解析测点地址失败: {do}, 错误: {e}")
311316
continue
312317

313-
log.info(f"IEC 61850 动态发现完成, 耗时: {time.time() - start_time:.2f}s, 发现并映射了 {discovered_count} 个测点")
318+
log.info(f"IEC 61850 动态发现完成, 耗时: {time.time() - start_time:.2f}s, 发现并映射了 {len(discovered_points)} 个测点")
319+
return discovered_points
320+
321+
def get_discovered_points(self) -> List[Dict[str, Any]]:
322+
"""获取当前已映射的测点列表
323+
324+
Returns:
325+
测点列表,每个元素为 {"address": int, "frame_type": int, "ref": str}
326+
"""
327+
return [
328+
{"address": addr, "frame_type": ft, "ref": ref}
329+
for (addr, ft), ref in self._point_refs.items()
330+
]
314331

315332
def browse_logical_devices(self) -> List[str]:
316333
"""浏览远端 IED 的逻辑设备列表"""

0 commit comments

Comments
 (0)