水电站生态流量在线监测,流量数据采集传输,水资源遥测终端机程序。
背景:现场使用SCJ-LL01多普勒超声波流量计采集生态下泄流量,使用太阳能供电系统,使用SCJ-RTU01遥测终端机进行数据采集,设备采用4G通讯,并配有串口工业相机抓拍照片。
要求:数据上传到指定的市监测平台,数据上传协议可以使用水资源SZY206或者http上传,但是平台方不支持图片直接通过水资源协议上传,需要使用专门的post接口上传,考虑到很多post接口上传图片都是使用BASE64以及https,单片机性能有限,而且图片数据间隔较大,因此选择将图片数据使用服务器转发,也就是流量数据上传由RTU直接发送,图片数据由计算机转发,下面是开发流程。
1.RTU程序开发。
由于水资源协议水流量上传部分有灵活变动的地方,主要表现在流量数据数量,有的可以传输2个流量,有的只能传输一个,有的可以传输1个瞬时流量+1个累计水量,因此几乎所有的水资源协议都需要进行定制微调。
水资源SZY206帧格式
此处主要实现自报流量数据,只需要上传5个字节的瞬时流量即可。
自报数据格式
自报流量格式,需要注意的是,这个瞬时流量的单位是不唯一的,需要与平台方约定,此处平台方使用的是m3/s。
下面是遥测终端机RTU的代码
#define CUSTOM_PROTOCOL_WENZHOU_SZY 16 //温州市小水电站生态流量,水资源协议
//通过宏定义选择使用的协议,定制很多,根据宏定义选择即可。
/*************************************************************************************************************************
* 函数 : u8 SZY206_SetPackWaterFlow(u8 *pBuff, REAL_DATA *pRealData)
* 功能 : 写入当前实时流量数据
* 参数 : pBuff:数据存放位置;pRealData:实时数据
* 返回 : 数据长度
* 依赖 : 无
* 作者 : cp1300@139.com
* 时间 : 2022-08-16
* 最后修改时间 : 2022-08-16
* 说明 : 5B字节数据,单位为m3/s,3位小数,会将2个流量计的瞬时流量相加上传
*************************************************************************************************************************/
u8 WENZHOUSZY_SetPackWaterFlow(u8* pBuff, ESS_DATA_TYPE* pData)
{
u8 ByteNum = 0;
u8 temp;
u32 InsFlowRate = 0; //瞬时流量
if (pData == NULL) return 0;
//瞬时流量m3/s,保留3位小数点
if (GET_ESS_FL_1_EN()) //使能了流量计1
{
if (pData->InsFlowRate[0] != ESS_DOUBLE_INVALID)
{
InsFlowRate += pData->InsFlowRate[0] / 3.6; //瞬时流量单位m3/h,保留3位小数点,转换为m3/s
}
}
if (GET_ESS_FL_2_EN()) //使能了流量计2
{
if (pData->InsFlowRate[1] != ESS_DOUBLE_INVALID)
{
InsFlowRate += pData->InsFlowRate[1] / 3.6; //瞬时流量单位m3/h,保留3位小数点,转换为m3/s
}
}
//流量1,实际上传输的是2个流量计只和
temp = InsFlowRate % 100;
pBuff[ByteNum++] = SZY206_DECtoBCD(temp);
temp = (InsFlowRate / 100) % 100;
pBuff[ByteNum++] = SZY206_DECtoBCD(temp);
temp = (InsFlowRate / 10000) % 100;
pBuff[ByteNum++] = SZY206_DECtoBCD(temp);
temp = (InsFlowRate / 1000000) % 100;
pBuff[ByteNum++] = SZY206_DECtoBCD(temp);
temp = (InsFlowRate / 100000000) % 10;
pBuff[ByteNum++] = SZY206_DECtoBCD(temp) & 0x0F;
return ByteNum;
}
/*************************************************************************************************************************
* 函数 : DCP_ERROR SZY206_SendRealDataFrame(WENZHOUSZY_HANDLE *pHandle, UFRAME_FUN Fun, REAL_DATA *pRealData, REAL_TIMER_TYPE* pAcqTime)
* 功能 : 遥测站自报实时数据
* 参数 : pHandle:句柄;Fun:功能码;ParaData:实时数据;pAcqTime:数据采集时间
* 返回 : DCP_ERROR
* 依赖 : 无
* 作者 : cp1300@139.com
* 时间 : 2022-08-16
* 最后修改时间 : 2022-08-16
* 说明 : 只支持水位/流量数据打包发送 UFUN_WL/UFUN_FLOW
*************************************************************************************************************************/
DCP_ERROR WENZHOUSZY_SendRealDataFrame(WENZHOUSZY_HANDLE* pHandle, SZY206_UFRAME_FUN Fun, ESS_DATA_TYPE* pData, REAL_TIMER_TYPE* pAcqTime)
{
SZY206_SRDATA_FRAME* pFrame;
u8 ByteNum = 0;
u8 FrameLen = 0;
u8 retry;
int len;
u16 ReceiveDelay;
u8 cnt;
for (retry = 0; retry < pHandle->SendRetry; retry++) //失败重复发送
{
FrameLen = 0;
Fun = (SZY206_UFRAME_FUN)(Fun & 0x0f); //功能码为4bit,0-15
pFrame = (SZY206_SRDATA_FRAME*)pHandle->pPackDataBuff;
//报文头
pFrame->SOH1 = 0x68; //起始字符 68H
//pFrame->len //长度
pFrame->SOH2 = 0x68; //起始字符 68H
ByteNum = 0;
pFrame->Data[ByteNum++] = (1 << 7) //BIT7->0:下行;1:上行
| (0 << 6) //BIT6->0:单帧;1:代表帧拆分过
| (((pHandle->SendRetry - retry) & 0x3) << 4) //重发变化位,3,2,1,0变化
| (Fun << 0); //BIT0-BIT3功能码
memcpy(&pFrame->Data[ByteNum], pHandle->SendDataTelAttr.TelNumber, 5); //遥测站地址编码
ByteNum += 5;
//报文正文
//应用层功能码,AFN
pFrame->Data[ByteNum++] = (u8)AFN_FUN_REPORT; //功能码:自报实时数据 功能码0xC0
//数据,根据功能码打包数据
switch (Fun & 0x0f)
{
case UFUN_WL: //自报帧 水位参数
{
cnt = SZY206_SetPackWaterLevel(&pFrame->Data[ByteNum], pData); //写入当前实时水位
ByteNum += cnt;
}break;
case UFUN_FLOW: //自报帧 流量(水量)参数
{
cnt = WENZHOUSZY_SetPackWaterFlow(&pFrame->Data[ByteNum], pData); //写入当前实时流量
ByteNum += cnt;
}break;
default:return DCP_DATA_ERROR;
}
pFrame->len = 1 + 5 + 1 + cnt + 4 + 5; //长度,1B:控制;5B:地址;1:用户功能码;cnt字节数据;4B:运行状态;5B:时间标签
//终端状态-4此处4字节
pFrame->Data[ByteNum++] = 0x00; //报警状态
pFrame->Data[ByteNum++] = 0x00;
pFrame->Data[ByteNum++] = 0x00;
pFrame->Data[ByteNum++] = 0x00;
//TP,时间
pFrame->Data[ByteNum++] = SZY206_DECtoBCD(pAcqTime->sec); //SS
pFrame->Data[ByteNum++] = SZY206_DECtoBCD(pAcqTime->min); //mm
pFrame->Data[ByteNum++] = SZY206_DECtoBCD(pAcqTime->hour); //HH
pFrame->Data[ByteNum++] = SZY206_DECtoBCD(pAcqTime->date); //DD
pFrame->Data[ByteNum++] = 0x00; //这个报文要求这个时间必须为0x00
FrameLen = ByteNum; //记录用户区数据长度
//CRC校验
pFrame->Data[ByteNum++] = SZY206_CRCByte(pFrame->Data, FrameLen); //计算用户数据CRC ,1B控制,5B地址,1B用户功能码,nB实时数据长度,4B报警状态,5B时间标签
//结束
pFrame->Data[ByteNum++] = 0x16;
//计算帧长度
FrameLen = SRDATA_FRAME_HEADER_SIZE + ByteNum;
//发送数据
#if 1
{
u16 i;
INFO_S("[SZY206]发送数据帧(%d):\r\n", +FrameLen);
for (i = 0; i < FrameLen; i++)
{
INFO_C("%02X ", pHandle->pPackDataBuff[i]);
}
INFO_C("\r\n\r\n");
}
#endif//SZY206_DEBUG_EN
pHandle->pHW_IF->SendData(pHandle->pPackDataBuff, FrameLen);
SYS_DelayMS(1500); //数据发送完成后不会收到响应
return DCP_OK;
}
return DCP_TIME_OUT;
}
/*************************************************************************************************************************
* 函数 : DCP_ERROR WENZHOUSZY_SendRealDataPackge(WENZHOUSZY_HANDLE *pHandle, REAL_TIMER_TYPE *pAcqTime,ESS_DATA_TYPE *pData, u16 SerialNumber)
* 功能 : 发送实时数据
* 参数 : pHandle:协议栈句柄;pConnectData:WENZHOUSZY连接结构指针;pAcqTime:发报时间;pData:实时数据指针;SerialNumber:流水号;
* 返回 : DCP_ERROR
* 依赖 : 无
* 作者 : cp1300@139.com
* 时间 : 2022-08-16
* 最后修改时间 : 2022-08-16
* 说明 :
*************************************************************************************************************************/
DCP_ERROR WENZHOUSZY_SendRealDataPackge(WENZHOUSZY_HANDLE *pHandle,ESS_DATA_TYPE *pData, REAL_TIMER_TYPE *pAcqTime,u16 SerialNumber)
{
WENZHOUSZY_SendRealDataFrame(pHandle, UFUN_FLOW, pData, pAcqTime); //发送流量数据包
return DCP_OK;
}
上面就是水资源协议的数据打包核心,下面是水资源协议相关的结构体定义
/*
//帧格式定义
起始字符(68H)
长度L 固定长度的报文头
起始字符(68H)
控制域C 控制域
用户数据区
地址域A 地址域
用户数据 用户数据域
校验CS 帧校验
结束字符(16H)*/
//帧定义,单包数据帧定义
typedef struct
{
u8 SOH1; //起始字符 68H
u8 len; //长度
u8 SOH2; //起始字符 68H
//
//用户数据区
u8 Control; //控制区
u8 TelAddr[5]; //遥测站地址
u8 Data[1024 + 3]; //报文正文
//
//报文正文
//CRC校验
//结束符16H
} SZY206_FRAME;
/*
用户数据 控制区 C,
由D0~D7(1 字节)组成,采用BIN 编码,是控制域、地址域、用户数据域(应用层)的字节总数。数据为图片数据流时,数据长度为L*1K
控制域C
D7 D6 D5~D4 D3~D0
传输方向位DIR 拆分标志位DIV 帧计数位FCB 功能码
0:下行,1:上行
*/
//应用层功能码
typedef enum
{
AFN_FUN_LINK = 0x02, //链路检测
AFN_FUN_TIMING = 0x11, //校时
AFN_FUN_QUERY_REAL = 0xB0, //查询遥测站实时数据
AFN_FUN_REPORT = 0xc0, //遥测站自报实时数据
AFN_FUN_REPORT_PIC = 0xc1, //遥测站自报图片数据
}SZY206_AFN_FUN;
//自报实时数据数据帧
typedef struct
{
u8 SOH1; //起始字符 68H
u8 len; //长度
u8 SOH2; //起始字符 68H
//
//用户数据区
//控制区 1B
//遥测站地址5B
//链路层功能码1B
//
//报文正文
u8 Data[1024 + 3]; //报文正文
//CRC校验
//结束符16H
} SZY206_SRDATA_FRAME;
#define SRDATA_FRAME_HEADER_SIZE 3 //正文前报头大小
//附加信息域AUX
//2B; //密码 PW 用于重要下行报文中,由2 字节组成
//5B; //时间标签 Tp
//下行帧功能码
typedef enum
{
DFUN_OK = 0, //发送∕确认 命令
DFUN_RAIN = 1, //查询∕响应帧 雨量参数
DFUN_WL = 2, //查询∕响应帧 水位参数
DFUN_FLOW = 3, //查询∕响应帧 流量(水量)参数
DFUN_CURR = 4, //查询∕响应帧 流速参数
DFUN_GATE = 5, //查询∕响应帧 闸位参数
DFUN_POWER = 6, //查询∕响应帧 功率参数
DFUN_PRESS = 7, //查询∕响应帧 气压参数
DFUN_WIND = 8, //查询∕响应帧 风速参数
DFUN_WT = 9, //查询∕响应帧 水温参数
DFUN_WQ = 10, //查询∕响应帧 水质参数
DFUN_SOILM = 11, //查询∕响应帧 土壤含水率参数
DFUN_EVAP = 12, //查询∕响应帧 蒸发量参数
DFUN_ALARM = 13, //查询∕响应帧 报警或状态参数
DFUN_COMPRE = 14, //查询∕响应帧 综合参数
DFUN_WPRESS = 15, //查询∕响应帧 水压参数
}SZY206_DFRAME_FUN;
//上行帧功能码
typedef enum
{
UFUN_OK = 0, //确认 命令
UFUN_RAIN = 1, //自报帧 雨量参数
UFUN_WL = 2, //自报帧 水位参数
UFUN_FLOW = 3, //自报帧 流量(水量)参数
UFUN_CURR = 4, //自报帧 流速参数
UFUN_GATE = 5, //自报帧 闸位参数
UFUN_POWER = 6, //自报帧 功率参数
UFUN_PRESS = 7, //自报帧 气压参数
UFUN_WIND = 8, //自报帧 风速参数
UFUN_WT = 9, //自报帧 水温参数
UFUN_WQ = 10, //自报帧 水质参数
UFUN_SOILM = 11, //自报帧 土壤含水率参数
UFUN_EVAP = 12, //自报帧 蒸发量参数
UFUN_ALARM = 13, //自报帧 报警或状态参数
UFUN_RAINFALL = 14, //自报帧 统计雨量
UFUN_WPRESS = 15, //自报帧 水压参数
}SZY206_UFRAME_FUN;
//将数字转换为压缩BCD格式,最大支持99
#define SZY206_DECtoBCD(DEC) (((u8)(DEC/10)<<4)+(DEC%10))
//将压缩BCD转为DEC,最大支持99
#define SZY206_BCDtoDEC(BCD) ((u8)(BCD>>4)*10+(BCD&0x0f))
2.PC端程序转发。
生态流量图片采集转发
程序采用VC++ CLR开发,程序启动后会从数据库读取当前要转发的设备的信息,比如测站编码,转发的接口等信息,然后去数据库查询当前设备的最新图片,找到图片后就按照要求进行上传,之后延时10分钟,重新查询,发送。
以下是平台方提供的图片上传接口:
这个接口可以看出,传输数据采用的json,将图片数据转换为base64字符串后,放入json传输,由于图片数据很大,起初我是想生成一个这个json对象,然后赋值,然后将对象转换为json,但是这样效率极低,我最终选择使用字符串拼接json,避免生成过多的对象,主要是考虑到以后很多类似的设备需要进行图片转发,降低系统内存CPU消耗。
以下是核心的数据发送的代码,至于数据查询部分就省略了,主要涉及到post发送数据:
bool RemoteCertificateValidationCallback1(System::Object^ sender, System::Security::Cryptography::X509Certificates::X509Certificate^ certificate, System::Security::Cryptography::X509Certificates::X509Chain^ chain, System::Net::Security::SslPolicyErrors sslPolicyErrors)
{
return true; //总是接受
}
ref class Headers_Class
{
public:
String^ key;
String^ value;
};
//POST方式发送字符串
String^ PostUrl(String^ url, String^ postData,String^ ContentType,String^ Accept,List<Headers_Class^>^ Headers, String^% pError)
{
char* pBuff = nullptr;
DWORD len;
HttpWebRequest^ request = nullptr;
String^ result = nullptr;
try
{
if (url->StartsWith("https", StringComparison::OrdinalIgnoreCase))
{
request = (HttpWebRequest^)WebRequest::Create(url);
ServicePointManager::ServerCertificateValidationCallback = gcnew System::Net::Security::RemoteCertificateValidationCallback(RemoteCertificateValidationCallback1);
request->ProtocolVersion = HttpVersion::Version11;
// 这里设置了协议类型。
ServicePointManager::SecurityProtocol = SecurityProtocolType::Tls12; //(SecurityProtocolType)3072;// SecurityProtocolType.Tls1.2;
request->KeepAlive = false;
ServicePointManager::CheckCertificateRevocationList = true;
ServicePointManager::DefaultConnectionLimit = 100;
ServicePointManager::Expect100Continue = false;
}
else
{
request = (HttpWebRequest^)WebRequest::Create(url);
}
request->Method = "POST"; //使用post方式发送数据
if (ContentType == nullptr)
{
request->ContentType = "application/json;charset:utf-8;";
}
else
{
request->ContentType = ContentType;
}
request->Referer = nullptr;
request->AllowAutoRedirect = true;
request->UserAgent = "PostmanRuntime/7.26.1";
if (Accept == nullptr)
{
request->Accept = "*/*";
}
else
{
request->Accept = Accept;
}
request->Timeout = 15 * 1000; //响应超时时间15秒
if (Headers != nullptr && Headers->Count > 0)
{
for (int i = 0; i < Headers->Count; i++)
{
Headers_Class^ mHeader = Headers[i];
request->Headers->Add(mHeader->key, mHeader->value);
}
}
System::Text::UTF8Encoding ^ascii = gcnew System::Text::UTF8Encoding();
array<unsigned char>^ data = ascii->GetBytes(postData);
Stream^ newStream = request->GetRequestStream();
newStream->Write(data, 0, data->Length);
newStream->Close();
//获取网页响应结果
HttpWebResponse^ response = (HttpWebResponse^)request->GetResponse();
Stream^ stream = response->GetResponseStream();
StreamReader^ sr = gcnew StreamReader(stream);
result = sr->ReadToEnd();
response->Close(); //关闭
}
catch (Exception^ e)
{
pError = e->Message + e->StackTrace;
}
return result;
}
//图片上传json格式
ref class ReportStaticPictureInfo_Class
{
public:
String^ PIC_INFO; //静态图片base64
String^ REC_TIME; //图片采集时间
String^ HYST_CODE; //测站编码
String^ WAIN_NUM; //从01开始,默认固定为1
};
//发送图片文件
//pURL:数据接口URL;HYST_CODE:测站图片编码;Token:AuthorizationToken;TT:图片采集时间;pPicFilePath:图片路径;pError:返回的错误字符串
bool SendPicFile(String^ url, String^ HYST_CODE, String^ Token,String ^TT, String^ pPicFilePath, String^% pError)
{
try
{
StringBuilder^ mStringBuilder;
array<BYTE>^ mDataBuff;
String^ PicBase64;
String^ pReturnString;
//读取图片数据
FileStream^ oFileStream = gcnew FileStream(pPicFilePath, FileMode::Open, FileAccess::Read);
BinaryReader^ oBinaryReader = gcnew BinaryReader(oFileStream);
if (oFileStream->Length > 800 * 1024 || oFileStream->Length < 1024)
{
pError = "上传图片不能超过800KB或小于1KB";
return false;
}
mDataBuff = gcnew array<BYTE>(oFileStream->Length);
//一次读取图片
if (oBinaryReader->Read(mDataBuff, 0, oFileStream->Length) <= 0)
{
pError = "读取图片文件错误";
return false;
}
//图片数据转换为base64
PicBase64 = Convert::ToBase64String(mDataBuff);
//准备json数据,直接拼接得到
mStringBuilder = gcnew StringBuilder(PicBase64->Length + 256);
mStringBuilder->Append("{\"DATA\":[{");
mStringBuilder->Append("\"PIC_INFO\":\"");
mStringBuilder->Append(PicBase64);
mStringBuilder->Append("\",");
mStringBuilder->Append("\"REC_TIME\":\"");
mStringBuilder->Append(TT);
mStringBuilder->Append("\",");
mStringBuilder->Append("\"HYST_CODE\":\"");
mStringBuilder->Append(HYST_CODE);
mStringBuilder->Append("\",");
mStringBuilder->Append("\"WAIN_NUM\":\"01\"");
mStringBuilder->Append("}]}");
//准备好header
List<Headers_Class^>^ Headers = gcnew List<Headers_Class^>();
Headers_Class^ mHeader;
//添加 AuthorizationKey
mHeader = gcnew Headers_Class;
mHeader->key = "AuthorizationKey";
mHeader->value = HYST_CODE;
Headers->Add(mHeader);
//添加 AuthorizationToken
mHeader = gcnew Headers_Class;
mHeader->key = "AuthorizationToken";
mHeader->value = Token;
Headers->Add(mHeader);
//SYS_LOG.Write(mStringBuilder->ToString());
pReturnString = PostUrl(url, mStringBuilder->ToString(), "application/json", nullptr, Headers, pError);
//Console::WriteLine(pReturnString);
if (pReturnString != nullptr && (pReturnString->IndexOf("上报成功")) > 0) return true;
else
{
pError = (pReturnString != nullptr) ? pReturnString : pError;
return false;
}
}
catch (Exception^ e)
{
pError = e->Message + e->StackTrace;
return false;
}
}
有需要相关产品的可以去官网查看:https://www.scj-water.com