Skip to content

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()
"

参数配置速查

参数标准默认值最小值最大值说明
k12132767发送窗口
w8132767接收窗口(≤ 2/3 k)
t030s1s255s连接建立超时
t115s1s255s发送超时
t210s1s255s确认超时(< t1)
t320s1s172800s测试帧超时

褚成志的IoT笔记