基于TCP的光伏制氢通信接口
一、通信协议设计
参考模型: 基于GB/T18657.3-2002规定的三层参考模型“增强性能体系结构”。
字节格式: 帧的基本单元为8位字节。链路层传输顺序为低位在前,高位在后;低字节在前,高字节在后。
1.1 帧格式
帧格式定义: 本标准采用GB/T18657.1的6.2.4条 FT1.2异步式传输帧格式,定义见图 1
传输规则
a) 线路空闲状态为二进制1。
b) 帧的字符之间无线路空闲间隔;两帧之间的线路空闲间隔最少需33位。
c) 如按e)检出了差错,两帧之间的线路空闲间隔最少需33位。
d) 帧校验和(CS)是用户数据区的八位位组的算术和,不考虑进位位。
e) 接收方校验:
(1) 对于每个字符:校验起动位、停止位、偶校验位。
(2) 对于每帧:
l 检验帧的固定报文头中的开头和结束所规定的字符以及协议.标识位;
l 识别2个长度L;
l 每帧接收的字符数为用户数据长度L1+8;
l 帧校验和;
l 结束字符;
l 校验出一个差错时,校验按c)的线路空闲间隔;
若这些校验有一个失败,舍弃此帧;若无差错,则此帧数据有效。
长度L:长度L包括协议.标识和用户数据长度,由2字节组成,如图 2:
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
D15 | D14 | D13 | D12 | D11 | D10 | D9 | D8 |
协议标识由图 2中D0~D1两位编码表示,定义如下:
——D0=0、D1=0:为禁用;
——D0=1、D1=0:为保留;
——D0=0、D1=1:为本协议使用;
——D0=1、D1=1,为保留。
用户数据长度L1:由D2~D15组成,采用 BIN编码,是控制域、地址域、链路用户数据(应用层)的字节总数。
——采用专用无线数传信道,长度L1不大于255;
——采用网络传输,长度L1不大于16383。
控制域C: 控制域C表示报文传输方向和所提供的传输服务类型的信息,定义见图 3:
传输方向位DIR: DIR=0:表示此帧报文是由服务提供方发出的报文; DIR=1:表示此帧报文是由服务请求方发出的报文。
启动标志位: PRM =1:表示此帧报文来自启动站;PRM =0:表示此帧报文来自从动站。
帧校验和CS: 帧校验和是用户数据区所有字节的八位位组算术和,不考虑溢出位。用户数据区包括控制域、地址域、链路用户数据(应用层)三部分。
1.2 数据部分
数据帧格式
应用层(链路用户数据)格式定义见图 4:
应用层功能码AFN: 应用层功能码AFN由一字节组成,采用二进制编码表示,具体定义见表 1:
应用功能码AFN | 应用功能定义 |
---|---|
00H | 确认∕否认 |
02H | 链路接口监测 |
0DH | 请求分析数据 |
20H | 回复分析结果 |
帧序列域SEQ: 帧序列域SEQ为1字节,用于描述帧之间的传输序列的变化规则,由于受报文长度限制,数据无法在一帧内传输,需要分成多帧传输(每帧都应有数据单元标识,都可以作为独立的报文处理)。SEQ定义如图 5:
D7 | D6 | D5 | D4 | D3-D0 |
---|---|---|---|---|
保留 | FIR | FIN | CON | 保留 |
首帧标志FIR、末帧标志FIN
FIR:置“1”,报文的第一帧。
FIN:置“1”,报文的最后一帧。
FIR、FIN组合状态所表示的含义见表 2:
FIR | FIN | 应用说明 |
---|---|---|
0 | 0 | 多帧:中间帧 |
0 | 1 | 多帧:结束帧 |
1 | 0 | 多帧:第1帧,有后续帧。 |
1 | 1 | 单帧 |
请求确认标志位CON : 在所收到的报文中,CON位置“1”,表示需要对该帧报文进行确认;置“0”,表示不需要对该帧报文进行确认。
数据单元标识定义: 数据单元标识由测量点ID和信息类标识DT组成,表示信息点和信息类型,格式见图 6:
信息点DA: 信息点DA由8字节组成pn,为测量点ID。
信息类DT: 信息类DT由信息类元DT1和信息类组DT2两个字节构成。DT2采用二进制编码方式表示信息类组,DT1对位表示某一信息类组的18种信息类型,以此共同构成信息类标识Fn(n=1248),格式见图9:
数据单元: 服务请求方在响应服务提供方对服务请求方的参数或数据请求时,如服务请求方没有所需的某个数据项,则将应答报文中DT的对应标志位清除;如服务请求方仅是没有某个数据项中的部分内容,则应将该数据项中的所缺部分内容的每个字节填写“EEH”。
1.3 控制帧
确认∕否认(AFN=00H): 确认∕否认报文是对接收报文中需要被确认(CON=1)的回答,以及服务请求方对所请求的数据不具备响应条件的否认回答。该报文为单帧报文,帧序列域的标志位FIR=1,FIN=1,CON=0。格式见图 8:
Fn和pn定义: Fn和pn定义见表 3:
Fn | 名称及说明 | pn |
---|---|---|
F1 | 全部确认:对收到报文中的全部数据单元标识进行确认 ,无数据体。 | 0 |
F2 | 全部否认:对收到报文中的全部数据单元标识进行否认, 无数据体。 | 0 |
F3~F248 | 备用 |
链路接口检测(AFN=02H): 链路接口检测命令下行报文格式见图 9:
Fn和pn定义:Fn和pn定义见表 4:
Fn | 名称及说明 | pn |
---|---|---|
组2 | 用户登录 | |
F9 | 登录 | p0 |
F10 | 退出登录 | p0 |
F11 | 心跳 | p0 |
F9:登陆命令
暂定 Data1 用户名
Data2 用户密码
其他数据待定
1.4 请求分析数据(AFN=0DH)
Fn和pn定义
Fn | 名称及说明 | pn | 数据时标 | 冻结 类型 |
---|---|---|---|---|
组11 | 功率曲线 | |||
F1 | 有功功率曲线 | 测量ID | Td_c | 曲线 |
F5 | 正向有功总点能量 | 测量ID | Td_c | 曲线 |
1.5 回复分析结果(AFN=20H)
报文格式: 链路接口检测命令服务请求方报文格式见:
Fn和pn定义: Fn和pn定义见表 4:
Fn | 名称及说明 | pn |
---|---|---|
F1 | 电池的充放电策略 | 测量点ID |
F2 | 系统的收益情况 | 测量点ID |
F3~F248 | 备用 |
二、通信架构
服务请求方工作流程
服务提供方工作流程
通信架构
三、关于IEC61850
SV:
SV(Sample Value)是IEC 61850标准中关于模拟量采样的协议,用于实时传输数字采样信息;SV报文也是采用发布者/订阅者的通信结构。SV报文是一种时间驱动的通信方式,即每隔一个固定时间发送一次采样值。其最主要的传输要求是实时、快速性。当由于网络原因导致报文传输丢失时,发布者(电流、电压传感器)并不要紧,应继续采集最新的电流、电压信息。而订阅者(比如保护装置)必须能够检测出来。这可以通过SV报文中的采样计数器参数SmpCnt来解决。
GOOSE:
GOOSE (Generic Object Oriented Substation Event,面向通用对象的变电站事件)是IEC 61850标准中用于满足变电站自动化系统快速报文需求的机制,主要用于实现在多IED 之间的信息传递,具有高传输成功概率,相当于传统保护的开入开出回路。是IEC61850标准定义的一种快速报文传输机制,被广泛应用到间隔闭锁和保护功能间的信号传递。GOOSE用网络信号代替了传统的硬接线通信方式,简化了变电站二次接线。
GOOSE报文的发送采用心跳报文和变位报文快速重发相结合的机制,按图所示的规律执行。其中T0又称心跳时间,在GOOSE数据集中的数据没有变化的情况下,装置平均每隔T0时间发送一次当前状态,即心跳报文,报文中的状态序号stNum(StateNumber用于记录GOOSE数据发生变位的总次数)不变,顺序号sqNum(SequenceNumber用于记录稳态情况下报文发出的帧数)递增。
当装置中有事件发生(如开关状态变位)时,GOOSE数据集中的数据就发生变化,装置立刻发送该数据集的所有数据,然后间隔T1发送第2帧及第3帧,间隔T2、T3发送第4、5帧,T2为2T1,T3为4T1,后续报文以此类推,发送间隔以2倍的规律逐渐增加,直到增加到T0,报文再次成为心跳报文。当数据变位后的第1帧报文中stNum增加1,sqNum从零开始,随后报文中stNum不变,sqNum递增。
工程应用中,T0设为5s,T1设为2ms。GOOSE状态变位过程共发5帧数据,即以2ms—2ms—4ms—8ms的时间间隔重发GOOSE报文,连续发5帧后便以5s时间间隔变成心跳报文。
GOOSE接收可以根据报文允许存活时间来检测链路中断,定义报文允许存活时间为2T0,接收方若超过2倍允许存活时间没有收到GOOSE报文即判为中断,发GOOSE断链报警信号。由此,通过GOOSE通信机制也实现了装置间二次回路状态在线监测。
T0—稳定条件下,心跳报文传输间隔;(T0)—稳定条件下,心跳报文传输可能被事件打断;T1—事件发生后,最短的重传间隔;T2、T3一直至获得稳定条件的重传间隔。
GOOSE报文的传输过程与普通的网络报文不同,它是从应用层经过表示层ASN.1编码后,直接映射到底层的数据链路层和物理层(如图所示),而不经TCP/IP协议,即不经网络层和传输层。这种映射方式避免了通信堆栈造成的传输延时,从而保证了报文传输的快速性。其中,ASN.1基本编码规则采用的编码结构由*标记(Tag)、长度(Length)以及内容(Value)*三个部分组成。
GOOSE采用发布者/订阅者通信结构,此通信结构支持多个通信节点之间的对等直接通信。与点对点通信结构和客户/服务器通信结构相比,发布者/订阅者通信结构是一个或多个数据源(即发布者)向多个接收者(即订阅者)发送数据的最佳解决方案,尤其适合数据流量大且实时性要求高的数据通信。发布者/订阅者通信结构符合GOOSE报文传输本质,是事件驱动的。
装置的单网GOOSE接收机制,如图所示,装置的GOOSE接收缓冲区接收到新的GOOSE报文,接收方严格检查GOOSE报文的相关参数后,首先比较新接收帧和上一帧GOOSE报文中的StNum(状态号)参数是否相等。若两帧GOOSE报文的StNum相等,继续比较两帧GOOSE报文的SqNum(顺序号)的大小关系,若新接收GOOSE帧的SqNum大于上一帧的SqNum,丢弃此报文,否则更新接收方的数据。若两帧GOOSE报文的StNum不相等,更新接收方的数据。
GOOSE报文头各参数含义如下:
(1)6个字节的目的地址“01 0c cd 01 00 33”和6个字节的源地址“00 10 00 00 00 33”。对于GOOSE报文的目的地址,前三个字节固定为“01-0C-CD”,第四个字节为“01”时代表GOOSE。IEC61850规定GOOSE报文目的地址取值范围为01-0C-CD-01-00-00~01-0C-CD-01-01-ff。
(2)地址字段后面是4个字节的Tag标签头信息“81 00 80 00”。“81 00”是TPID的固定值;“8000”换算成二进制数为“1000000000000000”,它包括三个部分的内容,用户优先级占据前三个Bit位“100”,CFI占第四个Bit位“0”,VLAN ID占最后十二个Bit位“000000000000”,换算成十进制数后可以看出优先级为4,VLAN ID为0。
(3)Tag标签头后是以太网类型值“88 b8”,代表该数据帧是一个GOOSE报文。IEC 61850中各种报文的以太网类型已经由IEEE的著作权注册机构进行了注册,是独一无二的,GOOSE报文的以太网类型值是0x88B8。
(4)紧接着是应用标识APPID“00 33”,该值全站唯一。
(5)APPID后面是长度字段“00 b6”,换算成十进制数为182,表示数据帧从APPID开始到应用协议数据单元APDU结束的部分共有182个字节。
(6)保留位1和保留位2共占有4个字节,默认值为“00 00 00 00”。
GOOSE 协议数据单元PDU各参数含义如下:
(1)gocbRef:即GOOSE控制块引用,由分层模型中的逻辑设备名、逻辑节点名、功能约束和控制块名级联而成。
(2)Time Allowed to Live:即报文允许生存时间,该参数值一般为心跳时间T0值的2倍,如果接收端超过2T0时间内没有收到报文则判断报文丢失,在4T0时间内没有收到下一帧报文即判断为GOOSE通信中断,判出中断后装置会发出GOOSE断链报警。
(3)dataset:即GOOSE控制块所对应的GOOSE数据集引用名,由逻辑设备名、逻辑节点名和数据集名级联而成。报文中Data部分传输的就是该数据集的成员值。
(4)goID:该参数是每个GOOSE报文的唯一性标识,该参数的作用和目的地址、APPID的作用类似。接收方通过对目的地址、APPID和goID等参数进行检查,判断是否是其所订阅的报文。
(5)t:即Event TimeStamp,事件时标,其值为GOOSE数据发生变位的时间,即状态号stNum加1的时间。
(6)stNum:即StateNumber,状态序号,用于记录GOOSE数据发生变位的总次数。
(7)sqNum:即SequenceNumber,顺序号SqNum,用于记录稳态情况下报文发出的帧数,装置每发出一帧GOOSE报文,SqNum应加1;当有GOOSE数据变化时,该值归0,从头开始重新计数。
(8)test:检修标识,用于表示发出该GOOSE报文的装置是否处于检修状态。当检修压板投入时,test标识应为True。
(9)confRev:配置版本号,Config Revision是一个计数器,代表GOOSE数据集配置被改变的次数。当对GOOSE数据集成员进行重新排序、删除等操作时,GOOSE数据集配置被改变。配置每改变一次,版本号应加1。
(10)ndsCom:即Needs Commissioning,该参数是一个布尔型变量,用于指示GOOSE是否需要进一步配置。
(11)NumDataSetEntries:即数据集条目数,图中其值为“19”,代表该GOOSE数据集中含有19个成员,相应地报文Data部分含有19个数据条目。
(12)Data:该部分是GOOSE报文所传输的数据当前值。Data部分各个条目的含义、先后次序和所属的数据类型都是由配置文件中的GOOSE数据集定义的。
SNTP:
用于时间同步
MMS:
IEC61850标准的一个重要目的就是使不同厂家的设备实现“互操作性”。这就需要在这些设备之间建立网络连接,并规范设备间的通信内容。使得接受请求的设备知道发送请求的设备的目的和要求,接受请求的设备进行操作后返回其结果,从而实现某一个特定的功能。智能变电站中统一采用MMS协议作为间隔层设备与站控层设备之间以及站控层设备相互之间的的通信协议标准,使得来自不同厂家的设备可以实现互操作。
GSSE:
通用变电站状态事件,用于传输状态变位信息
GSP:
国产的替代MMS协议
四、代码实现
import struct
import time
import binascii
import datetime
import select
import json
import socket
import threading
f=open('C:/Users/a1409/PycharmProjects/socket/'+str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S'))+' 日志.txt','w')
def num_to_BCD(num):
num = int(num)
low = num % 10
high = (int((num - low) / 10)) << 4
bcd = (high + low)
return bcd
def bcdDigits1(tuple):
date = '20'
for i in range(5):
date = date + str((tuple[4 - i] >> 4) % 16) + str((tuple[4 - i] % 16))
return date
def bcdDigits2(tuple):
data = 0
data = data + (tuple[2] >> 4) % 16 * pow(10, 1) + (tuple[2] % 16 * pow(10, 0))
data = data + (tuple[1] >> 4) % 16 * pow(10, -1) + (tuple[1] % 16 * pow(10, -2))
data = data + (tuple[0] >> 4) % 16 * pow(10, -3) + (tuple[0] % 16 * pow(10, -4))
return '%.4f' % data
def bcdDigits3(tuple):
data = 0
data = data + (tuple[3] >> 4) % 16 * pow(10, 3) + (tuple[3] % 16 * pow(10, 2))
data = data + (tuple[2] >> 4) % 16 * pow(10, 1) + (tuple[2] % 16 * pow(10, 0))
data = data + (tuple[1] >> 4) % 16 * pow(10, -1) + (tuple[1] % 16 * pow(10, -2))
data = data + (tuple[0] >> 4) % 16 * pow(10, -3) + (tuple[0] % 16 * pow(10, -4))
return '%.4f' % data
class Client(object):
host = '119.163.199.219'
port = 4405
# host = "localhost"
# port = 60000
ack = struct.pack("!21B", 0x68, 0x36, 0x00, 0x36, 0x00, 0x68, 0x40, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0xA1, 0x16)
den = struct.pack("!21B", 0x68, 0x36, 0x00, 0x36, 0x00, 0x68, 0x40, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xA2, 0x16)
hea = struct.pack("!21B", 0x68, 0x36, 0x00, 0x36, 0x00, 0x68, 0x40, 0x02, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0xB7, 0x16)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#client.bind(('192.168.2.102',8888))
# client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) #在客户端开启心跳维护
client.connect((host, port))
client.setblocking(False)
Lock = threading.Lock()
recv_time = 0
SED_correct = 0 # 发送成功标志位 1:发送成功 0:发送失败
RSG = 0 # 登录成功标志位 1:登录成功 0:登录失败
REC = 1 # 接受数据标志位 0:不在接收数据 1:在接受数据
SED = 0 # 发送数据标志位 0:不在发送数据 1:在发送数据
REC_edn = 0 # 接收完成标志位 0:接收未完成 1:接收完成
REC_datadictlist = []
REC_datadictlist1 = []
SED_list=[[2882303774267937025,"2022/05/30 11:15",
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0],[2882303774267937026,"2022/05/30 11:30",
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0]]
def savelist(self,Pn,date, m1, n1, F1,m2,n2,F5):
j=0
DataDict = {}
DataDict['Pn'] = Pn
DataDict['Date'] = date
DataDict['M1'] = m1
DataDict['N1'] = n1
DataDict['F1'] = F1
DataDict['M2'] = m2
DataDict['N2'] = n2
DataDict['F5'] = F5
if n1==1 and n2==1:
if len(self.REC_datadictlist1) == 0:
self.REC_datadictlist1.append(DataDict)
else:
j = 1
for i in range(len(self.REC_datadictlist1)):
if Pn == self.REC_datadictlist1[i]['Pn'] and int(date) > int(self.REC_datadictlist1[i]['Date']):
j = 0
self.REC_datadictlist1[i]['N1'] += n1
self.REC_datadictlist1[i]['N2'] += n2
F11 = self.REC_datadictlist1[i]['F1']
F55 = self.REC_datadictlist1[i]['F5']
self.REC_datadictlist1[i]['F1'] = F11+F1
self.REC_datadictlist1[i]['F5'] = F55+F5
break
else:
pass
if j == 1:
self.REC_datadictlist1.append(DataDict)
with open("real_data.json", "w") as fjson1:
json.dump(self.REC_datadictlist1, fjson1)
else:
if len(self.REC_datadictlist) == 0:
self.REC_datadictlist.append(DataDict)
else:
j = 1
for i in range(len(self.REC_datadictlist)):
if Pn == self.REC_datadictlist[i]['Pn'] and int(date)<int(self.REC_datadictlist[i]['Date']):
j=0
self.REC_datadictlist[i]['Date'] = date
self.REC_datadictlist[i]['N1'] += n1
self.REC_datadictlist[i]['N2'] += n2
F11 = F1 + self.REC_datadictlist[i]['F1']
F55 = F5 + self.REC_datadictlist[i]['F5']
self.REC_datadictlist[i]['F1'] = F11
self.REC_datadictlist[i]['F5'] = F55
break
else:
pass
if j == 1:
self.REC_datadictlist.append(DataDict)
with open("history_data.json", "w") as fjson:
json.dump(self.REC_datadictlist, fjson)
def STA_REG(self, USE, CYPHER):
len1 = len(USE) + 1
len2 = len(CYPHER) + 1
len3 = len1 + len2 + 2
len_all = len3 + 15
LEN = (len_all << 2) + 2 # 2字节
C = 0x40
AFN = 0X02
SEQ = 0x70
part1 = struct.pack("!B", 0x68) + struct.pack("2H14B", LEN, LEN, 0x68, C, AFN, SEQ,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01)
part2 = struct.pack('HB', len3, len1)
part3 = struct.pack('B', len2)
data = part1 + part2 + bytes(USE.encode()) + struct.pack('B', 0x00) + part3 + bytes(
CYPHER.encode()) + struct.pack('B', 0x00)
CS = 0x00
for i in range(len_all):
data_byte, = struct.unpack_from("!B", data, 6 + i)
CS += data_byte
CS = CS % 0x100
data = data + struct.pack("!2B", CS, 0x16)
while (True):
try:
self.client.send(data)
ready = select.select([self.client], [], [], 30)
if ready[0]:
Mes = Message()
time.sleep(0.08)
mes = self.client.recv(2048)
Mes.Mes_init(mes)
if Mes.state == "message error":
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送否认",file=f,flush=True)
self.client.send(self.den)
self.RSG = 0
time.sleep(5)
else:
DT2, = struct.unpack_from("!B", mes, 17)
AFN, = struct.unpack_from("!B", mes, 7)
if AFN == 0x00 and DT2 == 0x01:
print("登录成功",file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送心跳",file=f,flush=True)
self.client.send(self.hea)
self.RSG = 1
self.REC_edn = 0
self.SED=0
self.SED_correct = 0
self.realtime=time.time()
return 1
else:
print("error登录失败,重新登录",file=f,flush=True)
self.RSG = 0
time.sleep(2)
else:
self.RSG = 0
except ConnectionAbortedError as connecterror:
self.Lock.acquire()
print("error连接异常:", connecterror,file=f,flush=True)
self.client.close()
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#self.client.bind(('192.168.2.102', 8888))
self.client.connect((self.host, self.port))
self.client.setblocking(False)
self.Lock.release()
def Recieve_Msg(self):
while (True):
while(self.RSG==1):
ready = select.select([self.client], [], [], 15*60)
if ready[0]:
try:
self.REC = 1
time.sleep(0.08)
Head = self.client.recv(6)
if len(Head)==6:
self.Lock.acquire()
self.realtime = time.time()
self.Lock.release()
STA1, LEN1, LEN2, STA2 = struct.unpack_from("!B2HB", Head, 0)
data_len = (LEN1 >> 10) + (LEN2 % pow(2, 8)) * pow(2, 6)
Data=self.client.recv(data_len+2)
Mes=Message()
Mes.Mes_init(Head+Data)
if Mes.state == "message error":
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送否认",file=f,flush=True)
self.client.send(self.den)
else:
if Mes.AFN == 0x0D:
if Mes.CON == 1:
self.client.send(self.ack)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送确认",file=f,flush=True)
print("收到数据帧,正在保存",file=f,flush=True)
self.savelist(Mes.Pn, Mes.date1, Mes.M1, Mes.N1, Mes.data_point_list1, Mes.M2, Mes.N2,
Mes.data_point_list2)
self.Lock.acquire()
self.Lock.release()
if Mes.FIR == 0 and Mes.FIN == 1:
print("历史数据传输完成",file=f,flush=True)
self.Lock.acquire()
self.REC_edn=1
del self.REC_datadictlist[:]
self.Lock.release()
if Mes.FIR == 1 and Mes.FIN == 1:
print("实时单帧数据传输完成",file=f,flush=True)
self.Lock.acquire()
self.Lock.release()
if Mes.AFN == 0x00:
self.Lock.acquire()
self.SED_correct = 1
self.Lock.release()
else:
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":error报文长度小于6发送否认",file=f,flush=True)
self.client.send(self.den)
except Exception as connecterror:
print("error连接异常", connecterror,file=f,flush=True)
self.RSG =0
time.sleep(5)
self.ReConnect()
break
else:
self.Lock.acquire()
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "error接收等待超时",file=f,flush=True)
# self.REC_edn = 1
# self.REC = 0
self.Lock.release()
def SendHeart(self):
while(True):
while(self.RSG == 1):
time.sleep(240)
while (self.SED==1 or (time.time()-self.realtime)<239):
time.sleep(0.5)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送心跳",file=f,flush=True)
try:
self.client.send(self.hea)
except Exception as connecterror:
print("error连接异常", connecterror,file=f,flush=True)
self.RSG = 0
time.sleep(5)
self.ReConnect()
break
def SendMsg(self):
while(True):
while (self.REC_edn == 1):
time.sleep(7.5*60)
self.Lock.acquire()
self.SED = 1
self.Lock.release()
for i in range(len(self.SED_list)):
MES = SendMes_Creat()
MES.creat(self.SED_list[i])
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ":发送实时数据",binascii.hexlify(MES.Byte_mes),file=f,flush=True)
self.SED_correct = 0
try:
self.client.send(MES.Byte_mes)
except Exception as connecterror:
print("连接异常", connecterror,file=f,flush=True)
self.RSG = 0
self.ReConnect()
break
sta = time.time()
while (self.SED_correct == 0):
if (time.time() - sta) > 3:
break
if self.SED_correct == 0:
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),":error发送失败,重新发送",file=f,flush=True)
self.client.send(MES.Byte_mes)
else:
print("发送成功,发送下一帧",file=f,flush=True)
time.sleep(0.5)
time.sleep(4*60-2)
self.Lock.acquire()
self.SED = 0
self.Lock.release()
time.sleep(3.5*60+2)
def ReConnect(self):
self.Lock.acquire()
self.REC_datadictlist.clear()
self.REC_datadictlist1.clear()
self.client.close()
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#self.client.bind(('192.168.2.102', 8888))
self.client.connect((self.host, self.port))
self.client.setblocking(False)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "重新登录",file=f,flush=True)
self.RSG = self.STA_REG("268-7-D2-8", "123456")
self.Lock.release()
class Message(object):
Byte_mes = b''
STA1 = 0x68
LEN1 = 0x0000
LEN2 = 0x0000
STA2 = 0x68
C = 0x00
PRM = 0x00
DIR = 0x00
AFN = 0x00
SEQ = 0x00
CON = 0x00
FIN = 0x00
FIR = 0x00
Pn = 0x0000000000000000
DT1 = 0x00
DT2 = 0x00
date1 = "202201010000"
M1 = 0x00
N1 = 0x00
data_point1 = b''
data_point_list1 = []
date2 = "202201010000"
M2 = 0x00
N2 = 0x00
data_point2 = b''
data_point_list2 = []
state = "error"
def Mes_init(self,Mes):
self.MesCheck(Mes)
if self.state=="message correct":
self.MesProgress(Mes)
if self.AFN==0x0D:
self.get_recdata(Mes[9:-2])
else:
pass
else:
pass
def MesProgress(self, data):
self.C, self.AFN, self.SEQ = struct.unpack_from("!3B", data, 6)
self.PRM = (self.C >> 6) % 2 # 启动标志位,PRM =1:表示此帧报文来自启动站;PRM =0:表示此帧报文来自从动站。
self.DIR = (self.C >> 7) % 2 # 传输方向位,DIR=0:表示此帧报文是由服务提供方发出的报文; DIR=1:表示此帧报文是由服务请求方发出的报文。
self.CON = (self.SEQ >> 4) % 2 # 请求确认标志位 CON在所收到的报文中,CON 位置“1”,表示需要对该帧报文进行确认;置“0”,表示不需要对该帧报文进行确认。
self.FIN = (self.SEQ >> 5) % 2 # FIN:置“1”,报文的最后一帧。
self.FIR = (self.SEQ >> 6) % 2 # FIR:置“1”,报文的第一帧。
def MesCheck(self, Message):
LEN = len(Message)
if LEN < 21:
print("error 报文长度小于规定最低",end='',file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":", binascii.hexlify(Message),file=f,flush=True)
self.state = "message error"
return "error"
STA1, LEN1, LEN2, STA2 = struct.unpack_from("!B2HB", Message, 0)
if STA1 != 0x68 or STA2 != 0x68 or LEN1 != LEN2 or ((LEN1 >> 9) % 2) != 1:
print("error 报文头部不正确",end='',file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":", binascii.hexlify(Message),file=f,flush=True)
self.state = "message error"
return "error"
data_len = (LEN1 >> 10) + (LEN2 % pow(2, 8)) * pow(2, 6)
if (data_len + 8) != LEN:
print("error 报文数据部分长度不正确",end='',file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":", binascii.hexlify(Message),file=f,flush=True)
self.state = "message error"
return "error"
END, = struct.unpack_from("!B", Message, data_len + 7)
if END != 0X16:
print("error 报文尾部不正确",end='',file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":", binascii.hexlify(Message),file=f,flush=True)
self.state = "message error"
return "error"
CS, = struct.unpack_from("!B", Message, data_len + 6)
CS_check = 0
for i in range(data_len):
data_byte, = struct.unpack_from("!B", Message, 6 + i)
CS_check += data_byte
CS_check = CS_check % 0x100
if CS != CS_check:
print("error 报文数据部分校验不正确",end='',file=f,flush=True)
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":", binascii.hexlify(Message),file=f,flush=True)
self.state = "message error"
return "error"
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + ":收到报文正确"+" 报文长度:" + '%d' % LEN ,
"数据长度:" + '%d' % data_len , binascii.hexlify(Message),file=f,flush=True)
self.state = "message correct"
return "message correct"
def get_recdata(self, data_msg):
pin = 0
while (pin < len(data_msg)):
self.Pn, = struct.unpack_from("Q", data_msg, pin)
self.DT1, self.DT2 = struct.unpack_from("2B", data_msg, pin + 8)
self.date1 = bcdDigits1(struct.unpack_from("5B", data_msg, pin + 10))
self.M1, = struct.unpack_from("B", data_msg, pin + 15)
self.N1, = struct.unpack_from("B", data_msg, pin + 16)
self.data_point_list1 = []
for i in range(self.N1):
data_point = struct.unpack_from("3B", data_msg, pin + 17 + 3 * i)
self.data_point_list1.append(bcdDigits2(data_point))
pin = pin + 3 * self.N1 + 17
self.date2 = bcdDigits1(struct.unpack_from("5B", data_msg, pin))
self.M2, = struct.unpack_from("B", data_msg, pin + 5)
self.N2, = struct.unpack_from("B", data_msg, pin + 6)
self.data_point_list2 = []
for i in range(self.N2):
data_point = struct.unpack_from("4B", data_msg, pin + 7 + 4 * i)
self.data_point_list2.append(bcdDigits3(data_point))
pin = pin + 4 * self.N2 + 7
class SendMes_Creat(object):
Byte_mes = b''
STA1 = 0x68
LEN1 = 0x0000
LEN2 = 0x0000
STA2 = 0x68
C = 0x40
PRM = 0x00
DIR = 0x00
AFN = 0x20
SEQ = 0x7f
CON = 0x00
FIN = 0x00
FIR = 0x00
Pn = 0x0000000000000000
DT1 = 0x01
DT2 = 0x00
date = "202201010000"
M= 0x00
N = 0x00
data_point = b''
data_point_list = []
CS=0x00
END=0x16
def creat(self, List):
self.Pn = List[0]
self.M=1
self.N=len(List)-2
self.LEN1 = ((5 * self.N + 20) << 2) + 2 # 2字节
self.LEN2 = ((5 * self.N + 20) << 2) + 2 # 2字节
self.date=List[1]
head = struct.pack("!B", self.STA1) + struct.pack("2H4BQ2B", self.LEN1, self.LEN2, self.STA2, self.C,
self.AFN, self.SEQ, self.Pn, self.DT1, self.DT2)
year = num_to_BCD(List[1][2:4])
month = num_to_BCD(List[1][5:7])
day = num_to_BCD(List[1][8:10])
hour = num_to_BCD(List[1][11:13])
minute = num_to_BCD(List[1][14:16])
date = struct.pack("!7B", minute, hour, day, month, year, self.M, self.N)
for i in range(self.N):
if List[i + 2] < 0:
num = -List[i + 2]
Byte5 = num_to_BCD(80 + num // 100000 % 10)
else:
num = List[i + 2]
Byte5 = num_to_BCD(num // 100000 % 10)
Byte4 = num_to_BCD((num // 10000 % 10) * 10 + (num // 1000 % 10))
Byte3 = num_to_BCD((num // 100 % 10) * 10 + (num // 10 % 10))
Byte2 = num_to_BCD((num // 1 % 10) * 10 + ((num * 10) // 1 % 10))
Byte1 = num_to_BCD(((num * 100) // 1 % 10) * 10 + ((num * 1000) // 1 % 10))
self.data_point=self.data_point+struct.pack("!5B", Byte1, Byte2, Byte3, Byte4, Byte5)
mes = head+date+self.data_point
for i in range(5 * self.N + 20):
data_byte, = struct.unpack_from("!B", mes, 6 + i)
self.CS += data_byte
self.CS = self.CS % 0x100
self.Byte_mes = mes + struct.pack("!2B", self.CS, self.END)
from C_Package import Client
import threading
#历史数据在文件history_data.json
#实时数据在列表C.REC_datadictlist1中,文件real_data.json
#通过查询REC_edn:接收完成标志位 0:接收未完成 1:接收完成;来处理历史数据,应在接收完成后立马每15分钟更新一次策略列表C.SED_list,列表示例:
# C.SED_list=[[2882303774267937025,"2022/05/30 11:15",
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
# 20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
# 20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0],[2882303774267937026,"2022/05/30 11:30",
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0,0,0,0,0,
# 20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
# 20, 20, 20, 20, 0, 0, 0, -10, -10, -15, 50, 50, 60, 0, 0, 0, 0, 0, 0, 0,
# 20,20,20,20,0,0,0,-10,-10,-15,50,50,60,0,0,0]]
C=Client() #创建客户端
C.STA_REG("268-7-D2-8","123456") #登录,用户名&密码
Rec = threading.Thread(target=C.Recieve_Msg) #接收信息线程
Rec.start()
SendHeart = threading.Thread(target=C.SendHeart) #心跳维护线程
SendHeart.start()
SendMes = threading.Thread(target=C.SendMsg) #发送消息线程
SendMes.start()