1、项目目标
- 设备端:
(1)基于stm32mp157开发板,裁剪linux5.10.10,完成ov5640摄像头移植;
(2)完成用户层程序,完成对摄像头的控制及与云端服务的数据交互。
- 云端:
(1)完成TCP服务器,完成用户端<---->设备端的数据转发;
(2)使用关系型数据库,存储用户与设备的基础信息。
- 用户端:
(1)Qt开发应用界面,包括用户注册、用户登录、设备绑定、设备控制等功能;
(2)移植opencv,实现人脸框选功能。(未实现)
2、项目设计
2.1 Qt用户端设计(原型图)
2.1.1 用户登录和用户注册
点击眼睛:展示密码
2.1.2 主界面(Tab1: 设备监控)
双击左侧表格,设备具体信息展示在右侧;
点击开始按钮,黑色区域展示远程摄像头拍摄的内容(此时不可切换至其他页面,直至点击结束按钮)。
2.1.3 设备绑定(Tab2: 我的设备)
主要功能:将登录的用户与远程的摄像头设备关联。
2.2 Qt用户端程序设计
2.2.1 单例模式
设计了 MyWidgetFactory,管理LoginWidget、RegistWidget、MainWidget和AddDevWidget的单例,并提供静态接口获取这些单例。避免了各个页面之间跳转需要将其他页面的示例注入到自身,解决了管理麻烦的问题。以下是部分代码展示:
class MyWidgetFactory
{
public:
MyWidgetFactory()=delete;
static void init();
static LoginWidget* getLoginWidget();
static RegistWidget* getRegistWidget();
static MainWidget* getMainWidget();
static AddDeviceWidget* getAddDeviceWidget();
};
2.2.2 工厂方法模式或代理模式
设计了MyTcpProxy代理:
(1)QTcpSocket管理:以单例的方式管理QTcpSocket,将QTcpSocket与各个UI界面进行了解耦,在MyTcpProxy集中管理QTcpSocket单例,UI只处理UI的事情;
(2)报文发送:对外提供sendXXX的静态接口给各个界面调用以发送消息,在Proxy内部统一组包;
(3)报文解析:在MyTcpProxy内部完成TcpSocket返回消息的报文解析,并根据不同的报文种类,通过信号发送到各个界面,各界面通过槽函数进行消息返回处理及刷新界面;
(4)类MVC设计:对于复杂结构,如主界面的tab1和tab2的设备表格,构造表格设备模型,将解析内容刷新到模型,再通过信号发送到界面,实现类似MVC的模式。
class MyTcpProxy:public QObject
{
private:
QTcpSocket* socket; //单例: 全局有且仅有一个QSocket
public:
MyTcpProxy();
static MyTcpProxy* getTcpProxy(); //获取代理:也单例,所有界面可通过此代理操作socket
//发送报文(若干个函数),类似工厂方法模式
static bool sndLogin(const QString & user,const QString & pwd);
static bool sndReg(const QString & user,const QString & pwd);
static bool sndStartCamera(const QString& userid,const QString& devid);
// 省略......
public slots:
//socket通信的槽函数,所有远端来的消息先通过Pro需要处理,Proxy再将通过信号发送到各个UI中
//(1)报文的统一拆解
//(2)预处理,例如视频数据,等一帧收齐并进行格式转换后,再发送给UI界面
//(3)UI与逻辑分离,UI只处理UI的事情
void msgRecved();
void onDisconn();
signals:
//信号(若干)
void loginRet(const bool& ret,const QString& msg);
void regRet(const bool& ret,const QString& msg);
void videoRecv(QPixmap& pic);
// 省略......
};
2.2.3 config.ini配置文件
设计MyConfig类和config.ini文件,通过MyConfig::getValue(const QString & key)接口获取config.ini配置内容,避免将一些重要且需要频繁修改的参数写死在代码中,仅仅通过修改配置文件即可完成参数修改。
class MyConfig
{
private:
static QSettings* settings;
public:
MyConfig()=delete;
static QString getConfigString(QString key);
static int getConfigInt(QString key);
static double getConfigDouble(QString key);
};
配置文件:
[network]
host=127.0.0.1
port=8080
虽然配置内容较少了,但体现了配置与代码分离的思想。
2.2.4 yuyv4与RGB32的格式转换
这个是有公式的,见源码的 ImageUtil类。 我是从yuyv4转成RGB32格式的。
QImage ImageUtil::yuyvArr2QImage(const uchar *data, int width, int height)
{
QImage image(width, height, QImage::Format_RGB32);
for (int y = 0; y < height; ++y) {
// 获取当前行的YUV数据指针
const uchar *yuyvLine = data + y * width * 2; // 每行占用 width*2 字节
QRgb *rgbLine = reinterpret_cast<QRgb*>(image.scanLine(y));
// 每4字节处理两个像素(YUYV格式)
for (int x = 0; x < width; x += 2) {
// 提取YUV分量
uchar Y0 = yuyvLine[0]; // 第一个像素的Y
uchar U = yuyvLine[1]; // 共用U分量
uchar Y1 = yuyvLine[2]; // 第二个像素的Y
uchar V = yuyvLine[3]; // 共用V分量
yuyvLine += 4; // 移动到下一个YUYV块
// YUV转RGB(BT.601标准,TV范围)
auto convert = [](uchar Y, uchar U, uchar V) -> QRgb {
// 调整YUV到有效范围
int y = qMax((int)Y, 16) - 16; // Y范围: 16-235 → 0-219
int u = U - 128; // U范围: 0-255 → -128-127
int v = V - 128; // V范围: 0-255 → -128-127
// 整数运算(使用64位防溢出)
int r = (298 * y + 409 * v + 128) >> 8;
int g = (298 * y - 100 * u - 208 * v + 128) >> 8;
int b = (298 * y + 516 * u + 128) >> 8;
// 限制到0-255范围
r = qBound(0, r, 255);
g = qBound(0, g, 255);
b = qBound(0, b, 255);
return qRgb(r, g, b);
};
// 转换两个像素
rgbLine[x] = convert(Y0, U, V);
rgbLine[x + 1] = convert(Y1, U, V);
}
}
return image;
}
2.3 云端数据库设计
2.3.1 数据库
其实就两张表,tbl_user用户表、tbl_dev设备表。一般按照数据库设计范式,应该再添加一张关联关系表,例如tbl_user_dev,但是,我把userid字段直接放到了tbl_dev中,然后让userid和devid成为tbl_dev的联合主键。E-R就不画了。
实际中,应该用关联关系表还是把关联关系放到某个实体表中,要看实际应用情况,按我的理解,如果需要通过关联关系查询的频率很高且数据量还不小,我认为还是沉到实体表中更好,但这样实体表,确实就不能叫实体表了,可以称之为数据表。
2.3.2 网络数据接口设计
本案例中均使用tcp进行通信,除了设备心跳报文,其他报文都是一问一答的形式。相对麻烦的视频数据发送的时序图如下图所示:
2.3.3 关键的数据结构设计
(1)手写hashmap: 目的通过devid快速查找qt端的socketfd和设备端的socketfd,本案例实际上用不着,纯粹为了练手。贴下头文件
typedef struct{
char devid[20];//key
int devfd;//设备端fd
int userfd;//qt端fd
}data_t;
typedef struct pn{
union{ int subLen; data_t data; };
struct pn* next;
}node_t;
typedef struct{ node_t* arr[20]; int len; }hashmap_t;
hashmap_t* hashmap_create();
void hashmap_destroy(hashmap_t* mp);
int hashmap_hashcode(char* key);
void hashmap_put(hashmap_t* mp, char* key, data_t value);
data_t* hashmap_get(hashmap_t* mp, char* key);
void hashmap_remove(hashmap_t* mp, char* key);
int hashmap_size(hashmap_t* mp);
void hashmap_printself(hashmap_t* map);
(2)设备表与用户表对应的结构体
typedef struct{
char userid[20];//用户名
char pwd[20];//密码
}my_user_t;
typedef struct{
char userid[20];//用户名
char devid[20];//设备id
char devname[20];//设备名称
int state;//设备状态
char desc[50];//设备描述
}my_dev_t;
2.3.4 模块设计
(1)qt_server和dev_server: 两个基于EPoll模型的tcp服务端
(2)sqlite3的DAO层:贴下头文件
bool mysql_init();//初始化表
void mysql_deinit();//销毁资源
sqlite3* mysql_getConn();//获取sqlite3连接: 上锁!
void mysql_closeConn(sqlite3* db);//解锁!
bool mysql_user_exit(sqlite3* db, char* userid,char* pwd);//user表:查询
bool mysql_user_add(sqlite3* db, my_user_t* user);//user表:添加用户
//dev表:查询userid 关联的 设备列表
bool mysql_dev_list(sqlite3* db, char* userid, my_dev_t* arr, int* len);
bool mysql_dev_add(sqlite3* db, my_dev_t* dev);//dev表:添加关联的设备
//dev表:删除关联的设备
bool mysql_dev_del(sqlite3* db, char* userid, char* devid);
(3)统一的报文解析、处理与回复,思想类似Qt端的设计。
2.4 设备端设计
2.4.1 驱动层设计
外设: OV5640
驱动: linux5.10.10源码自带驱动 (基于V4L2驱动框架),通过make menuconfig直接配置内核
Device Drivers --->
<*> Multimedia support --->
Media core support --->
<*> Video4Linux core
Media drivers ---
[*] V4L platform devices --->
<*> STM32 Digital Camera Memory Interface (DCMI) support
Media ancillary drivers --->
Camera sensor devices --->
<*> OmniVision OV5640 sensor support
设备树的配置: 比较多,我也按照教程来做的,此处不写了。完成后,可以通过ls /dev/video0查看到这个设备文件。
2.4.2 用户层设计
(1)线程1: 定时拍照,生成图片。封装函数通过调用驱动生成拍摄图片
(2)线程2: 读取生成的图片文件,若有文件,上送到云端的dev_server
(3)线程3: 心跳线程
3、总结
3.1 图片传输效率
其实我使用的方法传输的效率特别,但由于时间原因,又不想做更多的改动,我能想到的可以改善的方法如下:
(1)改用udp,多包发送,确认后,再回复补包,补充缺少的包;
(2)采用一些压缩算法,减少数据量;
(3)本案例中已经将图片数据生成了文件,可以通过tftp直接发送文件;
(4)视频流传输(我还不会);
(5)采用一些专用的传输协议(我也还不会);
3.2 Qt的QSocket的readyRead信号绑定
3.3 hashmap的封装碰到的问题
3.4 缺陷
这个系统只是为了给小编自己练手用的,存在蛮多BUG的,例如:
(1)本来设计的用户端与设备端是多对多的,cloud_server需要维护多个hashmap来保存对应管理与通信时序,偷懒,在进行远程拍摄的时候,只能1对1;
(2)cloud_server用了不少全局变量;
(3)通过图片的方式再进行视频拍摄,视频应该有专门的方法,尚未研究;
(4)本来现在Qt端调用opencv的人脸框选接口,这是一个比较独立的功能,且,时间有限,先这样吧;
(5)QSS没有加,这个不是练习重点。
..... 如果要用的我源码,请注意这些坑以及还没有发现的坑。回归实际,如果云端程序面临比较的并发,且不用太考虑硬件资源的问题,我更建议用java来写,开发效率高、运维成本较低。
4、源码
tjzhuorui@163.com/远程监控系统