项目一. 移动物体监控系统
Sprint0-产品设计与规划
第1课-产品功能展示
我们在学校的时候,做项目开发,可能就是想到了哪里就做哪里。但是在实际公司的开发过程中,我们是要严格的按照公司的流程来进行的。
项目开发分成了准备阶段和开发阶段:
我们的最后效果就是,利用摄像头和音响完成连接,如图:
当有移动物体在摄像头面前移动时,摄像头能采集图像和视频,并且发出报警的声音。通过访问对应的局域网,我们可以通过网页访问,如下:
第2课-产品功能模型设计
第3课-Product Backlog规划
我们登录网址https://www.leangoo.com/kanban/board_list,进行相应的注册。
创建新的product-backlog,添加我们需要的功能,完成后如下:
Sprint1-声音报警子系统开发
第1节- Sprint Backlog规划
product-backlog是关于我们产品的一个功能的需求列表,这是一个大的需求,并不够细化,于是我们还要进行sprint-backlog的规划。
我们在网站https://www.leangoo.com/中,创建新的sprint-backlog。接下来我们分析我们应该做的事情,首先我们需要声卡的使能,接下来是播放器的移植。
我们创建如下所示的sprint-backlog,对项目进行相应的时间规划,工作量规划。随着我们工作量的完成,我们可以将待办中的标签进行拖拽,我们通过燃尽图可以看到我们项目的完成情况以及进度。
第2节-声卡驱动开发
1. 声卡驱动架构
(1)OSS架构
OSS全称是Open Sound System,叫做开放式音频系统,这种早期的音频系统这种基于文件系统的访问方式,这意味着对声音的操作完全可以像对普通文件那样执行open,read等操作。OSS中,主要提供了以下几种音频设备的抽象设备文件:
/dev/mixer:用来访问声卡中的混音器用于调整音量大小和选择音源
/dev/dsp、/dev/audio:读这个设备就相当于录音,写这个设备就相当于放音。
(2)ALSA架构
由于OSS设计上的缺陷,导致其对混音的支持不好,再加上2002年以后,OSS成为商业不开源软件,这就催生了Linux下另一种音频系统ALSA的出现,ALSA全称是Advanced Linux Sound Architecture,叫做Linux系统高级音频架构,它主要为声卡提供的驱动组件,以替代原先的OSS。ALSA架构借助于如下设备文件工作:
/dev/pcmC0D0c:用于录音的pcm设备
/dev/pcmC0D0p:用于播放的pcm设备
/dev/timer:定时器
/dev/controlC0:用于声卡的控制,如通道选择
/dev/mixer:混音处理
设备文件不创建,设备是工作不了的。
(3)ALSA-OSS封装
好多优秀的播放器用的是OSS接口,为了在ALSA下支持OSS架构,提供一个ALSA-OSS封装接口,这样使得原来的程序可以使用。
2. 声卡驱动的集成
为了ALSA-OSS封装,我们通过make menuconfig ARCH=arm进入内核选项,我们要在内核选项中,把OSS Mixer API和OSS PCM (digital audio) API选上。
为了查看驱动是否装好,我们只需查看我们对应的文件夹是否直接存在就好。但是一些芯片的内核是做了修改的,就像是210开发板。它的驱动是以.ko文件的形式存在的。
我们在210的光盘文件中,找到wm8960.ko文件和s5pv210_hdmi.ko文件,按着个顺序(必须是这个顺序),用insmod命令安装,这样声卡驱动就安装完成。这样我们再找对应的文件,就可以直接找到了。
第3节-嵌入式播放器移植
我们在网页中搜索音频播放器,选择了madplay播放器,接着查找该播放器移植的方法,找到对应的方法网页,不要管它是往什么系统上的移植,大致的移植步骤都是一样的。
我们对具体的步骤不做赘述,网上都能找的到,例如:
https://blog.csdn.net/simanstar/article/details/24035379
1. 我们来分析下每个步骤的意义。
(1)./configure --prefix=/opt/madplay/target-arm:我们一般移植的步骤第一步都是配置,切记后面一个配置项是双杠(--)。它的目的就是生成对应的Makefile。prefix指定的就是后面make install的安装路径。一般在执行这个步骤之前我们先通过命令mkdir _install创建一个名字为_install的文件,为了文件打包用。
(2)make:第二步是编译。一般在进行编译之前,是要进入Makefile文件中更改一些信息的,目的是对应我们将使用的环境。例如,将cc=gcc改成cc=arm-linux-gcc等。
(3)make _insatll:安装。将文件安装在_install文件中,之后我们就能在其中找到我们需要的文件或者库。
注意:
(1)以库的移植为例子,按照正常的步骤来走,我们生成的是静态库(.a文件),但是为了引用,我们实际要用的是动态库(.so文件)。为了解决这个问题,我们在配置的时候,加上--shared,例如:./configure --shared --prefix=/opt/madplay/target-arm,这样我们就能生成想要的动态库了。
(2)安装播放器的时候是需要率先安装一些库文件的,有时候我们按说明将需要的库都安装完成,发现还是缺库文件。不要着急,记住缺少的库文件,再上网搜索,移植就好。
2. 区分各种前缀的意义
--host:表示将使用的工具链,例如:--host=arm-linux
--prefix:指定安装目录,例如:--prefix=/opt/madplay/target-arm
--disable:不使用的选项,例如:--disable-debugging
--enable:使用的选线,例如:--enable-static
3. 常见问题
(1)我们当编译一些文件的时候,我们会遇到编译器无法编译一些选项。这是我们想到的最简单的方法,就是打开Makefile文件,将其中的对应选项删除。
(2)我们将编译好的madplay放在/sbin目录下,我们也能看到它的存在,但是运行的时候就是找不到。这个问题的成因就是因为,我们的madplay是动态编译的,它需要很多动态链接库,没有动态链接库的话,就会找不到它,我们根据命令:
arm-linux-readlf -d _install/bin/madplay
就可以查看我们的动态文件所需要的动态链接库了。我们将所需要的动态库拷贝到开发板的/lib目录下即可。
(3)madplay的使用方式,例如;madplay /user/1.mp3
Sprint2-摄像头子系统开发
第1节-摄像头驱动开发
1. 摄像头软件系统架构
我们可以看到架构主要分为四个部分:
(1)用户空间的应用程序:比如后面要用到的 Motion 以及 ffmpeg 等开源应用程序
(2) V4L2 核心组件(驱动核心):这是 Linux 内核专门用来管理视频子系统的核心组件,也正是由于这个组建的存在, 才让我们可以在用户空间使用统一的一套编程接口 API 去控制底层的不同硬件。
(3)具体的 V4L2 驱动:这部分主要是针对不同的摄像头有不同的驱动程序,这写驱动程序相互独立,但是共同注册到 V4L2 子系统下,以便和用户空间应用程序进行通信。
(4)底层硬件:就是实际的摄像头等物理硬件。
V4L2 的主要特性有:
① 视频采集接口(video capture interface):这种应用的设备可以是高频头或者摄像头.V4L2 的最初设计就是应用于这种功能的。
② 视频输出接口(video output interface):可以驱动计算机的外围视频图像设备--像可以输出电视信号格式的设备。
③ 直接传输视频接口(video overlay interface):它的主要工作是把从视频采集设备采集过来的信号直接输出到输出设备之上,而不用经过系统的 CPU。
④ 视频间隔消隐信号接口(VBI interface):它可以使应用可以访问传输消隐期的视频信号。
⑤ 收音机接口(radio interface):可用来处理从 AM 或 FM 高频头设备接收来的音频流。
v4l是video for linux的缩写,就是一套机制,目的是将即使有不同的应用程序,但是我们的驱动只用一个就好。这也是一种接口机制,为特定的功能提供特定的接口,接口统一。
在Linux中,视频设备是设备文件,可以像访问普通文件一样对其进行读写,摄像头在/dev/video0下。
2. 摄像头驱动使能
由于本项目采用的是基于 ZC3XX 系列芯片的 USB 摄像头,所以需要在内核里面添加该USB 摄像头驱动。以后如果换成别的摄像头,根据自己摄像头的驱动芯片加载合适的驱动模块到内核即可, 毕竟现在 Linux 支持了市面上大部分的摄像头, 一般能购买到的摄像头都有相应的驱动程序,只需要自己在内核使能即可!按照如下步骤可以使能 USB 摄像头。
(1)进入配置菜单
在内核源代码顶层目录执行 make menuconfig 命令。进入配置菜单。
(2)依次选择如下配置选项:
Device Drivers --->(毫无疑问,只要是添加驱动都是要进入这个选项)
Multimedia devices --->(显然视频摄像头属于多媒体范畴)
[*] Video capture adapters --->(视频捕获也即是摄像头)
[*] V4L USB devices --->(我们采用的是 USB 摄像头)
<*> GSPCA based webcams --->(万能摄像头驱动)
<*> ZC3XX USB Camera Driver(我们采用的具体 USB 摄像头对应的驱动程序)
(3)保存配置文件,退出配置模式,开始编译新内核
使用 make uImage ARCH=arm CROSS_COMPILE=arm-linux-开始编译配置好的内核,将新内核下载到开发板并启动,挂载 NFS 文件系统进入下一阶段的测试。 为了测试是否真的使能了,我们编写下面的程序:
camera.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#include <linux/videodev2.h>
struct buffer {
void * start;
size_t length;
};
struct buffer *buffers;
unsigned long n_buffers;
unsigned long file_length;
int file_fd;
char *dev_name = "/dev/video3";
int fd;
static int read_frame (void)
{
struct v4l2_buffer buf;
/*帧出列*/
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl (fd, VIDIOC_DQBUF, &buf);
write(file_fd,buffers[buf.index].start,buffers[buf.index].length);
/*buf入列*/
ioctl(fd, VIDIOC_QBUF, &buf);
return 1;
}
int main (int argc,char ** argv)
{
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
unsigned int i;
enum v4l2_buf_type type;
file_fd = open("test.jpg", O_RDWR | O_CREAT, 0777);
fd = open (dev_name, O_RDWR | O_NONBLOCK, 0);
/*获取驱动信息*/
ioctl (fd, VIDIOC_QUERYCAP, &cap);
printf("Driver Name:%s\n Card Name:%s\n Bus info:%s\n\n",cap.driver,cap.card,cap.bus_info);
/*设置图像格式*/
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 320;
fmt.fmt.pix.height = 240;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
ioctl (fd, VIDIOC_S_FMT, &fmt) ;
/*申请图像缓冲区*/
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl (fd, VIDIOC_REQBUFS, &req);
buffers = calloc (req.count, sizeof (*buffers));
for (n_buffers = 0; n_buffers < req.count; ++n_buffers)
{
/*获取图像缓冲区的信息*/
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
ioctl (fd, VIDIOC_QUERYBUF, &buf);
buffers[n_buffers].length = buf.length;
// 把内核空间中的图像缓冲区映射到用户空间
buffers[n_buffers].start = mmap (NULL , //通过mmap建立映射关系
buf.length,
PROT_READ | PROT_WRITE ,
MAP_SHARED ,
fd,
buf.m.offset);
}
/*图像缓冲入队*/
for (i = 0; i < n_buffers; ++i)
{
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl (fd, VIDIOC_QBUF, &buf);
}
//开始捕捉图像数据
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl (fd, VIDIOC_STREAMON, &type);
fd_set fds;
FD_ZERO (&fds);
FD_SET (fd, &fds);
select(fd + 1, &fds, NULL, NULL, NULL);
/*读取一幅图像*/
read_frame();
for (i = 0; i < n_buffers; ++i)
munmap (buffers[i].start, buffers[i].length);
close (fd);
close (file_fd);
printf("Camera Done.\n");
return 0;
}
在linux环境下,输入arm-linux-gcc camera.c -o camera,生成camera运行文件。在编译之前要记得修改文件里面的设备文件,也就是/dev/video0。记得在设备文件中修改open函数,如果传递给你open函数的参数不是你摄像头的实际设备文件,那么打开会有无法预知的结果,有可能失败,也有可能打开某个文件导致抓取的图片是空的等情况。
在开发板中运行./camera即可查看,是否采集到了一幅图片,正常的情况下会在camera文件所在的文件夹中找到test.jpg图片。
但是当我们在210开发板中运行该程序的时候,我们会发现报错,经查找会看到s5p-ehci: fatal error这段文字,这句话表示的是驱动出现了问题,ehci表示的是主控制器。
这种问题一般的修改是非常困难的,为了方便我们尝试下面一种方法:使用OHCI
我们进入内核编译,Device Drivers--USB pupport,我们可以看到EHCI HCD (USB 2.0) support是被选中的,因为这段代码是有问题的,我们把它不选,选择下面的OHCI HCD support。重新进行内核编译,完成新的内核文件的烧写。
第2节-V4L2图像编程接口深度学习
可以打开配套资源的 camera.c 进行 V4L2 的分析,我们可以得出如下的操作流程:
1. 打开摄像头设备文件。
例如:fd = open (dev_name, O_RDWR | O_NONBLOCK, 0);
2. 获取驱动信息-VIDIOC_QUERYCAP。
例如:ioctl (fd, VIDIOC_QUERYCAP, &cap);
在此之后可以获取更多的信息,不仅仅是本程序中后面的获取设置图像格式,我们还可以获取摄像头所支持的图像格式等信息。
3. 设置图像格式-VIDIOC_S_FMT;
例如:ioctl (fd, VIDIOC_S_FMT, &fmt) ;
4. 申请帧缓冲区-VIDIOC_REQBUFS;
在 Linux 里面, 必须要有帧缓冲区才可以进行图像的捕获,有了帧缓冲区以后,可以将帧缓冲区设置为输入队列,经过驱动程序以后,就将图像信息写到帧缓冲区, 写入图像数据以后的帧缓冲区会加入输出队列, 用户空间的应用程序。最重要取出输出队列的帧缓冲区, 然后读里面的内容, 最后将被读取数据以后的帧缓冲区再次放回到输入队列,如此循环完成视频监控功能。但是要注意的是,这里申请的帧缓冲区是内核空间的,所以应用程序不能直接访问,需要通过映射等操作。而且输入队列和输出队列都是帧缓冲构成的。
5. 获取帧缓冲的地址以及长度信息-VIDIOC_QUERYBUF;
6. 使用 mmap 将内核空间的帧缓冲映射到用户空间;
7. 帧缓冲入队列-VIDIOC_QBUF;
8. 开始采集图像-VIDIOC_STREAMON;
在此之后需要使用 select 函数等待输出缓冲有数据可读再进行下一步操作。
9. 取出帧缓冲(出队)-VIDIOC_DQBUF;
10. 访问帧缓冲读取数据-使用 write 等函数将数据写入到目标文件。
11. 帧缓冲重新入队列-VIDIOC_QBUF;已经读取过数据的帧缓冲要放入输入队列。
12. 关闭相关设备和文件进行以及解除映射等扫尾工作。
可以参考以下资料进行更多学习:
http://www.cnblogs.com/emouse/archive/2013/03/04/2943243.html
http://blog.csdn.net/eastmoon502136/article/details/8190262
编写的驱动程序程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <getopt.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <malloc.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <asm/types.h>
#include <linux/videodev2.h>
struct buffer {
void * start;
size_t length;
};
struct buffer *buffers;
unsigned long n_buffers;
unsigned long file_length;
int file_fd;
char *dev_name = "/dev/video3";
int fd;
static int read_frame (void)
{
struct v4l2_buffer buf;
/*帧出列*/
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl (fd, VIDIOC_DQBUF, &buf);
write(file_fd,buffers[buf.index].start,buffers[buf.index].length);
/*buf入列*/
ioctl(fd, VIDIOC_QBUF, &buf);
return 1;
}
int main (int argc,char ** argv)
{
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_requestbuffers req;
struct v4l2_buffer buf;
unsigned int i;
enum v4l2_buf_type type;
file_fd = open("test.jpg", O_RDWR | O_CREAT, 0777);
/*1.打开文件*/
fd = open (dev_name, O_RDWR | O_NONBLOCK, 0);
/*2.获取驱动信息*/
ioctl (fd, VIDIOC_QUERYCAP, &cap);
printf("Driver Name:%s\n Card Name:%s\n Bus info:%s\n\n",cap.driver,cap.card,cap.bus_info);
/*3.设置图像格式*/
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 320;
fmt.fmt.pix.height = 240;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
ioctl (fd, VIDIOC_S_FMT, &fmt) ;
/*4.申请图像缓冲区*/
req.count = 4; //四个帧缓冲
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl (fd, VIDIOC_REQBUFS, &req);
buffers = calloc (req.count, sizeof (*buffers));
for (n_buffers = 0; n_buffers < req.count; ++n_buffers)
{
/*5.获取图像缓冲区的信息*/
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
ioctl (fd, VIDIOC_QUERYBUF, &buf);
buffers[n_buffers].length = buf.length;
// 6.把内核空间中的图像缓冲区映射到用户空间
buffers[n_buffers].start = mmap (NULL , //通过mmap建立映射关系
buf.length,
PROT_READ | PROT_WRITE ,
MAP_SHARED ,
fd,
buf.m.offset);
}
/*7.图像缓冲入队*/
for (i = 0; i < n_buffers; ++i)
{
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl (fd, VIDIOC_QBUF, &buf);
}
//8.开始捕捉图像数据
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl (fd, VIDIOC_STREAMON, &type);
fd_set fds;
FD_ZERO (&fds);
FD_SET (fd, &fds);
select(fd + 1, &fds, NULL, NULL, NULL);
/*.读取一幅图像*/
read_frame();
for (i = 0; i < n_buffers; ++i)
munmap (buffers[i].start, buffers[i].length);
close (fd);
close (file_fd);
printf("Camera Done.\n");
return 0;
}
Sprint3-移动监控主系统设计与开发
所谓的移动就是前后两个场景不一样,我们抓取两幅图像看看是否一样,若一样就是没有移动,否则就是移动了。在我们工作之前,首先想到这个问题不是我们首先去做的,肯定已经有人去做过了,我们要做的不过是移植
一. Ffmpeg 的移植
ffmpeg 是一个开源免费跨平台的视频和音频流方案,它提供了录制、转换以及流化音
视频的完整解决方案。 移植 ffmpeg 的目的主要是为了使 motion 能支持将捕获到的视频流转换成 avi 格式的视频文件进行存储。因此,我们仅需要移植 ffmpeg 中如下两个库,而且要在移植 Motion 之前进行 ffmpeg 的移植:
libavformat:用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能。
libavcodec:用于各种类型声音/图像编解码
libavutil:包含一些公共的工具函数。
1. 将 resources/Sprint3/目录中的源码包解压:
tar xzf ffmpeg-0.5.1.tar.gz
2. 编译、安装 ffmpeg:
a. 进入ffmpeg-0.5.1源码目录。 使用命令: ./configure --enable-memalign-hack
--disable-debug
--prefix=/NFS/_install
--arch=arm
--cross-prefix=arm-linux- --enable-shared,生成 Makefile。其中--prefix
指明生成的库和执行程序的安装目录, 这里将该路径设置为 ffmpeg 源码目录下的_install 目录。
b. 使用命令:make,编译源码,使用命令:make install,将生成库和头文件安装到指定的_install 目录下。
c. 拷贝/NFS/_install/lib/下共享库到开发板根文件系统的/lib 目录下。例如,开发板根文件系统位于/nfs/rootfs/目录下,则可使用如下命令进行拷贝:
cp /NFS/_install/lib/libavcodec.so* /NFS/rootfs/lib/
cp /NFS/_install/lib/libavformat.so* /NFS/rootfs/lib/
cp /NFS/_install/lib/libavutil.so* /NFS/rootfs/lib/
二. Motion 主程序移植
motion 是一个开源的用于移动图像监控程序。下面是 motion 的移植安装和配置方法:
1. 将 resources/Sprint3 目录中的源码包解压:
tar xzf motion-3.2.12.tar.gz
2. 编译、安装 motion:
a.进入 motion-3.2.12 源码目录。使用命令:
./configure
--prefix=${PWD}/_install
--host=arm-linux
--without-mysql
--without-pgsql\
--with-ffmpeg=/NFS/_install,
生成 Makefile。红色部分是 ffmpeg 的安装目录,自己根据情况修改。一定要加上否则没有录视频的功能!这里的安装目录在 motion 源代码目录的_install 目录。 如果在 motion 加了 ffmpeg 是这样的配置结果 。
b. 使用命令:make,编译源码,使用命令:make install,将生成库和头文件安装到指定的./_install 目录下。
c.拷贝./_install/bin/下的可执行程序 motion 到开发板根文件系统的/usr/sbin 目录下。 例
如,开发板根文件系统位于/NFS/rootfs/目录下,则可使用如下命令进行拷贝:
cp ./_install/bin/motion /NFS/rootfs/usr/sbin/。
d.复制 glibc 库关于 jpeg 的动态库到开发板的 lib 目录
cp /opt/EmbedSky/4.3.3/arm-none-linux-gnueabi/libc/lib/libjpeg.* /NFS/rootfs/lib/ -rf
这一步可以根据自己开发板上是不是已经有了 libjpeg 的动态库确定是否需要做, 如果有了就可省略。
e. 在 开 发 板 根 文 件 系 统 目 录 中 创 建 /var/run/motion/ 目 录 ,
因 为 默 认/var/run/motion/motion.pid 文件将用来存放运行中的 motion 进程。 当然也要创建 motion.pid文件。
3. 配置 motion
编译 motion 的时候,在安装目录(_install)下有 etc/motion-dist.conf 文件,这个
文件是 motion 运行的时候所依赖的配置文件,我们利用该文件作为基础,针对我们的开发
板做一定的修改,这里我们列出了主要关心和修改的配置参数
参数名 |
设置值 |
说明 |
videodevice |
/dev/video0 |
摄像头设备名称。根据自己的节点号修改 |
v4l2_palette |
2 |
设备调色板格式。’2’表示 MJPEG。 |
width |
640 |
图像宽度。 |
height |
480 |
图像高度。 |
framerate |
15 |
每秒最大捕获帧数。考虑到嵌入式设备存储空间有限,这里限制为 15。这样可以适当减少捕获的图像信息。 |
threshold |
1500 |
捕获门限值。当系统检测到像素变化超过该值时触发 motion 事件。 |
target_dir |
/mnt/sd |
捕获的图像信息存储路径。 |
webcam_maxrate |
3 |
网页监控视频流的最大帧率。由于在局域网中应用,因此为获得更好的实时监控质量,提高了该值。 |
webcam_localhost |
off |
限制仅本地可连接网络摄像机。 取消该限制。 |
control_localhost |
off |
限制仅本地可通过网页进行控制。取消该限制。 |
on_motion_detected |
madplay alarm.mp3 |
当该事件产生时执行该命令
|
除此之外, 为了能通过 motion 使用 ffmpeg 录制视频的功能, 还需要确保该文件的以下配置参数如下设置:
ffmpeg_cap_new on
ffmpeg_cap_motion off
ffmpeg_video_codec mpeg4
如果想要在物体移动的时候将移动范围框出来,就要设置
locate on
将该文件复制到开发板文件系统的 etc 目录下,并重命名为 motion.conf,之后按照上述给出的参考值修改相关的配置参数,保存以后,使用如下命令开启 motion:
motion -c /etc/motion.conf
成功启动以后会有如下提示:
[0] Processing thread 0 - config file /etc/motion.conf
[0] Unknown config option "sql_log_image"
[0] Unknown config option "sql_log_snapshot"
[0] Unknown config option "sql_log_mpeg"
[0] Unknown config option "sql_log_timelapse"
[0] Unknown config option "sql_query"
[0] Motion 3.2.12 Started
[0] Motion going to daemon mode
关于 sql 由于我们暂时没有用到, 所以有关 sql 的错误和警告先不予理会。 通过 samba 观察开发板根文件系统的 mnt/sd 目录发现有如下文件,而且当有物体在摄像头前移动会报警。
由于我做实验的环境比较昏暗,所以图片不清晰,但是可看出来已经将移动物体表示出来了, 表明我们的 motion 移植成功了, 而且存在 avi 视频文件,也说明我们的 ffmpeg 也移植成功!至此该 sprint 任务完成!
注意:
在我进行库移植的过程中,发现了电脑里依旧缺少一个应该安装的动态库,于是上网查找了解决方案,根据网上的步骤,再一次移植了一个动态库,整个系统才运行顺畅。
Sprint4-嵌入式web服务器开发
第1课-BOA嵌入式服务器移植
1. BOA 简介
Boa是一种非常小巧的Web服务器,其可执行代码只有大约 60KB 左右。作为一种单任务Web服务器,Boa只能依次完成用户的请求,而不会fork出新的进程来处理并发连接请求。但Boa支持CGI,能够为CGI程序fork出一个进程来执行,Boa的设计目标是速度和安全。官方网址:http://www.boa.org/,是目前嵌入式领域比较流行的 web 服务器,智能家居的网关大部分也采用该 web 服务器作为主要架构。
2. 编译 BOA
(1)获取 boa 源代码
在这里,使用我们提供好的源代码解压就可以;
(2)生成 Makefile
进入 boa 源代码的 src 目录, 执行./configure 命令即可生成编译 boa 所必须的配置文件以及一些源代码和 Makefile 文件;
(3)修改 Makefile
找到CC=gcc,将其改成CC = arm-linux-gcc, 再找到CPP = gcc –E,将其改成 CPP =
arm-linux-gcc –E,并保存退出。
(4)修改源代码
Boa本身代码是存在些许 bug 的,我们要对这些 bug 进行修复。之所以这里能直接给出代码修改的步骤,是由于我们之前做了大量的实验,已经可以断定哪里的代码出了问题,这样避免大家少走弯路。
a. 将 util.c 的第 100 行注释掉,改为
//time_offset = TIMEZONE_OFFSET(t);
time_offset = 0;
保留以前的代码,将其注释就可以,以便以后排查原来代码内容。不修改这里会出现以
下错误:
util.c:100:1: error: pasting "t" and "->" does not give a valid preprocessing
token
b. 将 boa.c 中 210 行到 229 行代码注视掉; 否则 boa 运行的时候会出现 DIE 的现象,
打印各种找不到密码的信息而停止进程。注释如下:
#if 0
if (passwdbuf == NULL) {
DIE("getpwuid");
}
… …
/* test for failed-but-return-was-successful setuid
* http://www.securityportal.com/list-archive/bugtraq/2000/Jun/0101.html
*/
if (setuid(0) != -1) {
DIE("icky Linux kernel bug!");
}
#endif
编译完以后将可执行文件 boa 复制到开发板的根文件系统的/bin 目录下即可。
3. 配置 BOA
虽然前面已经编译好了可在 ARM 平台运行的 boa 服务器程序, 但是 boa 要真正能工作的话,还必须要有合适的配置文件才可以。而在 boa 源代码的顶层目录已经有了一个默认的boa.conf 配置文件,但是我们需要针对自己的情况对一些配置选项进行修改,可以用
boa.conf 作为基础进行修改, 将他复制到开发板文件系统的/etc/boa 目录 (自己新建) 下。
这里列出了常用的配置选项和含义:
选项名 |
说明 |
Port |
监听的端口号,缺省都是 80,一般无需修改。 |
User |
作为哪个用户组运行,即它拥有该用户组的权限,一般都是 nobody,需要/etc/passwd 中有 nobody 用户。所以这里我们将其注释掉。 |
Group |
作为哪个用户组运行, 即它拥有该用户组的权限, 一般都是 nogroup, 需要在/etc/group 文件中有 nogroup 组。也注释掉。 |
ErrorLog |
错误日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,则用/dev/null。设置为/dev/console 表示错误信息打印到终端。在调试阶段建议改成/dev/console,调试稳定以后改成 null 或者具体的文件。 |
AccessLog |
访问日志文件。如果没有以/开始,则表示从服务器的根路径开始。如果不需要错误日志,则用/dev/null 或直接注释掉。 |
ServerName |
服务器名字。 |
DocumentRoot |
HTML 文档的主目录,非常重要,boa 毕竟是为网页服务的,所以个参数指明了开发板上什么地方有网页文件, 如果不指定正确的 html文件的存放路径,访问会出错。如果没有以/开始,则表示从服务器的根路径开始。 这里拷贝我们提供的 web 目录到开发板的文件系统根目录即可,因此这里设置为/web/。 |
DirectoryIndex |
HTML 目录索引的文件名,也是没有用户只指明访问目录时返回的文件名。 |
KeepAliveMax |
一个连接所允许的 HTTP 持续作用请求最大数目, 注释或设为 0 都将关闭 HTTP 持续作用。 |
KeepAliveTimeout |
HTTP 持续作用中服务器在两次请求之间等待的时间数, 以秒为单位,超时将关闭连接。 |
MimeTypes |
指明 mime.types 文件位置。如果没有以/开始,则表示从服务器的根路径开始。可以注释掉避免使用 mime.types 文件,此时需要用AddType 在本文件里指明。实际上就是一个媒体文件,我们设置为/dev/null。 |
DefaultType |
文件扩展名没有或未知的话,使用的缺省 MIME 类型。 |
CGIPath |
提供 CGI 程序的 PATH 环境变量值。 |
AddType |
将文件扩展名和 MIME 类型关联起来, 和 mime.types 文件作用一样。如果用 mime.types 文件,且其中已经包含了该关联则注释掉。 |
ScriptAlias |
指明 CGI 脚本的虚拟路径对应的实际路径。一般所有 CGI 脚本都要放在实际路径里,用户访问执行时输入站点+虚拟路径+CGI 脚本名。 |
复制我们提供的 web 目录到开发板文件系统根目录,网页信息放在/boa/var/www文件夹内。之后启动 boa 程序,在 PC 浏览器输入你的开发板的 IP 地址,就会显现我们要的效果图。
第2课-CGI快速入门-网页控制LED
1. CGI 简介
CGI 是通用网关接口的简称,实际上是一个应用程序,运行在 WEB 服务器上,并由来自于浏览器端的用户输入而触发,提供与客户端 HTML 页面的接口。它能让网络用户访问和运行远程服务器系统所在主机的应用程序,进一步控制相关的硬件,并把结果格式化输出为HTML 格式,最终反馈到浏览器端。这样就完成了 HTML 静态页面和 WEB 服务器的动态交互。
CGI 和 JAVA Web 不同,利用 JAVA 开发的动态网页或者 web 程序是通过网络将 java 的应用程序传输到客户机,然后客户机再运行相应的 Java 程序,但是 CGI 则是在服务器上运行的, 这一点是他们最大的不同! 所以可以根据自己的需要进行选择使用何种方式开发动态网页,在嵌入式领域,要想实现远程控制某个嵌入式设备,首选 CGI 的方式进行网页的动交互。
他和 web 服务器的关系如下图所示:
他的工作原理是:
(1) 浏览器通过 HTML 表单或超链接请求指向一个 CGI 应用程序的 URL。
(2) 服务器收发到请求。
(3) 服务器执行指定所 CGI 应用程序。
(4) CGI 应用程序执行所需要的操作,通常是基于浏览者输入的内容。
(5) CGI应用程序把结果格式化为网络服务器和浏览器能够理解的文档(通常是HTML网页) 。
(6) 网络服务器把结果返回到浏览器中。
详细的 CGI 资料可以参考百度百科以及 CGI 简明教程。参考链接如下所示:
http://blog.csdn.net/ly61baby/article/details/6458335
2. CGIC 库移植
要想在嵌入式设备上运行CGI程序, 就必须有CGI的库, 而且CGI程序的编译也需要包含CGI特定的头文件, 而相关的头文件和库文件都需要移植。可以直接使用我们提供的源码包进行CGI的移植。具体一点的说,应该是CGIC库,这个主要是针对C语言编写的CGI 程序的库,CGI还支持JAVA以及Python等语言,使用到的库也不一样,这里我们主要讲C 语言使用的 CGIC 库。
CGIC是一个功能比较强大的支持CGI开发的标准C库, 并支持 Linux, Unix 和 Windows
等多操作系统。以下描述 CGIC 的移植过程。
(1)解压 CGIC 库
tar -xzf cgic205.tar.gz
(2)修改 Makefile
进入cgic205源码目录,修改Makefile。找到CC=gcc,将其改成CC=arm-linux-gcc,找到 AR=ar ,将其改成AR=arm-linux-ar,找到RANLIB=ranlib,将其改成RANLIB=arm-linux-ranlib。 找到gcc cgictest.o -o cgictest.cgi ${LIBS},将其改成$(CC) $(CFLAGS) cgictest.o -o cgictest.cgi ${LIBS},找到 gcc capture.o -o capture ${LIBS},将其改成$(CC) $(CFLAGS) capture.o -o capture ${LIBS},并保存退出。
(3)编译和安装
将编译好的 cgic 库文件(libcgic.a)和测试文件 capture 和 cgic.html 以及cgictest.cgi 分别复制到开发板文件系统的/lib 目录和/web/cam 下。
(4) 配置BOA
要支持 CGI 程序被外部浏览器访问, 首先要让 web 服务器支持 CGI 且针对 CGI 程序的路径做好相应的配置。 这里我们要针对 BOA 服务器给出 CGI 的虚拟路径对应的物理路径。 通过修改 boa.conf 文件的 ScriptAlias 参数完成。修改为 ScriptAlias /cam/ /web/cam/
表明浏览器地址栏输入的/cam 对应的实际上是开发板文件系统的/web/cam 目录。
(5)测试 CGI
运行boa程序,在PC的浏览器输入你的开发板Ip地址/cam/cgictest.cgi,回车就会出现一些文字,表明 boa 和 cgic 移植成功。
3. 网页点灯
(1)CGIC 程序代码分析
实现网页控制 LED 的 CGI 程序的源码如下:
/*必要的头文件*/
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<cgic.h>
/*主函数不能是 main 函数*/
intcgiMain()
{
intfd,led_control,led_state;
char*data;
/*输出 HTML 格式的内容以便反馈给浏览器*/
cgiHeaderContentType("text/html");
fprintf(cgiOut,"<HTML><HEAD>\n");
fprintf(cgiOut,"<TITLE>LEDCGI</TITLE></HEAD>\n");
fprintf(cgiOut,"<BODY>");
fprintf(cgiOut,"<H1>ControlLedOk!</H1>");
fprintf(cgiOut,"</BODY>\n");
fprintf(cgiOut,"</HTML>\n");
/*获取环境变量*/
data=getenv("QUERY_STRING");
/*从获取到的环境变量里面按格式提取所需变量的值*/
sscanf(data,"led_control=%d&led_state=%d",&led_control,&led_state);
printf("led_control=%d\n",led_control);
printf("led_control=%d\n",led_state);
/*打开开发板的 LED*/
fd=open("/dev/led",0);
/*根据浏览器的输入控制相应的 eLED*/
ioctl(fd,led_state,led_control);
/*关闭 LED*/
close(fd);
return0;
}
我们知道,CGI 程序在浏览器能被调用,是通过表单来完成的,在一个调用 CGI 程序的HTML 页面里常会有如下的内容:
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<head>
<title>web 控制开发板 led</title>
</head>
<body>
<h1 align="center">WEB 控制 LED</h1>
<form action="/cam/led.cgi" method="get">
<p align="center">
请输入需要控制的 led <input type="text" name="led_control"/>
</p>
<p align="center">
请输入控制 led 的动作 <input type="text" name="led_state"/>
</p>
<p align="center"><input type="submit" value="sure"/>
<input type="reset" value="back"/>
</p>
</form>
</body>
其中<form>和</form>标识着表单的开始和结束。 action="/cam/led.cgi"指明用来处理表单提交的数据的 CGI 程序为/cam/目录下的 led.cgi,而通过前面的 boa 配置我们知/cam/实际上是指/web/cam/这个目录,所以我们的 led.cgi 程序要存放在开发板的/web/cam/目录下。<input>标签标示输入表单中的内容默认为txt属性, 但是指定了type为text以后就是text属性, 而且后面还指定了 type 为 submit 表示提交属性, name="led_control"表示的是输入表单的元素名称为 led_control,同理另一个元素名是 led_state,这就是和 C 语言交互的数据变量。type="submit"标示输入类型为提交。所以当我们按下表单内的按钮,就会将点击结果提交给 led.cgi 这个程序来处理了。 C 语言的程序是如何获取到浏览器提交的数据呢?就是通过 data = getenv("QUERY_STRING")这条语句,这里的就是获取环境变量的意思,
QUERY_STRING 就是代表环境变量。环境变量就是指表单提交以后浏览器地址栏格式化了的
字符串数据。
(2)实际测试
将led.cgi放到/web/cam/目录以后,并复制新的index.html,led.html文件到/web
目录,启动boa,在浏览器地址栏输入开发板 IP 地址有下图效果:
可以看到相比前一节的页面多了 LED 控制菜单项,之后点击该菜单项进入下一级页面,
如下图所示:
第一栏输入 1 表示控制第 1 号 LED,动作栏输入 1 表示点亮 LED,点击 sure 按钮,结果如下:
与此同时 TQ2440 的第 1 号 LED 亮了!说明网页控制 LED 是成功的!证明 BOA 以及 CGI都移植好了,可以结合两者做出更多有意思的小项目!
(3)注意事项
- 编译 CGI 程序的时候要用如下的命令:
arm-linux-gcc -L CGIC 绝对路径 -lcgic -I CGIC 绝对路径 C 语言源文件 -o cgi 后缀目标文件 其中 arm-linux-gcc 表示使用的是交叉编译器,因为要在 ARM 上使用程序,-L 选项指定了查找库文件的路径, 因为要用到 libcgic.a 库, 所以先用-L 选项支出该库文件所在的路径,-lcgic 指明了具体使用前面所指的路径下的什么库,这里就是 libcgic.a,-I 选项指明了头文件的路径,这里我们的库文件和头文件都是在原来解压 CGIC 源代码得到的原目录下编译产生的。
- 注意在 C 语言程序中打开设备的时候, 要查看自己的 LED 对应的设备文件名, TQ2440的是 led,但不是 leds0,这里不注意的话会导致点不亮 LED。
- C 语言编写 CGI 程序的时候不能用 main 函数作为主函数,因为 main 函数在 CGIC 的库文件中已经被定义了。
- 只要有标准输入输出的语言都可以编写 CGI 程序,但是要注意标准 CGI 程序的头部信息,打印开头要使用 MIME 头信息“Content-Type : text/html\n\n”来表示输出 HTML源代码给 web 服务器。而任何 MIME 头信息后必须有两个空行,这就是为什么要有\n\n 的原因。
- 将 cgi 程序复制到开发板上以后,要注意修改其可执行权限,最好修改成 777。
第3课-CGI程序开发
1. 显示第一幅图片
基于前面所述的C语言编写CGI程序的知识, 我们可以很快编写出显示图片的CGI程序。实际上, 显示图片的还是 HTML 页面文件, 只不过通过 CGI 将 HTML 页面的内容输出而已。 HTML显示图片是通过 img 标签插入一幅图片到指定位置,基本格式如下:
<img src="divcss5-logo-201305.gif" width="165" height="60" />
从前面的知识我们知道 CGI 程序的基本格式如下:
/*必要的头文件*/
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<cgic.h>
/*主函数不能是 main 函数*/
intcgiMain()
{
/*输出 HTML 格式的内容以便反馈给浏览器*/
cgiHeaderContentType("text/html");
fprintf(cgiOut,"<HTML><HEAD>\n");
fprintf(cgiOut,"<TITLE>LEDCGI</TITLE></HEAD>\n");
fprintf(cgiOut,"<BODY>");
fprintf(cgiOut,"<H1>ControlLedOk!</H1>");
fprintf(cgiOut,"</BODY>\n");
fprintf(cgiOut,"</HTML>\n");
return0;
}
在fprintf(cgiOut,"<H1>ControlLedOk!</H1>");后面加上 img 标签语句, 表明要插入的图
片路径以及图片在浏览器中的显示位置和大小,加入如下语句:
fprintf(cgiOut, "<img src=\"/sd/%s\" width=\"165\" height=\"60\" />",
"01-20000101000815-00.jpg");
其中,要注意以下几点:
(1) 双引号要使用转义字符,才能正确输入到 cgiOut,才能在 HTML 文件中保留必要的
双引号;
(2) 图片的路径要针对 BOA 服务器来写,之前我们把根目录下的/web 目录设置成了 BOA的主目录, 所以后面的 HTML 文件访问的目录是/web/目录的相对目录, 而不能是开发板文件系统的绝对路径。所以在 HTML 文件里的/cam/目录实际上是开发板的/web/cam/目录;
(3) 图片是存放在/mnt/sd 目录下的,但是我们不能直接访问/mnt/sd 目录,只能访问/web 目录及其子目录,所以我们要在/web 目录下新建一个软链接,链接到/mnt/sd 目录,软链接就相当于 windows 下的快捷方式;进入/web 目录以后用 ln -s /mnt/sd sd 命令即可创建/web 目录下的一个软链接 sd,所以路径我们写成了/sd 开头;
(4) 注意修改 width 和 height 的值,图片的文件名要用自己 motion 拍到的图片的实际文件名;
(5) 编译 CGI 程序要加上必要的 L 选项指定库文件路径,和 I 选项指定头文件路径。 将编译好的 CGI 程序 image.cgi 放到开发板文件系统的/web/cam 目录,修改/web 目录下的index.html 文件。这里主要是修改“查看图片”菜单,修改内容如下:
<a class="menu" href="/cam/image.cgi">
查看监控图片
让他索引到一个超链接,也就是 image.cgi,这样在点击查看图片的时候就可以执行
image.cgi 程序了。
2. 界面美化
前面的努力已经可以显示图片了, 但是界面不太美观, 我们希望整个监控系统的头部和尾部保持不变,中间部分是可变的,所以我们要先将原来 inde.html 文件的头尾分离,以便后面单独处理。
分离后头部文件命名为 top.html,尾部文件命名为 bottom.html,其内容分别为:
Top.html:
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<HTML>
<HEAD>
<title>Web Cameras</title>
<style type="text/css">
BODY {background-color: PaleTurquoise}
td.header,td.footer
{
padding: 0.3em 0.1em 1.8em;
background-color: DeepSkyBlue ;
clear: left;
font-size: 68%;
text-align: center;
}
p.header
{
font-family:"华文行楷";
font-size: 400%;
padding: 20px;
}
p.title
{
font-family:"华文行楷";
font-size: 120%;
padding: 2px;
background-color:DeepSkyBlue ;
width: 16em;
}
.menu
{
font-family:"华文行楷";
float:left;
text-decoration:none;
color:white;
background-color:DodgerBlue ;
padding:0.2em 1.2em 0.2em 1.2em;
border-right:1px solid white;
text-align: center;
font-size: 153%;
}
a.menu:hover {background-color:DarkMagenta }
li {font-size: 88%; }
</style>
</HEAD>
<BODY>
<center><table border="0" width="750">
<tr>
<td class="header">
<p class="header">
移动图像监控系统    
</p>
</td>
</tr>
<tr align=right>
<td>
<hr />
<a class="menu" >
系统介绍
</a>
<a class="menu" href="/cam/image.cgi">
查看监控图片
</a>
<a class="menu" >
查看监控视频
</a>
<a class="menu" >
返回首页
</a>
<a class="menu" href="/led.html">
LED 控制
</a>
</td>
</tr>
bottom.html 的内容如下所示:
<tr>
<td class="footer">
版权所有:<br>
http://www.enjoylinux.cn<br>
</td>
</tr>
</table></center>
</BODY>
</HTML>
至于 index.html 中间部分的以下内容不写入到头部和尾部,是因为这部分内容是显示系统连接图用的,所以不是公用的头尾信息 .
<tr>
<td align=center>
<hr />
<img src="/resource/connect.jpg" alt="系统连接图"
width="480" height="320"/>
<hr />
</td>
</tr>
但是这个部分占据的浏览器页面位置就是我们想要显示图片的位置, 所以我们要在 CGI 程序中专门修改这个部分的内容来显示图片。
有了公有的头部和尾部 HTML 文件以后,就是要在之前显示图片的 cgi 程序中想办法将两个文件的内容输出到 cgiOut,从而使得 CGI 程序能够将整个 HTML 页面的内容在浏览器显示出来,我们通过实现 printf_file 函数来实现将某个 HTML 文件的内容输出到 cgiOut,然后分别在显示图片的前面调用该函数显示公有头部 HTML,在显示图片的后面调用该函数显示公有尾部即可。Printf_file 的实现如下:
#define MAXBUF 1024
void printf_file(FILE * dst, char * src)
{
FILE * src_fp;
char temp_buf[MAXBUF];
src_fp = fopen(src,"r");
while( NULL != fgets(temp_buf,MAXBUF,src_fp))
fputs(temp_buf,dst);
fclose(src_fp);
}
之后在 cgiMain 函数里,修改如下:
int cgiMain()
{
printf_file(cgiOut,"../top.html");
fprintf(cgiOut,"<tr>");
fprintf(cgiOut,"<td align=center>
");
fprintf(cgiOut,"<hr />");
fprintf(cgiOut,
"<img
src=\"/sd/%s\"
width=\"165\"
height=\"120\"
/>","01-20000101000815-00.jpg");
fprintf(cgiOut,"<hr />");
fprintf(cgiOut,"</td>
");
fprintf(cgiOut,"</tr>");
printf_file(cgiOut,"../bottom.html");
return 0;
}
最后重新编译复制 image.cgi 文件。
3. 功能完善
虽然前面的工作已经可以让我们显示一幅图片在浏览器上, 但是我们希望在一个页面内同时显示多幅图片且具有跳转功能, 从而进一步显示整个目录下的所有图片, 而且要删除控制 led 的菜单项。
(1) 将图片文件名加入待显示列表
主要的操作步骤包括以下几步:
① 打开图片存储目录
② 读取目录中的一个文件
③ 判断文件名的后缀是不是“jpg”
④ 如果是 jpg 文件就将文件名加入 g_img 列表
⑤ 重复步骤 2 直到读取完存储目录下的所有文件
⑥ 返回所有 jpg 文件的数量
主要代码如下所示:
int list_pic(const char* path, const char *sfx)
{
//遍历/mnt/sd 目录,把所有的图片文件的名字加入列表
DIR *dir;
struct dirent *ptr;
int i = 0;
//1. 打开目录
dir = opendir(path);
//2. 读取目录中的一个文件
while( (ptr = readdir(dir))!=NULL)
{
//3. 判断该文件的后缀是否为 jpg
if( 0==cmp_sfc(ptr->d_name,sfx))
{
//4. 如果是图片文件,将其文件名加入列表
strcpy(g_img[i].name,ptr->d_name);
i++;
}
}
return i;
}
(2) 显示指定起始序号的图片
这里我们拟采用每一页显示 16 张图片的形式布局,每一行 4 张图片,共 4 行,所以要主要换行, 这个体现在 CGI 程序的 tr 和 td 标签的使用。 还计算出了所有的图片数以及总的页数,为后续下拉列表框做好准备。主要代码如下所示:
void show_pic(int start,int end,int total)
{
int i;
int j=0;
fprintf(cgiOut,"<tr>");
fprintf(cgiOut,"<td align=center>");
fprintf(cgiOut,"<table border=\"1\">");
for(i=start;i<end+1;i++)
{
if ((j%4)==0)
{
fprintf(cgiOut,"<tr>");
fprintf(cgiOut,"<td>");
}
j++;
fprintf(cgiOut, "<img src=\"/sd/%s\" width=\"160\" height=\"120\"
/>",g_img[i].name);
if ((j%4)==0)
{
fprintf(cgiOut,"<tr>");
fprintf(cgiOut,"<td>");
}
}
fprintf(cgiOut,"</table>");
fprintf(cgiOut, "<p class=\"little\">-- 第%d 页 共%d 页 --</p>", start/16 + 1,
total%16 ? total/16+1 : total/16);
fprintf(cgiOut,"</td>");
fprintf(cgiOut,"</tr>");
}
(3)实现下拉框
这里主要注意 gopage 标签就是指浏览器中显示为“go”的那个按钮,通过点击它提交用户请求。至于响应请求的代码在下一节给出。这些知识点主要是 HTML 设计的知识点,在此可不做深入研究。具体代码如下所示:
void show_select_form(int total)
{
int i, pgn;
pgn = total/16;
if (total % 16)
pgn++;
fprintf(cgiOut, "<tr>");
fprintf(cgiOut, "<td align=center>");
fprintf(cgiOut, "<br><form>");
fprintf(cgiOut, "转到第");
fprintf(cgiOut, "<select name=\"selectpage\">");
for (i = 0; i < pgn; i++)
fprintf(cgiOut, "<option value=\"opt%d\">%d</option>", i, i+1);
fprintf(cgiOut, "</select>");
fprintf(cgiOut, "页 ");
fprintf(cgiOut, "<input type=\"submit\" name=\"gopage\" value=\"go\"/>");
fprintf(cgiOut, "<form>");
fprintf(cgiOut, "</td>");
fprintf(cgiOut, "</tr>");
}
(4)处理用户请求
同样,这里只给出具体代码,至于代码内容都是 HTML 网页设计的知识点,不作深入讨
论。
//处理用户的选择请求
if (cgiFormSubmitClicked("gopage") == cgiFormSuccess) {
int i, sel;
char **optlist;
char tmp[10];
optlist = (char**)malloc(sizeof(char*) * total);
for (i = 0; i < total; i++) {
sprintf(tmp, "opt%d", i);
optlist[i] = strdup(tmp);
}
cgiFormSelectSingle("selectpage", optlist, total, &sel, 0);
start = sel * 16;
end = start + 15;
end = total < end ? total : end;
free(optlist);
}
最后,整个 CGI 程序的主程序如下所示:
/*主函数不能是 main 函数*/
int cgiMain()
{
int start,end,total;
start = 0;
end = 15;
//显示网页头部
printf_file(cgiOut,"../top.html");
//将图片文件名加入显示列表
total=list_pic("/mnt/sd","jpg");
//处理用户的选择请求
if (cgiFormSubmitClicked("gopage") == cgiFormSuccess) {
int i, sel;
char **optlist;
char tmp[10];
optlist = (char**)malloc(sizeof(char*) * total);
for (i = 0; i < total; i++) {
sprintf(tmp, "opt%d", i);
optlist[i] = strdup(tmp);
}
cgiFormSelectSingle("selectpage", optlist, total, &sel, 0);
start = sel * 16;
end = start + 15;
end = total < end ? total : end;
//fprintf(cgiOut, "<p>s=%d, e=%d</p>", start, end);
free(optlist);
}
//显示图片
show_pic(start,end,total);
//显示下拉列表框
show_select_form(total);
//显示网页底部
printf_file(cgiOut,"../bottom.html");
return 0;
}
(5) 在 CGI 程序中嵌入视频
在 HTML 页面中嵌入多媒体可以通过 embed 标签实现, 具体的可以查阅 HTML 嵌入多媒体相关知识点。这里给出显示视频的核心代码,至于其他布局,诸如加网页头部以及底部,显示下拉框以及响应用户请求等其他功能, 和我们之前显示图片的实现原理一样, 参考前面代码修改即可。
void show_movie(int nr, int total)
{
int i, j = 0;
if (nr > total - 1)
return;
fprintf(cgiOut, "<tr>");
fprintf(cgiOut, "<td align=center>");
fprintf(cgiOut,
"<embed
src=\"/sd/%s\"
autostart=true
loop=true
width=\"640\" height=\"480\">", g_avi[nr].name);
fprintf(cgiOut, "<p class=\"little\">-- 第%d 页 共%d 页 --</p>",
nr+1, total);
fprintf(cgiOut, "</td>");
fprintf(cgiOut, "</tr>");
}
将编写好的显示视频的 movie.c 文件编译成 cgi 文件以后,还需要修改相应的
index.html 文件,给“显示视频”菜单加上超链接以访问该 cgi 文件,显示存储目录下的所有视频。最终效果图如下所示,至此整个移动物体监控项目全部完成,希望各位读者进展顺利。
补充说明
(1) .如果内核启动的时候打印以下内容, 或者在 USB 摄像头插入以后打印以下内容:
usb 1-1: new full speed USB device using s3c2410-ohci and address 2
usb 1-1: device descriptor read/64, error -62
usb 1-1: device descriptor read/64, error -62
usb 1-1: new full speed USB device using s3c2410-ohci and address 3
usb 1-1: device descriptor read/64, error -62
usb 1-1: device descriptor read/64, error -62
usb 1-1: new full speed USB device using s3c2410-ohci and address 4
usb 1-1: device not accepting address 4, error -62
usb 1-1: new full speed USB device using s3c2410-ohci and address 5
usb 1-1: device not accepting address 5, error -62
可以通过延长 MPLLCON 寄存器赋值和 UPLLCON 寄存器赋值之间的间隔时间解决。vi
uboot2013.01/board/tq2440/tq2440.c
修改 board_early_init_f 函数以下语句
/* configure MPLL */
writel((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV,
&clk_power->mpllcon);
/* some delay between MPLL and UPLL */
pll_delay(4000);
/* configure UPLL */
writel((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV,
&clk_power->upllcon);
/* some delay between MPLL and UPLL */
pll_delay(8000);
/* set up the I/O ports */
为
/* configure MPLL */
writel((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV,
&clk_power->mpllcon);
/* some delay between MPLL and UPLL */
pll_delay(4000);
pll_delay(4000);
/* configure UPLL */
writel((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV,
&clk_power->upllcon);
/* some delay between MPLL and UPLL */
pll_delay(8000);
/* set up the I/O ports */
实际上就是在 MPLL 和 UPLL 之间加了一个 pll_delay(4000);
老版本的 uboot 修改 board_init 函数(板级文件 tq244.c)以下语句:
clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
/* some delay between MPLL and UPLL */
delay (4000);
/* configure UPLL */
clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);
将其改为:
clk_power->MPLLCON = ((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV);
/* some delay between MPLL and UPLL */
delay (4000);
delay (4000);
/* configure UPLL */
clk_power->UPLLCON = ((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV);
也就是多加一个 delay (4000);
之后重新编译 uboot 即可!