| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- import socket
- import struct
- import threading
- import time
- from datetime import datetime
- class IEC104Client:
- def __init__(self, host, port):
- self.host = host
- self.port = port
- self.socket = None
- self.connected = False
- self.running = False
- # IEC 104 协议常量
- self.START_BYTE = 0x68
- self.TESTFR_ACT = b'\x68\x04\x43\x00\x00\x00'
- self.TESTFR_CON = b'\x68\x04\x83\x00\x00\x00'
- self.STARTDT_ACT = b'\x68\x04\x07\x00\x00\x00'
- self.STARTDT_CON = b'\x68\x04\x0B\x00\x00\x00'
- self.STOPDT_ACT = b'\x68\x04\x13\x00\x00\x00'
- self.send_seq = 0
- self.recv_seq = 0
- def connect(self):
- """连接到IEC 104服务器"""
- try:
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.settimeout(10)
- self.socket.connect((self.host, self.port))
- self.connected = True
- print(f"成功连接到 {self.host}:{self.port}")
- return True
- except Exception as e:
- print(f"连接失败: {e}")
- return False
- def disconnect(self):
- """断开连接"""
- self.running = False
- if self.socket:
- try:
- # 发送停止数据传输命令
- self.socket.send(self.STOPDT_ACT)
- self.socket.close()
- except:
- pass
- self.connected = False
- print("连接已断开")
- def send_startdt(self):
- """发送启动数据传输命令"""
- try:
- self.socket.send(self.STARTDT_ACT)
- print("发送启动数据传输命令")
- except Exception as e:
- print(f"发送启动命令失败: {e}")
- def send_testfr(self):
- """发送测试帧"""
- try:
- self.socket.send(self.TESTFR_ACT)
- print("发送测试帧")
- except Exception as e:
- print(f"发送测试帧失败: {e}")
- def send_s_frame(self):
- """发送S格式监视帧确认"""
- try:
- # S格式帧:68 04 01 00 + 接收序号
- s_frame = b'\x68\x04\x01\x00' + struct.pack('<H', (self.recv_seq << 1))
- self.socket.send(s_frame)
- except Exception as e:
- print(f"发送S帧失败: {e}")
- def parse_apdu(self, data):
- """解析APDU(应用协议数据单元)"""
- if len(data) < 6:
- return None
- # 检查起始字节
- if data[0] != self.START_BYTE:
- return None
- length = data[1]
- # U格式帧(无编号控制功能)
- if length == 4 and len(data) >= 6:
- ctrl1 = data[2]
- ctrl2 = data[3]
- ctrl3 = data[4]
- ctrl4 = data[5]
- if ctrl1 == 0x07 and ctrl2 == 0x00:
- return {"type": "STARTDT_ACT", "description": "启动数据传输激活"}
- elif ctrl1 == 0x0B and ctrl2 == 0x00:
- return {"type": "STARTDT_CON", "description": "启动数据传输确认"}
- elif ctrl1 == 0x43 and ctrl2 == 0x00:
- return {"type": "TESTFR_ACT", "description": "测试帧激活"}
- elif ctrl1 == 0x83 and ctrl2 == 0x00:
- return {"type": "TESTFR_CON", "description": "测试帧确认"}
- # I格式帧(信息传输)
- elif length > 4:
- return self.parse_i_frame(data)
- return {"type": "UNKNOWN", "data": data.hex()}
- def parse_i_frame(self, data):
- """解析I格式帧"""
- if len(data) < 8:
- return None
- # 提取序列号
- send_seq = struct.unpack('<H', data[2:4])[0] >> 1
- recv_seq = struct.unpack('<H', data[4:6])[0] >> 1
- # ASDU(应用服务数据单元)
- if len(data) > 6:
- asdu = data[6:]
- print('报文:', data.hex())
- return self.parse_asdu(asdu, send_seq, recv_seq)
- return {
- "type": "I_FRAME",
- "send_seq": send_seq,
- "recv_seq": recv_seq,
- "data": data[6:].hex() if len(data) > 6 else ""
- }
- def parse_asdu(self, asdu_data, send_seq, recv_seq):
- """解析ASDU"""
- if len(asdu_data) < 6:
- return {"type": "I_FRAME", "error": "ASDU too short"}
- type_id = asdu_data[0]
- vsq = asdu_data[1] # 变量结构限定词
- cot = struct.unpack('<H', asdu_data[2:4])[0] # 传送原因(2字节)
- common_addr = struct.unpack('<H', asdu_data[4:6])[0] # 公共地址(2字节)
- # 信息对象地址根据VSQ确定
- num_objects = vsq & 0x7F
- sq = (vsq & 0x80) >> 7 # 序列标志
- result = {
- "type": "I_FRAME",
- "send_seq": send_seq,
- "recv_seq": recv_seq,
- "type_id": type_id,
- "type_description": self.get_type_description(type_id),
- "vsq": vsq,
- "num_objects": num_objects,
- "sq": sq,
- "cot": cot,
- "cot_description": self.get_cot_description(cot & 0x3F), # 只取低6位
- "common_addr": common_addr,
- "data_values": [],
- # "raw_data": asdu_data[6:].hex() if len(asdu_data) > 6 else "",
- "raw_data": asdu_data.hex() if len(asdu_data) > 6 else ""
- }
- # 解析数据值
- if len(asdu_data) > 6:
- data_part = asdu_data[6:]
- # 根据类型ID解析具体数据
- if type_id == 1: # 单点信息
- result["data_values"] = self.parse_single_point_info(data_part, num_objects, sq)
- elif type_id == 3: # 双点信息
- result["data_values"] = self.parse_double_point_info(data_part, num_objects, sq)
- elif type_id == 9: # 测量值,标准化值
- result["data_values"] = self.parse_normalized_value(data_part, num_objects, sq)
- elif type_id == 11: # 测量值,标度化值
- result["data_values"] = self.parse_scaled_value(data_part, num_objects, sq)
- elif type_id == 13: # 测量值,浮点数
- result["data_values"] = self.parse_float_value(data_part, num_objects, sq)
- return result
- def get_type_description(self, type_id):
- """获取类型标识描述"""
- type_descriptions = {
- 1: "单点信息",
- 2: "带时标的单点信息",
- 3: "双点信息",
- 4: "带时标的双点信息",
- 5: "步位置信息",
- 6: "带时标的步位置信息",
- 7: "32位串信息",
- 8: "带时标的32位串信息",
- 9: "测量值,标准化值",
- 10: "带时标的测量值,标准化值",
- 11: "测量值,标度化值",
- 12: "带时标的测量值,标度化值",
- 13: "测量值,浮点数",
- 14: "带时标的测量值,浮点数",
- 30: "带时标的单点信息",
- 36: "测量值,浮点数带时标",
- 100: "总召唤命令",
- 101: "计数量召唤命令"
- }
- return type_descriptions.get(type_id, f"未知类型 ({type_id})")
- def get_cot_description(self, cot):
- """获取传送原因描述"""
- cot_descriptions = {
- 1: "周期循环",
- 2: "背景扫描",
- 3: "自发",
- 4: "初始化",
- 5: "请求",
- 6: "激活",
- 7: "激活确认",
- 8: "停止激活",
- 9: "停止激活确认",
- 10: "激活结束",
- 20: "响应站召唤"
- }
- return cot_descriptions.get(cot, f"未知原因 ({cot})")
- def parse_single_point_info(self, data, num_objects, sq):
- """解析单点信息"""
- values = []
- pos = 0
- if sq == 0: # 非连续地址
- for i in range(num_objects):
- if pos + 3 <= len(data):
- addr = struct.unpack('<H', data[pos:pos + 2])[0]
- value = data[pos + 2] & 0x01
- quality = data[pos + 3]
- values.append({
- "address": addr,
- "value": bool(value),
- "quality": quality
- })
- pos += 4
- else: # 连续地址
- if len(data) >= 2:
- base_addr = struct.unpack('<H', data[0:2])[0]
- pos = 2
- for i in range(num_objects):
- if pos + 1 <= len(data):
- value = data[pos] & 0x01
- quality = data[pos + 1]
- values.append({
- "address": base_addr + i,
- "value": bool(value),
- "quality": quality
- })
- pos += 2
- return values
- def parse_double_point_info(self, data, num_objects, sq):
- """解析双点信息"""
- values = []
- pos = 0
- if sq == 0: # 非连续地址
- for i in range(num_objects):
- if pos + 3 <= len(data):
- addr = struct.unpack('<H', data[pos:pos + 2])[0]
- value = data[pos + 2] & 0x03
- quality = data[pos + 3]
- values.append({
- "address": addr,
- "value": value,
- "quality": quality
- })
- pos += 4
- else: # 连续地址
- if len(data) >= 2:
- base_addr = struct.unpack('<H', data[0:2])[0]
- pos = 2
- for i in range(num_objects):
- if pos + 1 <= len(data):
- value = data[pos] & 0x03
- quality = data[pos + 1]
- values.append({
- "address": base_addr + i,
- "value": value,
- "quality": quality
- })
- pos += 2
- return values
- def parse_normalized_value(self, data, num_objects, sq):
- """解析标准化值"""
- values = []
- pos = 0
- if sq == 0: # 非连续地址
- for i in range(num_objects):
- if pos + 6 <= len(data):
- addr = struct.unpack('<H', data[pos:pos + 2])[0]
- # 标准化值范围是-1.0到1.0,对应-32768到32767
- raw_value = struct.unpack('<h', data[pos + 2:pos + 4])[0]
- value = raw_value / 32768.0 # 转换为-1.0到1.0范围
- quality = data[pos + 4]
- values.append({
- "address": addr,
- "value": round(value, 6), # 保留6位小数
- "quality": quality,
- "raw_value": raw_value # 保留原始值用于调试
- })
- pos += 6
- else: # 连续地址
- if len(data) >= 2:
- base_addr = struct.unpack('<H', data[0:2])[0]
- pos = 2
- for i in range(num_objects):
- if pos + 3 <= len(data):
- raw_value = struct.unpack('<h', data[pos:pos + 2])[0]
- value = raw_value / 32768.0
- quality = data[pos + 2]
- values.append({
- "address": base_addr + i,
- "value": round(value, 6),
- "quality": quality,
- "raw_value": raw_value
- })
- pos += 3
- return values
- def parse_scaled_value(self, data, num_objects, sq):
- """解析标度化值"""
- values = []
- pos = 0
- if sq == 0: # 非连续地址
- for i in range(num_objects):
- if pos + 6 <= len(data):
- addr = struct.unpack('<H', data[pos:pos + 2])[0]
- # 标度化值实际上是4字节,包含2字节值和2字节质量描述
- value = struct.unpack('<h', data[pos + 2:pos + 4])[0]
- quality = struct.unpack('<H', data[pos + 4:pos + 6])[0]
- # 将值转换为模拟器显示的格式
- scaled_value = value >> 7 # 取高8位作为显示值
- values.append({
- "address": addr,
- "value": scaled_value,
- "quality": quality & 0xFF # 只取低8位质量位
- })
- pos += 6
- else: # 连续地址
- if len(data) >= 2:
- base_addr = struct.unpack('<H', data[0:2])[0]
- pos = 2
- for i in range(num_objects):
- if pos + 4 <= len(data):
- value = struct.unpack('<h', data[pos:pos + 2])[0]
- quality = struct.unpack('<H', data[pos + 2:pos + 4])[0]
- scaled_value = value >> 8
- values.append({
- "address": base_addr + i,
- "value": scaled_value,
- "quality": quality & 0xFF
- })
- pos += 4
- return values
- def parse_float_value(self, data, num_objects, sq):
- """解析浮点数值"""
- values = []
- pos = 0
- if sq == 0: # 非连续地址
- # 每个信息对象:3字节地址 + 4字节浮点数 + 1字节质量
- for i in range(num_objects):
- if pos + 8 <= len(data):
- # 3字节地址(小端序)
- addr = struct.unpack('<I', data[pos:pos + 3] + b'\x00')[0]
- # 4字节IEEE754浮点数(小端序)
- value_bytes = data[pos + 3:pos + 7]
- value = struct.unpack('<f', value_bytes)[0]
- quality = data[pos + 7]
- # 检查数值有效性
- if not (value != value): # 检查NaN
- values.append({
- "address": addr,
- "value": round(value, 6),
- "quality": quality,
- "raw_bytes": value_bytes.hex()
- })
- pos += 8
- else: # 连续地址
- if len(data) >= 3:
- # 第一个信息对象地址
- base_addr = struct.unpack('<I', data[0:3] + b'\x00')[0]
- pos = 3
- # 后续对象只有数值和质量
- for i in range(num_objects):
- if pos + 5 <= len(data):
- value_bytes = data[pos:pos + 4]
- value = struct.unpack('<f', value_bytes)[0]
- quality = data[pos + 4]
- if not (value != value): # 检查NaN
- values.append({
- "address": base_addr + i,
- "value": round(value, 6),
- "quality": quality,
- "raw_bytes": value_bytes.hex()
- })
- pos += 5
- return values
- def receive_data(self):
- """接收数据的主循环"""
- buffer = b''
- while self.running:
- try:
- data = self.socket.recv(1024)
- if not data:
- print("连接被服务器关闭")
- break
- buffer += data
- # 处理缓冲区中的完整帧
- while len(buffer) >= 2:
- if buffer[0] != self.START_BYTE:
- buffer = buffer[1:]
- continue
- if len(buffer) < 2:
- break
- frame_length = buffer[1] + 2 # 长度字段不包括起始字节和长度字节本身
- if len(buffer) < frame_length:
- break
- frame = buffer[:frame_length]
- buffer = buffer[frame_length:]
- # 解析并显示帧
- parsed = self.parse_apdu(frame)
- if parsed:
- self.display_data(parsed)
- # 发送S格式确认帧(监视功能)
- if parsed.get("type") == "I_FRAME":
- self.send_s_frame()
- # 更新接收序号
- if parsed.get("type") == "I_FRAME" and "send_seq" in parsed:
- self.recv_seq = parsed["send_seq"] + 1
- # 自动回复确认帧
- if parsed and parsed.get("type") == "TESTFR_ACT":
- self.socket.send(self.TESTFR_CON)
- except socket.timeout:
- continue
- except Exception as e:
- print(f"接收数据错误: {e}")
- break
- def send_general_interrogation(self):
- """发送总召命令(C_IC_NA_1)"""
- try:
- # 构造总召命令帧
- # ASDU: 类型标识=100, VSQ=1, COT=6(激活), 公共地址, 信息对象地址=0, QOI=20(站召唤)
- asdu = struct.pack('<BBHHIB',
- 100, # 类型标识:总召命令
- 1, # VSQ:1个信息对象
- 6, # COT:激活
- self.common_addr if hasattr(self, 'common_addr') else 1, # 公共地址
- 0, # 信息对象地址
- 20 # QOI:站召唤
- )
- # 构造I格式帧
- frame_length = len(asdu) + 4
- i_frame = struct.pack('<BBH',
- self.START_BYTE,
- frame_length,
- (self.send_seq << 1) # 发送序号
- ) + struct.pack('<H', (self.recv_seq << 1)) + asdu # 接收序号 + ASDU
- self.socket.send(i_frame)
- self.send_seq += 1
- print(f"发送总召命令 (发送序号: {self.send_seq - 1})")
- except Exception as e:
- print(f"发送总召命令失败: {e}")
- def send_clock_sync(self):
- """发送时钟同步命令"""
- try:
- # 获取当前时间
- now = datetime.now()
- # CP56Time2a格式:毫秒(2) + 分钟(1) + 小时(1) + 日期(3)
- ms = (now.second * 1000 + now.microsecond // 1000) & 0xFFFF
- minute = now.minute & 0x3F
- hour = now.hour & 0x1F
- day = now.day & 0x1F
- month = now.month & 0x0F
- year = (now.year - 2000) & 0x7F
- time_bytes = struct.pack('<HBBBB', ms, minute, hour,
- (month << 5) | day, year)
- # 构造时钟同步ASDU
- asdu = struct.pack('<BBHHI',
- 103, # 类型标识:时钟同步命令
- 1, # VSQ
- 6, # COT:激活
- self.common_addr if hasattr(self, 'common_addr') else 1,
- 0 # 信息对象地址
- ) + time_bytes
- frame_length = len(asdu) + 4
- i_frame = struct.pack('<BBH',
- self.START_BYTE,
- frame_length,
- (self.send_seq << 1)
- ) + struct.pack('<H', (self.recv_seq << 1)) + asdu
- self.socket.send(i_frame)
- self.send_seq += 1
- print(f"发送时钟同步命令")
- except Exception as e:
- print(f"发送时钟同步命令失败: {e}")
- def display_data(self, parsed_data):
- """显示解析后的数据"""
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- print(f"\n[{timestamp}] 接收到数据:")
- print(f"类型: {parsed_data.get('type', 'UNKNOWN')}")
- if 'description' in parsed_data:
- print(f"描述: {parsed_data['description']}")
- if 'type_description' in parsed_data:
- print(f"ASDU类型: {parsed_data['type_description']} (ID: {parsed_data['type_id']})")
- print(f"传送原因: {parsed_data['cot_description']} ({parsed_data['cot']})")
- print(f"公共地址: {parsed_data['common_addr']}")
- if 'num_objects' in parsed_data:
- print(f"信息对象数量: {parsed_data['num_objects']}, SQ: {parsed_data.get('sq', 0)}")
- if parsed_data.get('data_values'):
- print("数据值:")
- valid_values = [v for v in parsed_data['data_values'] if v.get('value') is not None]
- for i, value in enumerate(valid_values[:10]): # 显示前10个有效值
- quality_desc = self.get_quality_description(value.get('quality', 0))
- raw_info = f" [原始: {value.get('raw_bytes', '')}]" if 'raw_bytes' in value else ""
- print(f" 地址 {value.get('address', 'N/A')}: {value.get('value', 'N/A')} {quality_desc}{raw_info}")
- if len(valid_values) > 10:
- print(f" ... 还有 {len(valid_values) - 10} 个有效数据点")
- if len(parsed_data['data_values']) > len(valid_values):
- print(f" (过滤了 {len(parsed_data['data_values']) - len(valid_values)} 个异常值)")
- # 显示原始数据用于调试
- if 'raw_data' in parsed_data and parsed_data['raw_data']:
- print(f"原始数据: {parsed_data['raw_data']}")
- # 记录公共地址用于总召
- if 'common_addr' in parsed_data:
- self.common_addr = parsed_data['common_addr']
- def get_quality_description(self, quality):
- """获取质量描述符"""
- if quality == 0:
- return "(良好)"
- elif quality & 0x80:
- return "(无效)"
- elif quality & 0x40:
- return "(未更新)"
- elif quality & 0x20:
- return "(被取代)"
- elif quality & 0x10:
- return "(被阻塞)"
- else:
- return f"(质量: {quality})"
- print("-" * 50)
- def start(self):
- """启动客户端"""
- max_retries = 5
- retry_count = 0
- while retry_count < max_retries:
- if not self.connect():
- retry_count += 1
- if retry_count < max_retries:
- print(f"连接失败,{5}秒后重试 ({retry_count}/{max_retries})")
- time.sleep(5)
- continue
- else:
- print("达到最大重试次数,退出程序")
- return
- self.running = True
- retry_count = 0 # 重置重试计数
- # 启动接收线程
- receive_thread = threading.Thread(target=self.receive_data)
- receive_thread.daemon = True
- receive_thread.start()
- # 发送启动数据传输命令
- time.sleep(1)
- self.send_startdt()
- # 等待启动确认后发送总召
- time.sleep(2)
- self.send_general_interrogation()
- try:
- # 主循环 - 定期发送测试帧和总召
- last_gi_time = time.time()
- while self.running and self.connected:
- current_time = time.time()
- # 每5分钟发送一次总召
- if current_time - last_gi_time >= 300: # 300秒 = 5分钟
- self.send_general_interrogation()
- last_gi_time = current_time
- # 每30秒发送一次测试帧
- time.sleep(30)
- if self.connected:
- self.send_testfr()
- # 检查接收线程是否还活着
- if not receive_thread.is_alive():
- print("接收线程已停止,准备重连")
- break
- except KeyboardInterrupt:
- print("\n程序被用户中断")
- break
- finally:
- self.disconnect()
- # 如果不是用户中断,则尝试重连
- if self.running:
- print("连接断开,5秒后尝试重连...")
- time.sleep(5)
- def main():
- # 配置参数
- HOST = "192.168.50.242"
- PORT = 2404
- print(f"启动IEC 104客户端")
- print(f"目标服务器: {HOST}:{PORT}")
- print("按 Ctrl+C 退出程序\n")
- client = IEC104Client(HOST, PORT)
- client.start()
- if __name__ == "__main__":
- main()
|