移动物联网项目搭建(四)
- 功能设计
- 设计思考
- 程序设计
- 接收线程的创建与实现
- 用于回显副线程传递消息的Widget界面设计
- 主线程的创建与实现
- 项目传送门
功能设计
设计思考
在书写代码之前,一定要去思考一些项目架构的问题,这有助于我们能力的提升和代码书写效率的提高。
1. 我们具体要实现哪些功能?
对于监控端,我们要实现短消息的发送与接收,小图片的传输与接收和接收来自采集端采集到的温度数据。
2. 我们是否需要多线程,如果是的话需要几个线程?
对于上述的几个功能,我们可以了解我们既要能随时发送消息又要能随时接收消息且要随时接收来自采集端的温度数据,那我们就肯定要用到多线程,一个线程用来监听是否有发送事件到来,一个线程用来接收消息同时也可用来接收来自采集端的温度数据,故需用到两个线程,那么我们就设计一个主线程,一个副线程。
3. 我们是否需要多个界面,如果是的话需要几个界面?
根据上述的功能和主界面的设计情况,我们可能还需要一个Dialog界面用于回显所接收消息的详细信息和图片消息的图片,除此之外,我们可以再额外实现一个登录功能,需要一个登录界面作为Splash窗口。
4. 阿里云提供的C-SDK是纯C的代码,而QT工程是C++的代码,移植过程是否有需要注意的地方?
C语言的内容有99%都可以在C++中实现,有区别的地方不多,但我们这个项目着实遇到了移植相关的问题。这里分享一下移植的经验:
- 注意可变指针void*的使用,在C++中编译器更加严格,若要进行赋值或者传参之类的操作需要加上强制类型转换。
- 注意函数指针的用法,由于C++使用面向对象的思想,大多函数均封装为某个类的成员函数,而成员函数作为函数指针来使用会遇到许许多多的问题,这里不建议成员函数使用函数指针。
- 由于库文件连接的一些复杂性,这里建议将一些重要函数作为普通函数来直接使用,而不用封装为成员函数。
5. 消息发送接收的格式约定是怎样的?
在与云端的通信中,可以选择传二进制数据流,也可以选择传Json消息包,我们这里选择后者,然后约定一个消息的大致格式如下:
{
"type":"Text";
"data":"Hello, world!";
"note":"";
}
每个消息有三个属性:类型,数据和备注。
然后我们这里选择使用mjson包来实现json包的打包和拆包:官网链接:http://bolerio.github.io/mjson/
如果进不去可以下载我上传的资源:
结合mjson库,编写了两个封装好的函数以供使用
char *new_entry(char *type, char *payload, char* note)
{
char *str;
json_t *entry, *label, *value;
entry = json_new_object();
//insert first label
label = json_new_string("type");
value = json_new_string(type);
json_insert_child(label, value);
json_insert_child(entry, label);
//insert second label
label = json_new_string("data");
value = json_new_string(payload);
json_insert_child(label, value);
json_insert_child(entry, label);
//insert third label
label = json_new_string("note");
value = json_new_string(note);
json_insert_child(label, value);
json_insert_child(entry, label);
json_tree_to_string(entry, &str);
return str;
}
将参数给定的属性封装为一个json格式的字符串返回。
void json_to_msg(char *type, char *data, char *note,char *payload)
{
json_t *root = NULL, *labelt, *labeld, *labeln;
json_t *valuet, *valued, *valuen;
json_parse_document(&root, payload);
labelt = json_find_first_label(root, "type"); //直接获取type所对应的值
valuet = labelt -> child;
strcpy(type, valuet->text);
labeld = json_find_first_label(root, "data"); //直接获取data所对应的值
valued = labeld -> child;
strcpy(data, valued->text);
labeln = json_find_first_label(root, "note");
valuen = labeln -> child;
strcpy(note, valuen->text);
}
将json格式进行解包,并将各规定好的属性放入参数指向的盘块区存储。
6.对图片的处理该如何进行?
图片文件实际也是二进制编码构成的,那我们首先要把二进制编码转换为字符串,再打包为json包进行发布,于是我们使用了Base64编码的方式实现。
Base64编码实现:
char *base64_encode(char *bindata, char *base64, int binlength)
{
int i,j;
unsigned char current;
char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for(i=0,j=0; i < binlength; i+=3)
{
current = (bindata[i] >> 2);
current &= (unsigned char)0x3F;
base64[j++] = base64char[(int)current];
current = ((unsigned char)(bindata[i] << 4)) & ((unsigned char)0x30);
if(i+1 >= binlength)
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
base64[j++] = '=';
break;
}
current |= ((unsigned char)(bindata[i+1] >> 4)) & ((unsigned char)0x0F);
base64[j++] = base64char[(int)current];
current = ((unsigned char)(bindata[i+1] << 2)) & ((unsigned char)0x3C);
if( i+2 >= binlength)
{
base64[j++] = base64char[(int)current];
base64[j++] = '=';
break;
}
current |= ((unsigned char)(bindata[i+2] >> 6)) & ((unsigned char)0x03);
base64[j++] = base64char[(int)current];
current = ((unsigned char)bindata[i+2]) & ((unsigned char)0X3F);
base64[j++] = base64char[(int)current];
}
base64[j] = '\0';
return 0;
}
Base64解码实现:
int base64_decode(char * base64, char * bindata)
{
int i, j;
unsigned char k;
unsigned char temp[4];
char * base64char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for(i = 0, j = 0;base64[i] != '\0'; i += 4)
{
memset( temp, 0xFF, sizeof(temp));
for(k = 0; k < 64; k++)
{
if(base64char[k] == base64[i])
temp[0] = k;
}
for(k = 0; k < 64; k++)
{
if(base64char[k] == base64[i+1])
temp[1] = k;
}
for(k = 0; k < 64; k++)
{
if(base64char[k] == base64[i+2])
temp[2] = k;
}
for(k = 0; k < 64; k++)
{
if(base64char[k] == base64[i+3])
temp[3] = k;
}
bindata[j++] = ((unsigned char)(((unsigned char)(temp[0] << 2)) & 0xFC)) |
((unsigned char)((unsigned char)(temp[1] >> 4) & 0x03));
if ( base64[i+2] == '=' )
break;
bindata[j++] = ((unsigned char)(((unsigned char)(temp[1] << 4)) & 0xF0)) |
((unsigned char)((unsigned char)(temp[2] >> 2) & 0x0F));
if ( base64[i+3] == '=' )
break;
bindata[j++] = ((unsigned char)(((unsigned char)(temp[2] << 6)) & 0xF0)) |
((unsigned char)(temp[3]&0x3F));
}
return j;
}
程序设计
几个全局变量,在c++编程过程中按理说要少用全局变量,多做封装,但由于这里涉及到c的移植和主副线程的通信,不得不采取了使用全局变量的下下策。
char topicdes[262144];//Topic来源
char dataKind[20];//消息类型
long int dataLen;//消息长度
char data[262144];//消息数据
char note[100];//备注
接收线程的创建与实现
这里需要为主线程创建一个副线程用于实现实时的消息接收。
在Qt creator中新建一个C++ class,类名为ThreadReceive,以QThread为基类,具体定义如下
class ThreadReceive : public QThread
{
Q_OBJECT
public:
ThreadReceive();
void Yield(void *pclient);
void stop();
signals:
void Log(char* topic, char* kind, int len, char *data, char*note);
protected:
void run();
private:
volatile bool stopped;
int res;
};
变量/函数名 | 功能描述 |
stopped | 标志线程是否停止 |
res | 线程订阅Topic是否成功 |
run() | 线程的工作函数,主线程通过start启动线程后,线程就会完成此函数所定义的内容 |
stop() | 线程停止 |
Yield() | 线程发送心跳包接收下行消息 |
Log() | 副线程传向主线程的信号量,接收到消息后通过该信号量提醒主线程有事件产生,同时传递消息参数 |
Yield函数具体实现:
void ThreadReceive::Yield(void *pclient)
{
IOT_MQTT_Yield(pclient, 200);
}
该函数中调用了SDK包中的IOT_MQTT_Yield函数,这个函数的作用就是向云端发送心跳包,表示自己在线,请求继续连接,同时会接收所订阅Topic的下行消息。
处理收到消息后处理的函数Message_arrive:
void message_arrive(void *pcontext, void *pclient, iotx_mqtt_event_msg_t* comemsg)
{
iotx_mqtt_topic_info_t *topic_info = (iotx_mqtt_topic_info_pt)(comemsg->msg);
if(comemsg->event_type == IOTX_MQTT_EVENT_PUBLISH_RECEIVED)
{
char arvstr[topic_info->payload_len];
strcpy(arvstr,topic_info->payload);
json_to_msg(dataKind,data,note,arvstr);
strcpy(topicdes,topic_info->ptopic);
dataLen = topic_info->payload_len;
}
return;
}
该函数将发送来的json格式消息通过json_to_msg函数拆包为了多个属性分别传递给了先前定义的全局变量中。
订阅函数实现,功能就是订阅指定的Topic:
int subscribe_to_cloud(void *handle)
{
int res = 0;
const char *fmt = "/%s/%s/user/get";
char *topic = NULL;
int topic_len = 0;
topic_len = strlen(fmt) + strlen(_product_key) + strlen(_device_name) + 1;
topic = (char*)HAL_Malloc(topic_len);
if (topic == NULL) {
qDebug()<<"Topic error";
return -1;
}
memset(topic, 0, topic_len);
HAL_Snprintf(topic, topic_len, fmt, _product_key, _device_name);
res = IOT_MQTT_Subscribe(handle, topic, IOTX_MQTT_QOS0,message_arrive,NULL);
if (res < 0) {
qDebug()<<("subscribe failed");
HAL_Free(topic);
return -1;
}
qDebug()<<"Subscribed";
HAL_Free(topic);
return 0;
}
订阅函数看似简单,但是在IOT_MQTT_Subscribe函数中,将message_arrive函数的地址传递到了底层中的某个地方,而message_arrive就会在IOT_MQTT_Yield函数中被调用。
run函数具体实现:
void ThreadReceive::run()
{
strcpy(data,"");
res = subscribe_to_cloud(pclient);
if(res < 0)
{
IOT_MQTT_Destroy(&pclient);
qDebug()<<"订阅失败";
}
while(!stopped)
{
Yield(pclient);
if(strcmp(data,"")!=0)
{
qDebug()<<("有消息将显示");
emit Log(topicdes,dataKind,dataLen,data,note);//回调
sleep(1);
}
sleep(1);
strcpy(data,"");
}
stopped = false;
}
该函数就是线程的运行函数,首先会调用subscribe_to_cloud函数来订阅指定的Topic,然后循环执行Yield函数进行心跳包的维持发送和接收下行消息,一旦接收到消息,就会emit Log向主线程传达Log信号,并且将几个已有数据的全局变量的地址作为参数传递给主线程。
用于回显副线程传递消息的Widget界面设计
定义:
class FormList : public QWidget
{
Q_OBJECT
public:
explicit FormList(QWidget *parent = nullptr);
~FormList();
Ui::FormList *ui;
QStringListModel *ListModel;
};
初始化及model/view对接:
FormList::FormList(QWidget *parent) :
QWidget(parent),
ui(new Ui::FormList)
{
ui->setupUi(this);
ListModel=new QStringListModel(this);
ui->listView->setModel(ListModel);
ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::SelectedClicked);
}
界面设计:
放入一张QListView即可
主线程的创建与实现
一个全局变量,副线程Monitor监听是否有消息到来:
ThreadReceive *Monitor;
主线程的定义:
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QStringListModel *theModel;
int res = 0;
int payload_len = 0;
iotx_mqtt_param_t mqtt_params;
const char *fmt = "{\"%s\":\"%s\"}";
char *key = (char*)malloc(64),*value = (char*)malloc(1024);
char *payload = NULL;
int publish_to_cloud(char *payload);
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_btnSend_clicked();
void Change(char *topic,char *kind, int len, char* data, char* note);
void on_btnSendPicture_clicked();
private:
Ui::MainWindow *ui;
};
主要函数 | 功能描述 |
Pic_encode | 对接收到的图片进行处理 |
publish_to_cloud | 向指定Topic发布消息 |
on_btnSend_clicked | 单击发送按钮事件槽函数 |
on_btnSendPicture_clicked | 单击发送图片按钮事件槽函数 |
Change | Log接收信号事件槽函数 |
构造函数初始化工作:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
QStringList theStrList;
theStrList<<"消息";
theModel=new QStringListModel(this);
theModel->setStringList(theStrList);
ui->listView->setModel(theModel);
ui->listView->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::SelectedClicked);
Monitor = new ThreadReceive();
connect(Monitor,SIGNAL(Log(char*,char*,int,char*,char*)),this,SLOT(Change(char*,char*,int,char*,char*)));
memset(&mqtt_params, 0x0, sizeof(mqtt_params));
mqtt_params.handle_event.h_fp = event_handle;
pclient = IOT_MQTT_Construct(&mqtt_params);
if(pclient == NULL)
{
//messageBox
printf("链接失败");
}
(*Monitor).start();
}
pic_encode处理图片数据
int Pic_encode(char* filename,char *resultBase64)
{
unsigned int imageSize; //编码图片数据大小
unsigned int base64StrLength; //编码后图片数据大小
char *imageBin;
char *imageBase64;
size_t resultBase;
FILE *fp = NULL;
fp=fopen(filename,"rb");
if(fp == NULL)
{
printf("file open error1");
return -1;
}
fseek(fp , 0L , SEEK_END);
imageSize = ftell(fp);
fseek(fp , 0L , SEEK_SET);
imageBin = (char*)malloc(sizeof(char) *imageSize);
if(imageBin == NULL)
{
//printf("malloc error");
return -1;
}
resultBase = fread(imageBin, 1, imageSize, fp);
if(resultBase != imageSize)
{
//printf("file read error");
return -1;
}
fclose(fp);
imageBase64 = (char*)malloc(sizeof(char) *imageSize*2);
if(imageBase64 == NULL)
{
//printf("malloc error");
return -1;
}
base64_encode(imageBin, imageBase64, imageSize); //调用编码函数
base64StrLength = strlen(imageBase64);
base64StrLength = (base64StrLength / 4) *3;
strcpy(resultBase64,imageBase64);
return 1;
}
该函数实现了对图片数据的解码并保存至指定文件。
publish_to_cloud发布消息函数:
int MainWindow::publish_to_cloud(char *payload)
{
int res = 0;//判断是否成功的结果
const char *fmt = "/%s/%s/user/transmit";//Topic形式
char *topic = NULL;//目标Topic
int topic_len = 0;//Topic长度
topic_len = strlen(fmt) + strlen(_product_key) + strlen(_device_name) + 1;
topic = (char*)HAL_Malloc(topic_len);//根据Topic长度预分配内存空间
if (topic == NULL) {//若内存不足
qDebug()<<"memory NOT enough";
return -1;
}
memset(topic, 0, topic_len);//内存空间分配
HAL_Snprintf(topic, topic_len, fmt, _product_key, _device_name);//补全Topic
qDebug()<<payload;
qDebug()<<strlen(payload);
res = IOT_MQTT_Publish_Simple(0, topic, IOTX_MQTT_QOS0, payload, strlen(payload));//发布消息
if (res < 0) {//判断是否发布成功
HAL_Free(topic);//释放Topic
qDebug()<<"Send fail";
return -1;
}
qDebug()<<"Send OK";
HAL_Free(topic);//释放Topic
return 0;
}
单击发送按钮事件实现:
void MainWindow::on_btnSend_clicked()
{
QString qstr = ui->SenderEdit->toPlainText();//获得输入
QByteArray qByteArray = qstr.toUtf8();
char *cstr = qByteArray.data();
char *jtype = new char[5];
strcpy(jtype,"text");
char *jnote = new char[1];
strcpy(jnote,"");
cstr = new_entry(jtype,cstr,jnote);
this->publish_to_cloud(cstr);
ui->SenderEdit->clear();
}
单击发送图片按钮事件实现:
void MainWindow::on_btnSendPicture_clicked()
{
QString curPath = QDir::currentPath();
QString dlgTitle = "选择一张图片";
QString filter = "图片文件(*.jpg *.gif *.png *.svg)";
QString aFileName = QFileDialog::getOpenFileName(this,dlgTitle,curPath,filter);
QByteArray aFileNameArray = aFileName.toUtf8();
QUrl aFileUrl = QFileDialog::getOpenFileUrl(this,dlgTitle,curPath,filter);
QString aFileUrlString = aFileUrl.toString();
int n = aFileUrlString.indexOf('/',3);
aFileUrlString = aFileUrlString.right(aFileUrlString.size()-n-1);
QByteArray aFileByteArray = aFileUrlString.toUtf8();
char *cstr = (char *)malloc(200);
strcpy(cstr,aFileByteArray.data());
if(!aFileName.isEmpty())
{
int m = aFileNameArray.lastIndexOf('/');
aFileNameArray = aFileNameArray.right(aFileNameArray.size()-m-1);
char *picBase = (char*)malloc(256*1024);
Pic_encode(cstr,picBase);
publish_to_cloud(new_entry("Picture",picBase,aFileNameArray.data()));
}
}
该函数打开一个文件对话框让用户去选择一张图片以进行发送。
接收信号事件实现:
void MainWindow::Change(char *tp,char *ki,int le,char *da,char *no)
{
QDateTime local(QDateTime::currentDateTime());
QString localTime = local.toString("yyyy-MM-dd:hh:mm:ss");
char inttemp[100];
sprintf(inttemp,"%d",le);
QString qtp(tp),qtpgai;
int indextemp=qtp.indexOf("{");
qtpgai = qtp.left(indextemp);
if(strcmp(ki,"Temperature")==0)
{
ui->Temperature->setText(da);
}
else if(strcmp(ki,"Picture")==0)
{
FILE *fp = NULL;
char *imageoutput = (char*)malloc(1024*256);
base64_decode(da, imageoutput);
char tempstr[10];
strcpy(tempstr,"../Pics/");
strcat(tempstr,no);
fp = fopen(tempstr, "wb"); //新建一个图片文件,输入解码后数据
if(fp == NULL)
{
qDebug()<<"Fail Error";
return ;
}
fwrite(imageoutput, 1, strlen(imageoutput), fp);
fclose(fp);
}
else
{
char *ttemp[4];
char stp[100];
char ski[100];
char sle[100];
char sda[10000];
ttemp[0] = stp;
ttemp[1] = ski;
ttemp[2] = sle;
ttemp[3] = sda;
strcpy(ttemp[0],"Topic:\t");
strcpy(ttemp[1],"数据类型:\t");
strcpy(ttemp[2],"数据长度:\t");
strcpy(ttemp[3],"数据:\t");
strcat(ttemp[0],qtpgai.toUtf8().data());
strcat(ttemp[1],ki);
strcat(ttemp[2],inttemp);
strcat(ttemp[3],da);
FormList *formList = new FormList();
theModel->insertRow(theModel->rowCount());//在尾部插入一空行
QModelIndex index=theModel->index(theModel->rowCount()-1,0);//获取最后一行
theModel->setData(index,localTime,Qt::DisplayRole);
formList->setAttribute(Qt::WA_DeleteOnClose);
formList->setWindowTitle(localTime);
formList->setWindowFlag(Qt::Window,true);
formList->setWindowOpacity(0.9);
for(int i = 0; i < 4; i++)
{
formList->ListModel->insertRow(i);
QModelIndex listIndex = formList->ListModel->index(formList->ListModel->rowCount()-1,0);
formList->ListModel->setData(listIndex,ttemp[i],Qt::DisplayRole);
formList->ui->listView->setCurrentIndex(listIndex);
}
ui->listView->setCurrentIndex(index);//设置选中的行
formList->show();
}
}
在该函数中进行了数据类型的判断,以此确认是要去回显文本还是接收保存图片又或者回显温度数据。
项目传送门
移动物联网项目搭建(一)——起步移动物联网项目搭建(二)——云端配置移动物联网项目搭建(三)——SDK抽取与Qt工程建立移动物联网项目搭建(五)——采集端设计