目录
一、项目介绍
二、前期准备
1.硬件准备
2.开发环境
三、C语言的简单工厂模式
1.工厂模式介绍
2.类和对象
3.工厂模式的优缺点
四、树莓派的基本使用
1.树莓派刷机和登入
2.树莓派WiringPi库介绍
3.树莓派 CSI 摄像头配置
4.树莓派与其他模块接线
五、智能家居设计框架
1.产品工厂
2.指令工厂
六、项目总结
一、项目介绍
该项目主控为树莓派4B开发板,使用 wiringPi 库结合 DHT11 温湿度传感器、火焰传感器、振动传感器、烟雾传感器、蓝牙模块、L298N电机驱动模块、继电器组模块、SU-03T语音模块、CSI 摄像头模块、风扇模块等,可实现语音控制、网络控制、自制蓝牙手机APP或者安卓APP控制继电器状态从而控制灯、风扇、警报器的开关,可以结合 mjpg-streamer 库,CSI 摄像头实现视频监控和通过翔云人工智能开放平台或阿里云进行人脸识别进而通过 PWM 控制舵机旋转模拟开关门状态,当有火花或振动时会触发报警器报警,报警器状态、温湿度采集和通风扇状态可在手机APP上查看。
采用工厂模式,将以上模块分为指令工厂和设备工厂,语音模块通过串口产生的指令和手机 app 通过串口蓝牙或 socket 产生的指令为指令工厂,其余模块为设备工厂,指令工厂和设备工厂分别用单链表进行链接,指令工厂产生指令,树莓派4B收到指令然后通过指令在设备工厂寻找设备从而进行控制。
扩展:后续可以自行增加其他模块和功能,例如红外遥控、人体红外模块、使用OpenCV实现人脸识别、QT上位机控制下位机树莓派、增加MQTT协议等,等暑假有时间我再把扩展功能一起实现呈现给大家。
二、前期准备
1.硬件准备
首先我们需要一块树莓派作为主控,树莓派3B、4B、5B都行,然后需要准备两盒电池和各种各样的模块,下面我列出硬件清单。
硬件清单:
树莓派、CSI 摄像头模块、DHT11 温湿度传感器、火焰传感器、振动传感器、烟雾传感器、蓝牙模块、L298N电机驱动模块、继电器组模块、SU-03T语音模块、风扇模块、sg90舵机、红外遥控、红外模块、oled屏幕、人体红外模块、面包板加电池供电。
2.开发环境
做该项目需要先学习C语言、单片机、linux系统编程、ARM开发、QT等知识。
开发环境主要有:
- ubuntu
- mobaxterm
- 手机APP
- QT上位机(后续扩展)
三、C语言的简单工厂模式
1.工厂模式介绍
工厂模式是最常用的设计模式之一,属于创建型模式,提供一种创建对象的最佳方式。在工厂模式中,创建对象时,不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。即工厂模式把创建对象和使用对象的两个过程分离,对于使用者无需关心对象时如何创建出来的,直接指定一个对象即可使用该对象的方法。
C语言不是一门面向对象的语言,而是一门面向过程的语言,但C语言一样支持面向对象编程,即支持简单工厂模式。
工厂模式就是类的继承,C语言使用函数指针来实现类的继承,工厂中的设备用链表来链接起来。
2.类和对象
- 类:一种用户定义的数据类型,也称类类型,例如结构体。
- 对象:类的一种实例化,例如结构体赋值。
具体介绍可看类和对象详细介绍
3.工厂模式的优缺点
优点
- 在创建对象时,只需要知道对象的名字就行,代码维护性强。
- 需要添加设备时,只需要添加一个对象就行了,代码扩展性。
缺点
- 代码阅读性变复杂。
- 设备增多时会使对象增多,提高了代码复杂度。
四、树莓派的基本使用
1.树莓派刷机和登入
在这里使用的是开发板ssh登入,具体看我之前写过的文章,链接如下:树莓派刷机和登入
2.树莓派WiringPi库介绍
具体看我之前写过的文章,链接如下:树莓派WiringPi库总结
3.树莓派 CSI 摄像头配置
具体看我之前写过的文章,链接如下:树莓派 CSI 摄像头配置
4.树莓派与其他模块接线
下载完 WiringPi 库后输入命令 gpio readall 查看树莓派引脚
具体接线:
- 火焰传感器:GPIO 0
- 烟雾传感器:GPIO 1
- SG90舵机:GPIO 2
- 温湿度传感器:GPIO 7
- OLED模块:SDA连对应SDA、SCL连对应SCL
- 蓝牙模块或者语音模块:Tx连Rx、Rx连Tx
- 继电器组控制四盏灯:GPIO 21-24
- 报警器:GPIO 25
- 风扇模块电机驱动:GPIO 27 28
- 振动传感器:GPIO 29
五、智能家居设计框架
该智能家居由两个工厂和若干个设备组成,分别是产品工厂和指令工厂。包含功能如下:
1.产品工厂
该工厂主要是用来生产一些供人们使用的设备,如各种灯设备、OLED屏幕显示设备、环境数据检测设备、检测安全设备等等。
创建产品工厂的类,即产品工厂的生产线:
- 设备名称
- 设备状态
- 设备引脚
- 设备数据存储区
- 设备功能
- 打开
- 关闭
- 设备初始化
- 读状态位
- 改变状态位
- 读数据
- 链表链接节点,使对应的设备上线成为商品
controlDevices.h
struct Devices
{
char devicesName[128]; // 设备名称
int status; // 设备状态
int pinNum; // 设备引脚
unsigned long databuf; // 设备数据存储区
/* 设备功能 */
int (*open)(int pinNum);
int (*close)(int pinNum);
int (*devicesInit)(int pinNum);
int (*readStatus)(int pinNum);
int (*changeStatus)(int status);
int (*readData)(struct Devices *devices,int pinNum);
struct Devices *next; // 链表链接节点
};
具体产品设备有:
- 一楼灯设备
- 二楼灯设备
- 餐厅灯设备
- 卧室灯设备
- OLED屏幕显示设备
- 风扇设备
- 锁设备
- 警报器设备
- 地震监测设备
- 火灾监测设备
- 烟雾监测设备
- 温湿度监测设备
四盏灯设备代码示例:代码基本相同,只列举一个
#include "controlDevices.h"
int bathroomLightOpen(int pinNum)
{
digitalWrite(pinNum,LOW);
}
int bathroomLightClose(int pinNum)
{
digitalWrite(pinNum,HIGH);
}
int bathroomLightInit(int pinNum)
{
pinMode(pinNum,OUTPUT);
digitalWrite(pinNum,HIGH);
}
struct Devices bathroomLight = {
.devicesName = "bathroom",
.pinNum = 21,
.open = bathroomLightOpen,
.close = bathroomLightClose,
.devicesInit = bathroomLightInit,
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct Devices* addBathroomLightToDevicesLink(struct Devices *phead) {
if (phead == NULL) {
phead = &bathroomLight;
return phead;
} else {
bathroomLight.next = phead;
phead = &bathroomLight;
return phead;
}
}
风扇设备代码示例:
#include "controlDevices.h"
int fanOpen(int pinNum)
{
digitalWrite(27,LOW);
digitalWrite(28,HIGH);
}
int fanClose(int pinNum)
{
digitalWrite(27,LOW);
digitalWrite(28,LOW);
}
int fanInit(int pinNum)
{
pinMode(27,OUTPUT);
pinMode(28,OUTPUT);
digitalWrite(27,LOW);
digitalWrite(28,LOW);
}
struct Devices fan = {
.devicesName = "fan",
.pinNum = 27,
.open = fanOpen,
.close = fanClose,
.devicesInit = fanInit,
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct Devices* addfanToDevicesLink(struct Devices *phead) {
if (phead == NULL) {
phead = &fan;
return phead;
} else {
fan.next = phead;
phead = &fan;
return phead;
}
}
火灾、地震、烟雾监测设备代码示例:代码基本相同,只列举一个
#include "controlDevices.h"
int fireInit(int pinNum)
{
pinMode(pinNum,INPUT);
digitalWrite(pinNum,HIGH);
}
/* 检测火灾发生情况 */
int fireReadStatus(int pinNum)
{
return digitalRead(pinNum);
}
struct Devices fire = {
.devicesName = "fire",
.pinNum = 0,
.devicesInit = fireInit,
.readStatus = fireReadStatus,
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct Devices* addfireToDevicesLink(struct Devices *phead) {
if (phead == NULL) {
phead = &fire;
return phead;
} else {
fire.next = phead;
phead = &fire;
return phead;
}
}
温湿度监测代码示例:
#include "controlDevices.h"
int sensorInit(int pinNum)
{
pinMode(pinNum,OUTPUT);
digitalWrite(pinNum,HIGH);
}
/* 获取温度传感器的数据 */
int sensorReadData(struct Devices *sensorMsg,int pinNum)
{
char crc;
char i;
unsigned long databuf; // 温湿度存储地址
pinMode(pinNum, OUTPUT);
digitalWrite(pinNum, 0);
delay(25);
digitalWrite(pinNum, 1);
pinMode(pinNum, INPUT);
pullUpDnControl(pinNum, PUD_UP); // 启用上拉电阻,引脚电平拉倒3.3v
delayMicroseconds(27);
if (digitalRead(pinNum) == 0)
{
while (!digitalRead(pinNum));
for (i = 0; i < 32; i++)
{
while (digitalRead(pinNum));
while (!digitalRead(pinNum));
delayMicroseconds(32);
databuf *= 2;
if (digitalRead(pinNum) == 1)
{
databuf++;
}
}
for (i = 0; i < 8; i++)
{
while (digitalRead(pinNum));
while (!digitalRead(pinNum));
delayMicroseconds(32);
crc *= 2;
if (digitalRead(pinNum) == 1)
{
crc++;
}
}
sensorMsg->databuf = databuf;
return 1;
}
else
{
return 0;
}
}
/* 继承产品工厂的类,生产温度检测设备 */
struct Devices sensor = {
.devicesName = "sensor",
.pinNum = 7,
.devicesInit = sensorInit,
.readData = sensorReadData,
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct Devices* addsensorToDevicesLink(struct Devices *phead)
{
if(phead == NULL){
phead = &sensor;
return phead;
}else{
sensor.next = phead;
phead = &sensor;
return phead;
}
}
2.指令工厂
该工厂主要是用来生产一些供人们发出指令控制的设备,如语音设备、蓝牙设备、server设备、人脸识别设备等等,这些设备可以控制产品工厂的设备做出一些控制行为。
创建指令工厂的类,即指令工厂的生产线:
- 指令内容
- 指令名称
- 日志
- 语言模块或蓝牙模块
- 文件的fd
- 设备名称
- 网络通信
- server的fd
- 端口号
- IP地址
- 人脸识别
- 指向主人的人脸图片
- 指向临时的人脸图片
- 人脸识别匹配度
- 设备功能
- 指令设备初始化
- 获取指令设备
- 处理函数
- 链表链接节点,使对应的设备上线成为商品
inputCmd.h
struct InputCmd
{
int fd; //文件的fd
char deviceName[128]; //设备名称
char cmd[64]; //指令内容
char cmdName[128]; //指令名称
char log[1024]; //日志
int sfd; //server的fd
char port[12]; //端口号
char ipAress[32]; //IP地址
char *img_buf; //指向主人的人脸图片数据存储区的指针
char *camera_buf; //指向临时的人脸图片数据存储区的指针
float face_data; //人脸识别匹配度
/* 设备功能 */
int (*Init)(struct InputCmd *voicer,char *ipAdress,char *port);
int (*getCmd)(struct InputCmd *voicer);
int (*ReadHandle)(void *ptr, size_t size, size_t nmemb, void *stream);
struct InputCmd *next; //链表链接节点
};
具体指令设备有:
- 语音设备
- 蓝牙设备
- server设备
- 人脸识别设备
树莓派串口通信详细介绍可看我之前写过的文章,链接如下:树莓派串口通信
语音设备、蓝牙设备代码示例:代码基本相同,只列举一个。
#include "inputCmd.h"
/* 语音控制设备初始化,启用串口 */
int voiceInit(struct InputCmd *voicer,char *ipAdress,char *port)
{
int fd;
if((fd = serialOpen(voicer->deviceName,9600))==-1){ //打开串口
exit(-1);
}
voicer->fd = fd;
return fd;
}
/* 读取来自语音控制设备的命令 */
int voiceGetCmd(struct InputCmd *voicer)
{
char tmp[128];
int nread;
memset(tmp,'\0',sizeof(tmp));
nread = read(voicer->fd,tmp,sizeof(tmp)); // 一切皆文件,串口也是文件,读串口数据
if(nread == 0){
printf("usart for voice read overtime\n");
}else if(nread > 0){
memset(voicer->cmd,'\0',sizeof(voicer->cmd));
/* 对串口数据进行优化 */
if(strstr(tmp,"open ba") != NULL){
strcat(voicer->cmd,"open bathroom light");
}
if(strstr(tmp,"close ba") != NULL){
strcat(voicer->cmd,"close bathroom light");
}
if(strstr(tmp,"open li") != NULL){
strcat(voicer->cmd,"open livingroom light");
}
if(strstr(tmp,"close li") != NULL){
strcat(voicer->cmd,"close livingroom light");
}
if(strstr(tmp,"open re") != NULL){
strcat(voicer->cmd,"open restaurant light");
}
if(strstr(tmp,"close re") != NULL){
strcat(voicer->cmd,"close restaurant light");
}
if(strstr(tmp,"open se") != NULL){
strcat(voicer->cmd,"open secondfloor light");
}
if(strstr(tmp,"close se") != NULL){
strcat(voicer->cmd,"close secondfloor light");
}
if(strstr(tmp,"open bu") != NULL){
strcat(voicer->cmd,"open buzzer light");
}
if(strstr(tmp,"close bu") != NULL){
strcat(voicer->cmd,"close buzzer light");
}
if(strstr(tmp,"open fn") != NULL){
strcat(voicer->cmd,"open fan light");
}
if(strstr(tmp,"close fn") != NULL){
strcat(voicer->cmd,"close fan light");
}
if(strstr(tmp,"open fac") != NULL){
strcat(voicer->cmd,"open face light");
}
if(strstr(tmp,"open lo") != NULL){
strcat(voicer->cmd,"open lock light");
}
if(strstr(tmp,"close lo") != NULL){
strcat(voicer->cmd,"close lock light");
}
return nread;
}else{
printf("read is fali\n");
}
}
/* 继承指令工厂的类,生产语音控制设备 */
struct InputCmd voiceControl = {
.cmdName = "voice",
.deviceName = "/dev/ttyAMA0",
.cmd = {'\0'},
.Init = voiceInit,
.getCmd = voiceGetCmd,
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct InputCmd* addvoiceControlToInputCmdLink(struct InputCmd *phead)
{
if(phead == NULL){
phead = &voiceControl;
return phead;
}else{
voiceControl.next = phead;
phead = &voiceControl;
return phead;
}
}
网络通信详细介绍和编程可看我之前写过的文章,链接如下:
网络通信TCP、UDP介绍
网络通信TCP、UDP编程详解
server设备代码示例:
#include "inputCmd.h"
/* server控制设备初始化,为套接字绑定端口号和IP地址 */
int serverInit(struct InputCmd *serverMsg,char *ipAdress,char *port)
{
int s_fd;
struct sockaddr_in s_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(serverMsg->port));
inet_aton(serverMsg->ipAress,&s_addr.sin_addr);
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
listen(s_fd,10);
serverMsg->sfd = s_fd;
return s_fd;
}
/* 继承指令工厂的类,生产server控制设备 */
struct InputCmd serverControl = {
.cmdName = "server",
.cmd = {'\0'},
.log = {'\0'},
.Init = serverInit,
.port = "8888",
.ipAress = "192.168.137.115", // 树莓派ip地址
.next = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct InputCmd* addserverControlToInputCmdLink(struct InputCmd *phead)
{
if(phead == NULL){
phead = &serverControl;
return phead;
}else{
serverControl.next = phead;
phead = &serverControl;
return phead;
}
}
人脸识别设备代码示例:该设备通过使用 curl 第三方库访问网站,调用祥云人脸识别平台进行人脸识别。
#include "inputCmd.h"
char post_info[500];
/* 人脸识别设备初始化 */
int cameraInit(struct InputCmd *camera,char *ipAdress,char *port)
{
int fd;
int imgSize=0;
char cmd[30];
memset(post_info,'\0',500);
curl_global_init(CURL_GLOBAL_ALL); //curl库初始化
/* 获取主人的人脸图片的base64编码 */
fd = open("imgfile.txt",O_RDWR,0666); //打开用来存放主人的人脸图片的base64编码的文件
imgSize = lseek(fd,0,SEEK_END);
camera->img_buf = (char *)malloc(imgSize + 8);
memset(camera->img_buf,'\0',imgSize + 8);
lseek(fd,0,SEEK_SET);
read(fd,camera->img_buf,imgSize); //将主人的人脸图片的base64编码读到img_buf数据存储区
close(fd);
system("raspistill -q 5 -o haozi.jpg"); //调用树莓派摄像头拍摄图片,-q 5为降低画质
while(access("./haozi.jpg",F_OK) != 0); //等待拍摄完成
sprintf(cmd,"base64 %s >camerafile.txt","haozi.jpg"); //字符串拼接
if(system(cmd) == 256){ //生成摄像头拍摄图片的base64编码的图像地址
return -1;
}
/* 获取临时拍摄的人脸图片的base64编码 */
fd = open("camerafile.txt",O_RDWR,0666); //打开用来存放临时拍摄的人脸图片的base64编码的文件
imgSize = lseek(fd,0,SEEK_END);
camera->camera_buf = (char *)malloc(imgSize + 8);
memset(camera->camera_buf,'\0',imgSize + 8);
lseek(fd,0,SEEK_SET);
read(fd,camera->camera_buf,imgSize); //将临时拍摄的人脸图片的base64编码读到camera_buf数据存储区
close(fd);
system("rm camerafile.txt");
system("rm haozi.jpg");
if((camera->img_buf == NULL)||(camera->camera_buf == NULL)){
printf("img dont exist!\n");
return -1;
}
return 0;
}
/* 获取祥云平台的人脸识别结果数据 */
int cameraReadHandle( void *ptr, size_t size, size_t nmemb, void *stream)
{
int buf_size=size*nmemb;
char *buf=malloc(buf_size+1);
memset(buf,'\0',buf_size+1);
strncpy(buf,ptr,buf_size); //获取祥云平台的人脸识别结果
memset(post_info,'\0',sizeof(post_info));
strcat(post_info,buf);
free(buf);
return buf_size;
}
/* 调用祥云平台进行人脸识别 */
int cameraGetCmd(struct InputCmd *camera)
{
CURL *curl;
CURLcode res;
char *PostString;
/* 祥云平台的个人基本信息 */
char *key="6M9M9rFAa3DuG33Awu9hKq";
char *secret="db56882c3a0b4a3dbeeaed2af79f026c";
int typeId=21;
char *format="xml";
char *result_str = NULL;
float result_data;
int len = strlen(camera->img_buf)+strlen(camera->camera_buf)+strlen(key)+strlen(secret)+sizeof(typeId)+strlen(format) + 24;
PostString=malloc(len);
sprintf(PostString,"&img1=%s&img2=%s&key=%s&secret=%s&typeId=%d&format=%s",camera->img_buf,camera->camera_buf,key,secret,typeId,format); //设置需要发送给祥云平台的数据格式
curl = curl_easy_init(); //获取句柄
if (curl){
curl_easy_setopt(curl, CURLOPT_URL, "https://netocr.com/api/faceliu.do"); //设置访问的URL,即祥云人脸识别的接口地址
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,PostString); //向祥云平台发送数据,调用人脸识别功能
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, camera->ReadHandle); //设置回调函数,该函数用来接收人脸识别的结果
res = curl_easy_perform(curl);//错误检测,0没有错误,非0出现错误
curl_easy_cleanup(curl);
/* 处理人脸识别的结果数据 */
result_str = strstr(post_info,"CDATA");
if(result_str != NULL){
result_str += 6;
result_str = strtok(result_str,"]");
result_data = atof(result_str);
}else{
printf("get result error!\n");
}
}else{
printf("get result error!\n");
}
camera->face_data = result_data;
return result_data;
}
/* 继承指令工厂的类,生产人脸识别设备 */
struct InputCmd cameraControl = {
.cmdName = "camera",
.cmd = {'\0'},
.log = {'\0'},
.Init = cameraInit,
.getCmd = cameraGetCmd,
.ReadHandle = cameraReadHandle,
.next = NULL,
.img_buf = NULL,
.camera_buf = NULL,
};
/* 头插法加入链表,提供购买渠道,以供客户购买 */
struct InputCmd* addcameraControlToInputCmdLink(struct InputCmd *phead)
{
if(phead == NULL){
phead = &cameraControl;
return phead;
}else{
cameraControl.next = phead;
phead = &cameraControl;
return phead;
}
}
完整代码我后续再上传到资源
编译时要链接 wiringPi 库跟线程库:gcc *.c *.h -lwiringPi -lpthread
编译调试图示:
六、项目总结
总体来说,只要掌握好C语言、裸机外设和Linux,整个项目做下来其实难度并不大,从这个项目中我们可以学到面向对象的编程思想和练习对多个外设模块进行控制。
该项目还有很多可以扩展的地方,后续我打算用IMX6ULL 加 MQTT 重新实现一下智能家居,再用QT作为上位机控制下位机(选择 IMX6ULL),使用 OpenCV 进行人脸识别从而进行开关锁,计划在暑假实现全部扩展功能,让不同的开发板也可以完美实现该项目。