IEC 104 故障处理案例
案例一:连接频繁断开,T1 超时
故障现象
储能电站与调度中心的 IEC 104 连接每隔 15-30 分钟断开一次,日志显示 T1 超时(发送超时)。
排查过程
bash
# 查看 IEC 104 日志
grep "T1 timeout\|connection lost" /var/log/iec104.log | tail -20
# 2024-01-15 14:23:45 T1 timeout, closing connection
# 2024-01-15 14:23:46 Reconnecting to 10.1.1.100:2404
# 抓包分析
tcpdump -i eth0 -w iec104.pcap host 10.1.1.100 and port 2404
# 用 Wireshark 分析:
# 发现大量 I 帧发出后,对端长时间未回 S 帧确认
# 网络延迟:平均 800ms,峰值 12000ms!
# 检查网络质量
ping -c 100 10.1.1.100 | tail -5
# 100 packets transmitted, 98 received, 2% packet loss
# rtt min/avg/max/mdev = 45.2/823.4/12456.7/2341.2 ms
# 网络延迟极不稳定(调度专线质量差)解决方案
python
# 调整 IEC 104 超时参数,适应高延迟网络
ADJUSTED_PARAMS = {
'k': 12,
'w': 8,
't0': 60, # 连接超时从 30s 增加到 60s
't1': 60, # 发送超时从 15s 增加到 60s(关键!)
't2': 30, # 确认超时从 10s 增加到 30s
't3': 120, # 测试帧超时从 20s 增加到 120s
}
# 同时向网络运维反馈,要求修复专线质量
# 正常调度专线延迟应 < 100ms案例二:总召唤后数据不完整
故障现象
主站发送总召唤后,子站响应的遥测数据只有 60%,部分 IOA 的数据缺失。
排查过程
python
# 在主站侧记录收到的 IOA
received_ioas = set()
def on_asdu_received(connection, asdu, param):
cot = asdu.getCOT()
if cot in [CauseOfTransmission.INTERROGATED_BY_STATION,
CauseOfTransmission.SPONTANEOUS]:
for i in range(asdu.getNumberOfElements()):
io = asdu.getElement(i)
received_ioas.add(io.getObjectAddress())
# 总召唤结束后检查
expected_ioas = set(range(2001, 2050)) # 期望的 IOA 范围
missing_ioas = expected_ioas - received_ioas
print(f"Missing IOAs: {missing_ioas}")
# Missing IOAs: {2015, 2016, 2017, 2031, 2032}
# 检查子站日志
# 发现子站在发送这些 IOA 时,对应的传感器读取超时
# 子站代码:如果传感器读取失败,直接跳过该 IOA解决方案
python
# 子站修复:传感器失败时发送带 Invalid 质量标志的数据
def send_measurement_with_quality(self, ioa: int, value: float,
quality_flags: int):
"""
quality_flags:
0x00 = 正常
0x01 = Overflow(溢出)
0x04 = Time Invalid(时标无效)
0x08 = Blocked(被封锁)
0x10 = Substituted(替代值)
0x20 = Non-topical(非当前值)
0x40 = Invalid(无效)
"""
asdu = ASDU.create(TypeID.M_ME_NC_1, False,
CauseOfTransmission.INTERROGATED_BY_STATION,
0, self.ca, False, False)
io = MeasuredValueShort.create(ioa, value, quality_flags)
asdu.addInformationObject(io)
self.connection.sendASDU(asdu)
# 传感器失败时
try:
value = read_sensor(ioa)
send_measurement_with_quality(ioa, value, 0x00) # 正常
except SensorException:
send_measurement_with_quality(ioa, 0.0, 0x40) # Invalid案例三:遥控命令执行后无激活终止
故障现象
主站发送遥控执行命令后,收到激活确认(COT=7),但长时间未收到激活终止(COT=10),主站超时报错。
排查过程
python
# 主站侧超时等待逻辑
class CommandTracker:
TERMINATION_TIMEOUT = 30 # 等待激活终止的超时
def __init__(self):
self.pending_commands = {}
def on_activation_confirm(self, ioa):
self.pending_commands[ioa] = {
'confirmed_at': time.time(),
'terminated': False
}
# 启动超时检查
threading.Timer(
self.TERMINATION_TIMEOUT,
self._check_termination,
args=[ioa]
).start()
def on_activation_termination(self, ioa):
if ioa in self.pending_commands:
self.pending_commands[ioa]['terminated'] = True
elapsed = time.time() - self.pending_commands[ioa]['confirmed_at']
logging.info(f"Command {ioa} terminated in {elapsed:.1f}s")
def _check_termination(self, ioa):
cmd = self.pending_commands.get(ioa)
if cmd and not cmd['terminated']:
logging.error(f"Command {ioa}: no termination received within {self.TERMINATION_TIMEOUT}s")
# 查询设备实际状态
self.query_device_status(ioa)python
# 子站修复:确保发送激活终止
def execute_control(self, ioa: int, value: bool) -> bool:
try:
# 执行实际控制操作
success = self.hardware_control(ioa, value)
# 等待设备响应(最多 10 秒)
deadline = time.time() + 10
while time.time() < deadline:
actual_state = self.read_feedback(ioa)
if actual_state == value:
# 控制成功,发送激活终止
self.send_activation_termination(ioa, value)
return True
time.sleep(0.5)
# 超时,发送负确认
self.send_negative_confirm_termination(ioa)
return False
except Exception as e:
logging.error(f"Control execution failed: {e}")
self.send_negative_confirm_termination(ioa)
return False案例四:时标异常,数据时序混乱
故障现象
历史数据库中,同一设备的数据时序混乱,部分数据时标比实际时间早 8 小时(时区问题)。
根本原因
子站使用本地时间(UTC+8)生成 CP56Time2a 时标,但 IEC 104 标准要求使用 UTC 时间。
解决方案
python
import time
from datetime import datetime, timezone
def create_cp56time2a_utc() -> CP56Time2a:
"""
创建 CP56Time2a 时标(UTC 时间)
IEC 104 标准:时标使用 UTC,不含时区偏移
"""
utc_now = datetime.now(timezone.utc)
utc_ms = int(utc_now.timestamp() * 1000)
return CP56Time2a.createFromMsTimestamp(utc_ms)
# 错误做法(使用本地时间)
def bad_timestamp():
local_ms = int(time.time() * 1000) # 如果系统时区是 UTC+8,会有 8 小时偏差
return CP56Time2a.createFromMsTimestamp(local_ms)
# 正确做法
def good_timestamp():
# time.time() 返回 UTC 时间戳,直接使用
utc_ms = int(time.time() * 1000)
return CP56Time2a.createFromMsTimestamp(utc_ms)
# 确保系统时区配置正确
# /etc/timezone 应为 UTC 或通过 NTP 同步常用诊断方法
bash
# Wireshark 过滤 IEC 104 流量
# 过滤器:iec60870_104 或 tcp.port == 2404
# 关键帧类型识别:
# U 帧:STARTDT act/con, STOPDT act/con, TESTFR act/con
# S 帧:纯确认帧
# I 帧:数据帧(含 ASDU)
# 检查序号连续性:
# N(S) 应单调递增,N(R) 应及时确认
# 常见问题特征:
# 大量重传 → 网络丢包或 T1 超时
# N(S) 跳变 → 连接重置
# 长时间无 S 帧 → 接收方处理慢或 T2 配置过大
# 测试连接
nc -zv 10.1.1.100 2404
# 或使用专用测试工具
python3 -c "
from lib60870 import Connection
conn = Connection.create('10.1.1.100', 2404)
conn.connect()
print('Connected successfully')
conn.close()
"参数配置速查
| 参数 | 标准默认值 | 最小值 | 最大值 | 说明 |
|---|---|---|---|---|
| k | 12 | 1 | 32767 | 发送窗口 |
| w | 8 | 1 | 32767 | 接收窗口(≤ 2/3 k) |
| t0 | 30s | 1s | 255s | 连接建立超时 |
| t1 | 15s | 1s | 255s | 发送超时 |
| t2 | 10s | 1s | 255s | 确认超时(< t1) |
| t3 | 20s | 1s | 172800s | 测试帧超时 |