协议引擎开发

注意

VOFA+ beta 使用Qt5.14.2开发,windows版本使用MSVC2017编译器,请使用同样的环境编译插件。

调试方法

  • 运行DebugView之后,再运行VOFA+,可以在DebugView中查看到VOFA+打印的调试信息;
  • 在协议代码中,运行qDebug("你的调试信息"),可以打印调试信息,也可以打印软件本身运行错误的信息。

协议引擎是如何工作的?

VOFA+使用协议引擎进行数据解析的流程如下:

接口的详细说明

DataEngineInterface接口类

所有协议引擎都继承自DataEngineInterface接口。

class DataEngineInterface
{
public:
~DataEngineInterface() {}
virtual void ProcessingDatas(char *data, int count) = 0;
const QList<Frame> &frame_list() {
return frame_list_;
}
const QList<RawImage*> &image_channels() {
return image_channels_;
}
protected:
QList<Frame> frame_list_;
QList<RawImage*> image_channels_;
};
  • 虚函数ProcessingDatas - VOFA+接收到字节数据时,会调用此函数;
    • data - 所有未解析数据的缓冲区;
    • count - 缓冲区字节数。
  • frame_list_ - 记录每一帧协议数据的帧头、帧尾位置,从协议中解析出的采样数据等,调用ProcessingDatas函数之后,VOFA+通过调用frame_list()函数获取解析结果;
  • image_channels_ - 记录协议中解析出的图片通道,调用ProcessingDatas函数之后,VOFA+通过调用image_channels()函数获取解析结果。

Frame结构体

DataEngineInterface接口中,维护着一个Frame链表。

Frame记录着一帧协议数据的信息,VOFA+依据这个信息,更新采样数据,更新缓冲区,打印文本。

struct Frame {
int start_index_ = 0;
int end_index_ = 0;
int image_size_ = 0;
QVector<float> datas_;
bool is_valid_ = 0;
};
  • start_index_ - 帧头在缓冲区的偏移;
  • end_index_ - 帧尾在缓冲区的偏移;
  • image_size_ - 图片尺寸;
    • 如果>0,这是一个图片数据包;
    • 如果<=0,这是一个采样数据包或其他数据包。
  • datas_ - 当前帧解析出的采样数据;
  • is_valid_ - 是否是合法的数据包,这决定了文本区的对此帧的打印行为;
    • 如果为true,文本区勾选隐藏采样数据包时,此帧不打印;
    • 如果为false,文本区只有勾选隐藏所有数据时,此帧不打印。

RawImage类

DataEngineInterface接口中,维护着一个RawImage链表。

记录这图片的原始数据、格式、长宽等信息,VOFA+依据这个类,可以解码出一张图片。

class RawImage {
public:
RawImage() {;}
enum Format {
...
};
void set(uchar *data, int len, int width, int height, Format format) {
data_.resize(len);
memcpy(data_.data(), data, len);
length_ = len;
format_ = format;
width_ = width;
height_ = height;
updated_ = true;
}
uchar *data() { return data_.data(); }
int length() { return length_; }
int width() { return width_; }
int height() { return height_; }
Format format() { return format_; }
bool updated() { return updated_; }
private:
QVector<uchar> data_;
Format format_;
int length_ = 0;
int width_ = 0;
int height_= 0;
bool updated_ = true;
};
  • data_ - 图片数据;
  • format_ - 图片格式;
  • length_ - 图片尺寸;
  • width_ - 图片宽度;
  • height_ - 图片高度;
  • updated_ - 是否已更新;
    • 调用完ProcessingDatas,VOFA+会检查图片是否已更新,已更新的图片才会刷新到图片控件。

实例讲解

本章节只讲解RawData、FireWater,其他协议大同小异,源码中有详细注释。

RawData

协议特点:RawData

项目地址:https://gitee.com/gutega/Vodka/tree/master/dataengines/rawdata

  • 头文件:rawdata.h
#ifndef RawData_H
#define RawData_H
#include "dataengineinterface.h"
class RawData : public QObject, public DataEngineInterface
{
Q_OBJECT
Q_INTERFACES(DataEngineInterface)
Q_PLUGIN_METADATA(IID "VOFA+.Plugin.RawData")
public:
explicit RawData();
~RawData();
void ProcessingDatas(char *data, int count);
private:
Frame *frame_;
};
#endif // RawData_H

以下3行代码是必须的,因为协议名称为RawData,所以Q_PLUGIN_METADATA设置了IID为VOFA+.Plugin.RawData。当你编写自己的协议引擎时吗,把RawData修改成你的协议名字。

Q_OBJECT
Q_INTERFACES(DataEngineInterface)
Q_PLUGIN_METADATA(IID "VOFA+.Plugin.RawData")
  • 源码文件:rawdata.cpp
#include "rawdata.h"
RawData::RawData()
{
frame_list_.append(Frame());
frame_ = &frame_list_[0];
}
RawData::~RawData()
{
frame_list_.clear();
}
void RawData::ProcessingDatas(char *data, int count)
{
// 将所有数据包含为一帧,is_valid_为false,表示这不是一个采样数据包、也不是一个图片数据包
frame_->start_index_ = 0;
frame_->end_index_ = count-1;
frame_->is_valid_ = false;
frame_->image_size_ = 0;
}
  • 在构造函数中,给frame_list_(继承自DataEngineInterface的成员变量)添加一帧数据,并且将其指针赋予RawData的私有成员frame_;
  • RawData协议中,每次都把整个缓冲区包裹成一帧,并且通过frame_->is_valid_ = false;,将其标记为其他协议包。

FireWater

协议特点:FireWater

项目地址:https://gitee.com/gutega/Vodka/tree/master/dataengines/firewater

  • 头文件:firewater.h
#ifndef FIREWATER_H
#define FIREWATER_H
#include "dataengineinterface.h"
class FireWater : public QObject, public DataEngineInterface
{
Q_OBJECT
Q_INTERFACES(DataEngineInterface)
Q_PLUGIN_METADATA(IID "VOFA+.Plugin.FireWater")
public:
explicit FireWater();
~FireWater();
void ProcessingDatas(char *data, int count);
private:
uint32_t image_count_mutation_count_ = 0;
};
#endif // FIREWATER_H

以下3行代码是必须的,因为协议名称为FireWater,所以Q_PLUGIN_METADATA设置了IID为VOFA+.Plugin.FireWater。当你编写自己的协议引擎时吗,把FireWater修改成你的协议名字。

Q_OBJECT
Q_INTERFACES(DataEngineInterface)
Q_PLUGIN_METADATA(IID "VOFA+.Plugin.RawData")
  • 源码文件:firewater.cpp
#include "firewater.h"
#include <QDebug>
#include <limits>
FireWater::FireWater()
{
}
FireWater::~FireWater()
{
}
// 帧结构:任意字符串 : CSV结构的数据 \n
void FireWater::ProcessingDatas(char *data, int count)
{
frame_list_.clear();
int begin = 0, end = 0;
for (int i = 0; i < count; i++) {
if (data[i] != '\n')
continue;
// 已找到帧尾 —— '\n'
end = i;
bool frame_is_valid = false;
char *frame_head_ptr = data + begin;
int frame_count = i - begin + 1;
int image_size = 0;
Frame frame;
// 将分别位于':'左右两侧的任意字符串和CSV结构的数据分割
// 如果符合firewater的帧结构,分割出的段数为2
QString frame_str = QString::fromLocal8Bit(frame_head_ptr , frame_count);
QList<QString> name_and_datas = frame_str.split(':');
if (name_and_datas.size() >= 2) {
// 为什么分割段数>2,也进行帧解析?
// 答:没有关系,d:d:1,2,3,4\n,这样分割段数是3,不影响解析。
if (name_and_datas.size() > 2) {
// 采用最后一个':'后面的数据作为有效CSV数据
begin += frame_count - name_and_datas.last().length() - 2;
}
QVector<float> dd;
QString name = name_and_datas[name_and_datas.size()-2].trimmed();
QList<QString> datas = name_and_datas[name_and_datas.size()-1].trimmed().split(',');
if (name == "image") {
// 图片前导帧
if (datas.length() != 5) {
// 图片前导帧异常
break;
}
// 获取图片信息
int image_id = datas[0].toInt();
image_size = datas[1].toInt();
int image_width = datas[2].toInt();
int image_height = datas[3].toInt();
RawImage::Format image_format = static_cast<RawImage::Format>(
datas[4].toInt());
// !获取图片信息
if ((count - (i + 1)) < image_size) {
// 图片长度超过缓冲区长度,可能还没接收完,直接返回,下次再来
return;
}
if (image_id > (image_channels_.length() - 1)) {
// 图片id > 图片通道数量,扩充图片通道
// 在扩充图片通道之前,为过滤异常情况,保证发送了6帧大id的图片之后,再进行扩充
image_count_mutation_count_++;
if (image_id < 6 || image_count_mutation_count_ >= 6) {
image_count_mutation_count_ = 0;
while (image_channels_.length() < image_id + 1) {
image_channels_.append(new RawImage());
}
}
}
if (image_id < image_channels_.length()) {
// 图片id合法,把图片数据放到图片通道中
image_channels_[image_id]->set((uchar*)data + i + 1, image_size,
image_width, image_height,
image_format);
}
// 把图片数据结尾记录为帧尾,图片前导帧+图片数据,构成了一个图片数据包
end = i + image_size;
i = end;
frame_is_valid = true; // 至此,可以确定这是一个合法的图片数据包
} else {
// 解析CSV数据,将其转换为采样数据
for (int i = 0; i < datas.length(); i++) {
float value = datas[i].trimmed().toFloat();
frame.datas_.append(value);
}
}
frame_is_valid = true; // 至此,可以确定这是一个合法的采样数据包
}
// 记录帧 是否合法,开始位置,结束位置,图片尺寸(如果为0,标识其不是图片数据包)
frame.is_valid_ = frame_is_valid;
frame.start_index_ = begin;
frame.end_index_ = end;
frame.image_size_ = image_size;
frame_list_.append(frame);
// !记录帧
begin = i+1;
}
}
  • 每次调用ProcessingDatas,都先将帧链表清空:frame_list_.clear();
  • 使用 \n识别帧尾,使用 :分割数据,判断协议为采样数据包、图片数据包还是其他数据包;
  • 将每一帧解析出的采样数据,填入frame.datas_,将frame的其他信息填写准确后,插到frame_list_尾部;
  • 将解析出的图片刷新到image_channels_链表的第id个位置。

如何编译?

安装Qt5.13.2

Qt下载地址:Qt5.13.2

  • 安装的时候需要一个登录Qt的账号和密码,直接上Qt官网注册即可;
  • 只需要安装MSVC2017版本的Qt和QtCreator,其他组件不需要安装,这样能保证Qt安装体积最小;
  • 根据你使用的软件版本,选择32位或者64位。

下载VOFA+协议引擎源码并编译

源码仓库:https://gitee.com/je0000/Vodka

协议引擎位于dataengines目录,每个协议为单独的qt项目。

用QtCreator打开.pro工程,选择Release构建模式。

点击构建即可生成动态链接文件,文件一般位于dataengines/build-xxxx...-release/release