水电站生态流量在线监测,流量数据采集传输,水资源遥测终端机程序。
背景:现场使用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


















