本项目须要实现以嵌入式Web服务器为核心的视频监控系统。css
摄像头采集的到的图像通过压缩后,传到内置的web服务器中。用户只须要经过浏览器就能够观看摄像头采集到的数据。html
除了视频监控功能外,本项目还能够搜集空气湿度、空气温度、光照强度等信息。前端
该项目基本能够看做B/S架构,由有三部分组成:java
(1)核心服务端:实际上是客户端(严格意义上说,PC上的浏览器才是客户端)一台普通的PC机,须要与前端数据中心在同一局域网中,而后经过浏览器对系统进行监控和设置。linux
(2)前端数据中心:FS4412开发平台,能够链接摄像头、GPRS、zigBee、传感器、WIFI等模块。ios
(3)远程控制终端:zigBee模块、摄像头等用来进行信息采集的设备。采集到的信息有前端数据中心进行处理。web
该远程监控系统支持chrome浏览器和Firefox浏览器,但不支持IE浏览器。sql
(1)环境搭建chrome
(2)编译源码数据库
(3)镜像烧写
(4)前端数据中心数据接收与处理模块的调试
(5)理解Web服务器的搭建与配置
(6)理解CGI程序的编译调试
仅需在VMware Workstation Pro打开打开虚拟机便可
编译源码的环境是在Linux下,首先应先熟悉Linux终端的一些操做。
cd 进入一个目录 如cd /usr/local 或者是cd usr,用cd ..返回上一层。
利用tab来补全命令。
Mkdir 是一个用来在 Linux 系统下建立目录的命令。是一层一层的建立,能够建立后用cd打开而后建立。
cp命令用来将一个或多个源文件或者目录复制到指定的目的文件或目录。如cp/xx/xx ./是将文件拷贝到当前目录下。利用这两条指令以及cd指令拷贝源码。
编译源码须要修改环境变量,BootLoader编译,Linux内核编译,主应用程序编译,根文件系统镜像制做。
①完成bootloader的编译与SD bootloader的编译
②Linux内核的编译
下面是根据指导书进入Linux内核配置图形界面。
下面是用make工具完成zImage内核二进制文件的截图
等待片刻以后,linux内核编译完成
③ 主应用程序编译
直接用make工具便可
④ 根文件系统镜像制做
在制做根文件时须要将以前编译好的应用程序、驱动等项目拷贝到rootfs下。
①制做SD卡启动盘
用读卡器将SD卡插入电脑,虚拟机识别SD读卡器,将uboot烧写到sd卡,将SD卡插入开发板SD卡槽内,拨码至1000,链接开发板。在win上设置串口调试工具,选择Serial链接方式(若是不选择的话直接open会致使乱码,其中COMX由设备管理器看)。启动开发板,在倒计时5s结束前,按任意键中止。
②安装Fastboot
在安卓手机中fastboot是一种比recovery更底层的刷机模式。就是使用USB数据线链接手机的一种刷机模式。相对于某些系统(如ios)卡刷来讲,线刷更可靠,安全。
遇到问题:fastboot驱动安装的强制签名问题
解决方案:
经过配置win10关闭驱动签名(百度上轻松找到解决方案)
在计算机属性下的系统属性,查看高级,选择环境变量,编译系统变量的path项,在变量值最后添加D:\Fastboot.使用win+r,输入cmd,输入fastboot测试是否安装成功。
在设备管理器中是否有Android,有则更新驱动,路径选择D:\Fastboot。
③烧写到开发板的Flash
硬件链接后拨动至1000 ,设置串口工具后,启动开发板,按任意键暂停,输入sdfuse flashall。等烧写结束,关闭开发板,拨至0110,启动开发板,暂停。输入fdisk -c 0
对SD卡分区,输入fastboot,再执行flash-all.bat。
在cmd命令行中输入fastboot flash BootLoader u-boot-fs4412.bin完成烧写BootLoader。
在cmd命令行中输入fastboot flash kernel zimage完成烧写内核镜像zImage。
在cmd命令行中输入fastboot flash system system.img完成烧写根文件系统镜像。
①WIFI模块的移植——内核配置
配置内核
编译内核
②WIFI模块——wpa_suppliant的移植
openssl补丁安装
Openssl安装
注意权限问题——最后应该输入sudo make install
wpa_suppliant的移植——修改Makefile
使用make工具编译
==遇到问题==:不知道如何测试,文档中没有明确说明如何操做
后咱们选择使用网线直连,其中若是须要修改网卡IP,因为是直连,输入ifconfig eth0 192.168.x.x
③Web服务器的配置
==遇到问题==:
解决方法
按照指导书修改过compat.h
Make工具进行编译
我本身有过Web编程的经验,也了解早期的Web编程是靠CGI来完成的,用的语言也是五花八门——C/C++、perl、bash……
我选择研究该项目的CGI源码,也是由于本身对Web编程有一些了解,而对系统编程就彻底不懂,也搞不清什么线程、锁、同步的概念。
①登陆表单处理——login.cgi
咱们进入文件系统rootfs的www目录下,这里就是存放web服务器html,css,js和cgi程序的地方。咱们先看看index.html.
咱们直接看登陆的表单部分。
<form name="form1" method="post" action="cgi-bin/login.cgi"> <table width="100%" border="0" cellspacing="9" cellpadding="0"> <tbody> <tr> <td width="92">用户账号:</td> <td width="130"><label> <input name="username" type="text" id="username" value="user"></label></td> </tr> <tr> <td>登陆密码:</td> <td> <label> <input name="password" type="password" id="password" value="123456"> </label> </td> </tr> <tr> <td height="25"></td> <td><input type="image" name="submit" style="width:97px;height:25px;" src="images/login/go.gif"></td> </tr> </tbody> </table> </form>
这个表单的数据会提交给login.cgi这个程序去处理。学过java servlet的同窗会发现,CGI程序和servlet比较接近。
咱们来分析一下login.cgi的源码。
//login.c cgiFormStringNoNewlines("username", name, N); cgiFormStringNoNewlines("password", pw, N);
这两条语句将username和password分别放到char数组name和pw中,接下来确定是要到数据库里面去查询了。这里的数据库用的是sqlite,很是轻量级的一个数据库。
//login.c if(sqlite3_open("/user.db", &db) != SQLITE_OK) { fprintf(cgiOut, "<BODY>"); fprintf(cgiOut, "<H1>%s</H1>", "Server is busy..."); fprintf(cgiOut, "<meta http-equiv=\"refresh\" content=\"1;url=../index.html\">"); return -1; } sprintf(sql, "select * from usr where name='%s' and password='%s'", name, pw); if(sqlite3_get_table(db, sql, &result, &row, &column, NULL) != SQLITE_OK) { fprintf(cgiOut, "<BODY>"); fprintf(cgiOut, "<H1>%s</H1>", "Server is busy..."); fprintf(cgiOut, "<meta http-equiv=\"refresh\" content=\"1;url=../index.html\">"); sqlite3_close(db); return -1; }
很是容易理解,sqlite3_open()
函数用来打开数据user.db,若是打开失败就会看到网页上有Server is busy...字样。
char数组sql中存放了一条select语句,sqlite3_get_table()
函数就是用来查询的,若是数据库出现问题,依然在网页上显示有Server is busy...
若是在数据库里找不到对应的用户,变量row的值就是0,网页上显示Name or password error。
//login.c if(row == 0) { fprintf(cgiOut, "<BODY>"); fprintf(cgiOut, "<H1>%s</H1>", "Name or password error"); fprintf(cgiOut, "<meta http-equiv=\"refresh\" content=\"1;url=../index.html\">"); sqlite3_close(db); return 0; }
查看user.db数据库,只有一条记录
代码里的cgiOut是什么呢?,咱们在cgic.h头文件的实现文件cgic.h中找到了答案。
//cgic.c cgiIn = stdin; cgiOut = stdout;
cgiOut就是标准输出。登陆成功后,会跳转到主页面main.html。
②环境信息获取——env_1_a9_info.cgi
main.html页面是一个frameset框架,由left.html,top.html,right.html三个页面构成。其中只有left.html导航栏具备实际的功能选项。
咱们先看看环境信息env1.html页面中的cgi程序
<iframe frameborder="0" border=0 scrolling="no" src="cgi-bin/env_1_a9_info.cgi" width="100%" height="100%"></iframe>
这个iframe告诉咱们env_1_a9_info.cgi程序会处理当前的实时数据,并返回结果。分析一下env_1_a9_info.cgi的源码,来看看这些数据是怎么获得的。
get_env()函数是用来获取环境信息的。
//env_1_a9_info.c void get_env() { sqlite3 *db; char sql1[N] = {0}, sql2[N] = {0}; char **result1, **result2; int row1, colunm1, row2, colunm2; if (sqlite3_open("/smartfarm.db", &db) != SQLITE_OK) { fprintf(cgiOut, "<H2>sqlit smartfarm.db open err</H2>"); errflag = 1; return ; } sprintf(sql1, "select * from env where farm_no=1"); if (sqlite3_get_table(db, sql1, &result1, &row1, &colunm1, NULL) != SQLITE_OK) { fprintf(cgiOut, "<H2>sqlite3_get_table err</H2>"); errflag = 1; return ; } strncpy(temp_max, result1[colunm1 + 1], 9); strncpy(temp_min, result1[colunm1 + 2], 9); strncpy(hum_max, result1[colunm1 + 3], 9); strncpy(hum_min, result1[colunm1 + 4], 9); strncpy(light_max, result1[colunm1 +5], 9); strncpy(light_min, result1[colunm1 + 6], 9); ....... sqlite3_close(db); }
不难发现,这些数据都是从数据库smartfarm.db中获取的。
最高/最低气温,湿度,光照,都在env表中。
还有一张collect_env表,显示的是实时信息,可是好像没有用到。
//env_1_a9_info.c void get_env() { ........ sprintf(sql2, "select * from collect_env where farm_no=-1"); if (sqlite3_get_table(db, sql2, &result2, &row2, &colunm2, NULL) != SQLITE_OK) { errflag = 1; return ; } #if 0 strncpy(temp, result2[colunm2 + 1], 9); strncpy(hum, result2[colunm2 + 2], 9); strncpy(light, result2[colunm2 + 3], 9); #endif }
很明显,temp,hum,light分别存放当前的温度、湿度和光照,可是被注释掉了。
④实时监控的图像拍照处理——take_photo.cgi
实现监控的CGI程序是take_photo.cgi,咱们仍是直接分析它的源代码。.
抛开cgi程序的fprintf不看,咱们直接看核心处理的部分。
//take_photo.c key_t key; int msgid; char buf[2] = {0}; if((key = ftok("/lib", 'a')) < 0) { perror("ftok"); exit(1); } if ((msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666)) < 0) { if (errno == EEXIST) { msgid = msgget(key, 0666); }else{ perror("msgget msgid"); return 0; } } struct message msgbuf; msgbuf.type = 1L; msgbuf.msg_type = 2L; cgiFormString("mode", buf, 2); if(buf[0] <= '0' || buf[0] > '9') goto err; msgbuf.text.camera = buf[0] - '0'; msgsnd (msgid, &msgbuf, sizeof (msgbuf) - sizeof (long), 0);
这是一段linux的进程间通讯,咱们须要关注如下三个系统调用:
- key_t ftok(const char *pathname, int proj_id)
- int msgget(key_t key, int msgflg)
- int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg)
这些调用的细节不去管它,只要知道该进程须要收发其余进程的消息就能够了。
咱们关注这段代码,看看进程发送的消息msgbuf是什么。
//take_photo.c struct message msgbuf; msgbuf.type = 1L; msgbuf.msg_type = 2L; cgiFormString("mode", buf, 2); if(buf[0] <= '0' || buf[0] > '9') goto err; msgbuf.text.camera = buf[0] - '0';
注意到这里的cgiFormStirng函数,咱们的buf存的是表单中收集到的数字,下面是表单的内容。
<form id="take_photo" name="take_photo" method="post" action="cgi-bin/take_photo.cgi"> <td width="15%"><input type="radio" name="mode" id="mode_1" value="1" />1</td> <td width="15%"><input type="radio" name="mode" id="mode_2" value="3" />3</td> <td width="15%"><input type="radio" name="mode" id="mode_3" value="5" />5</td> <td width="15%"><input type="radio" name="mode" id="mode_4" value="7" />7</td> <td width="15%"><input type="radio" name="mode" id="mode_5" value="9" />9</td> <td width="25%"><input type="submit" name="take_photo" id="take_photo" value="图像抓拍" /></td> </form>
结构体message在struct.h头文件中有定义。
//struct.h union text { unsigned char led; unsigned char buzzer; unsigned char camera; unsigned char fan; unsigned char relay; unsigned char uart; char phone[24]; struct control_parameter parameter; }; struct message { long type; long msg_type; union text text; };
那么谁会接收msgbuf这个消息呢?咱们能够在项目源码中找到答案。
看一下,项目源码里面pthread_client_request.c中的一个代码片断。
//pthread_client_request.c switch (msgbuf.msg_type) { ........ case CAMERA: printf("received camera request\n"); pthread_mutex_lock (&mutex_camera); dev_camera_mask = msgbuf.text.camera; pthread_cond_signal (&cond_camera); pthread_mutex_unlock (&mutex_camera); break; ........ default: #if DEBUG printf("pthread client request default break\n"); #endif break; }
也就是说会 有专门的线程用来处理摄像头有关的消息。
咱们还记得以前take_photo.c有这样一段代码。
struct message msgbuf; msgbuf.type = 1L; msgbuf.msg_type = 2L;
1L、2L这样的魔数是什么意思呢
答案在项目源码的global_variable.h头文件中。
//global_variable.h /*message queue type */ #define REQUEST 1L /*message queue msg_type*/ #define BUZZER 1L #define CAMERA 2L #define FAN 3L #define LED 4L #define RELAY 5L #define SET_PARAMETER 6L #define SMS 7L //#define SQLITE 7L #define UART 8L #define MODIFY_PHONE_NUM 9L #define BEEP 10L
linux下一切都是文件,打开摄像头也不例外。
//pthread_camera.c if ((dev_camera_fd=open (DEV_CAMERA, O_RDWR | O_NOCTTY | O_NDELAY)) < 0) { printf ("Cann't open file /tmp/webcam\n"); exit (-1); }
其中DEV_CAMERA这个常量的定义在头文件global_variable.h中也能找到。
//global_variable.h #define DEV_GPRS "/dev/ttyUSB1" #define DEV_GPRS_2 "/dev/ttyUSB2" #define DEV_ZIGBEE "/dev/ttyUSB0" #define DEV_LED "/dev/led" #define DEV_BUZZER "/dev/buzzer" #define DEV_CAMERA "/tmp/webcam" #define SQLITE_OPEN "./smartfarm.db"
这个常量就是/tmp/webcam,就是摄像头设备。
④照片展现——show_photo.cgi
在“历史照片”这一功能中,调用的CGI程序是show_photo.cgi
咱们一样仍是忽略掉CGI源码中的fprintf部分。
#define DIRNAME "../pice" #define PHOTO_NUM_MAX 100 // 最多100张,0-99 int cgiMain() { ......... if((dir = opendir(DIRNAME)) == NULL) //打开图片存放的目录 { perror("fail to opendir"); exit(1); } while((dirp = readdir(dir)) != NULL) //读文件夹里文件的名字 { if(dirp->d_name[0] > '9' || dirp->d_name[0] < '0') //名字的第一个字母 continue; sprintf(photoname[syncmsg1.photo_num++], "%s", dirp->d_name); if(syncmsg1.photo_num >= PHOTO_NUM_MAX) //判断是否超过最大值 { syncmsg1.photo_num = PHOTO_NUM_MAX - 1; break; } } ......... }
咱们从中能够知道,历史照片都是保存在/www/pice目录下的,并且最多只保存100张。
本来这个系统是经过WIFI来访问并进行控制的,可是咱们的WIFI模块出现了问题,如今只能用网线直连的方式控制系统。
咱们将手机热点配置为my_accent,密码设为012345678,在rootfs/etc中添加配置文件wpa-spk-tkip.conf。
# WPA-PSK/TKIP ctrl_interface=/var/run/wpa_supplicant network={ ssid="my_accent" key_mgmt=WPA-PSK proto=WPA pairwise=CCMP group=CCMP psk="012345678" }
更新文件系统之后从新烧写镜像,配置IP和网关后,执行命令wpa_supplicant -B -i wlan0 -c /etc/wpa-psk-tkip.conf
咱们能够看到,wlan网卡依然没有链接到手机热点上。咱们暂时没有解决这个问题。
咱们不管直接使用含有项目内容的文件系统,仍是本身从新编译mjpeg-streamer都出现了一样的问题。
咱们启动开发板后,对mjpeg-streamer进行测试。
运行mjpg_streamer -i "/mjpg/input_uvc.so -y -d /dev/video0" -o "/mjpg/output_http.so -w 192.168.9.111:8080"
系统显示因为设备被占用了,初始化失败。咱们没有找到解决的方法。
这些问题目前依然没有找到解决方案。
本项目特点在于与物联网内容相关联,拓宽了学生的眼界,让咱们更好的学习Linux指令、物联网相关知识、嵌入式系统调试等不少本不属于咱们专业的知识,对于三个代码和嵌入式基础薄弱的理科专业学生来讲是一个大量接触相关知识的好机会。课题特点的地方在于充分结合嵌入式系统调试,可让咱们给硬件嵌入Linux内核以方便咱们进行硬件控制。