赞
踩
本系列文章将详细讲解该基于RK3399及ESP8285自动售货柜的完整实现方法,从硬件连接到网络通信再到软件实现,本产品所用开发板为RK3399以及MP08_2019/11/03 , 如有疑问与见解,可随时留言或评论
本项目所有资源文件:
https://pan.baidu.com/s/1L3gFmzxl-PafaeSkAFi58Q?pwd=srer
本系列以往文章列表:
①基于RK3399&ESP8285自动售货柜项目—ESP8266(8285)程序编写与烧录
②基于RK3399&ESP8285自动售货柜项目—MP08开发板端代码详解
前两篇文章中,我们学习了如何搭建ESP_SDK_V3.x开发环境、ESP代码框架以及本项目MP08开发板端的代码实现,那么本项目就剩最后一大块—售货机与用户的交互
我们知道,一个售货机,如果它没有手机端app或者小程序的交互,那么它必定有一个屏幕放在售货机上,用户直接可以通过这块屏幕来操作、支付,从而获得商品,那么我们现在也来做一块这样的交互屏幕
交互界面示例:
我们在做交互端前,要写出基本的需求,这样才能更好的、更加高效率的完成我们所需要的功能并且保证代码的稳定性
我这里列出几项基本需求,能保证大部分售货机都能够用上的基本需求:
那么有了这些基本需求,我们就可以分模块进行开发
在实现上述几个需求之前,我们需要事先移植几个开源库以及软件源码包,以支持rk3399开发板实现我们需要的功能
1、创建一个工作目录 mkdir ~/work/rk3399 2、创建源码目录:各个库的源码文件都存放在这个路径下 mkdir ~/work/rk3399/mplayer_source 3、创建编译目录:各个库编译的时候都在这个目录下进行 mkdir ~/work/rk3399/mplayer_build 4、创建编译安装的目录 mkdir ~/work/rk3399/mplayer_install 5、进入到工作目录 cd ~/work/rk3399 6、把相关的源码包复制到 mplayer_source 的文件夹中 cp /mnt/hgfs/mplayer源码包 ./mplayer_source/ 8、导出安装路径为全局路径:MPLAY_INSTALL是变量名,要记住,后面移植库需要使用到。 export MPLAY_INSTALL=$HOME/work/rk3399/mplayer_install 9、导出安装路径为全局路径:MPLAY_SRC是变量名,要记住,后面移植库需要使用到。 export MPLAY_SRC=$HOME/work/rk3399/mplayer_source 10、导出安装路径为全局路径:MPLAY_BUILD是变量名,要记住,后面移植库需要使用到。 export MPLAY_BUILD=$HOME/work/rk3399/mplayer_build
注意:这一步导出的全局路径,只在当前命令终端有效,如果新开了一个终端必须重新导出这个路径
1、解压源码
tar -xf zlib-1.2.3.tar.gz -C ./mplayer_build/
3、进入到解压后目录,
cd ./mplayer_build/zlib-1.2.3/
4、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行
CC=aarch64-linux-gnu-gcc \
./configure --prefix=/home/huzhiyuan/work/xiangmu/mplayer_install \
--libdir=/home/huzhiyuan/work/xiangmu/mplayer_install/lib --includedir=$MPLAY_INSTALL/include \
--shared
5、编译源码包,安装到指定目录下。
make -j8 && make install -j8
1、解压源码
$ tar -xf $MPLAY_SRC/libpng-1.2.57.tar.gz -C $MPLAY_BUILD
2、进入到解压后目录,
$ cd $MPLAY_BUILD/libpng-1.2.57/
3、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行
$ ./configure --prefix=$MPLAY_INSTALL \
CC=aarch64-linux-gnu-gcc --host=aarch64-linux-gnu \
--enable-shared --enable-static \
CPPFLAGS=-I/$MPLAY_INSTALL/install/include/ \
LDFLAGS=-L/$MPLAY_INSTALL/lib/ \
LIBS=-lz
4、编译源码包,安装到指定目录下。
$ make -j8 && make install -j8
如果提示没有zlib.h文件,解决办法:
然后再编译源码包即可
1、解压源码
$ tar -xf $MPLAY_SRC/jpegsrc.v9b.tar.gz -C $MPLAY_BUILD
2、进入到解压后目录,
$ cd $MPLAY_BUILD/jpeg-9b
3、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行
$ ./configure --prefix=$MPLAY_INSTALL \
CC=aarch64-linux-gnu-gcc --host=aarch64-linux-gnu \
--enable-shared --enable-static
4、编译源码包,安装到指定目录下。
$ make -j8 && make install -j8
1、解压源码
$ tar -xf $MPLAY_SRC/fftw-3.3.4.tar.gz -C $MPLAY_BUILD
2、进入到解压后目录,
$ cd $MPLAY_BUILD/fftw-3.3.4/
3、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行
$ ./configure --prefix=$MPLAY_INSTALL \
CC=aarch64-linux-gnu-gcc --host=aarch64-linux-gnu \
--enable-shared --enable-static
4、编译源码包,安装到指定目录下。
$ make -j8 && make install -j8
1、解压源码
$ tar -xf $MPLAY_SRC/alsa-lib-1.1.0.tar.bz2 -C $MPLAY_BUILD
2、进入到解压后目录,
$ cd $MPLAY_BUILD/alsa-lib-1.1.0
3、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行
$ ./configure --prefix=$MPLAY_INSTALL \
CC=aarch64-linux-gnu-gcc --host=aarch64-linux-gnu \
--disable-python
4、编译源码包,安装到指定目录下。
$ make -j8 && make install -j8
1、解压源码 $ tar -xf $MPLAY_SRC/MPlayer-1.3.0.tar.gz -C $MPLAY_BUILD 2、进入到解压后目录, $ cd $MPLAY_BUILD/MPlayer-1.3.0/ 3、配置:注意以下不是单行命令,需要一次性复制贴到命令终端上执行 $ ./configure --prefix=/home/huzhiyuan/work/xiangmu/mplayer_install \ --cc=aarch64-linux-gnu-gcc --host-cc=gcc \ --target=aarch64-linux-gnu \ --enable-cross-compile --enable-fbdev \ --enable-png --enable-jpeg --enable-alsa --enable-ossaudio \ --disable-x264-lavc --disable-freetype --disable-fontconfig \ --extra-cflags="-I/home/huzhiyuan/work/include/ -DHAVE_ARMV8=0" \ --extra-ldflags="-L/home/huzhiyuan/work/lib/" \ --extra-libs="-lasound -ljpeg -lpng" 2>&1 |tee logfile 4、修改config.mak文件,删除第33行的 ‘ -s ’ #解决安装时候的错误:修改config.mak ,删除 config.mak 中的 -s $ sed -i 's/INSTALLSTRIP = -s/INSTALLSTRIP = /g' config.mak 5、编译源码包,安装到指定目录下。 $ make -j8 && make install -j8
1、创建打包文件目录 mkdir mplayer-count/lib mplayer-count/bin 2、复制生成动态库文件和mplayer可执行文件到打包目录中 cp ./mplayer_install/lib/ lib* so* ./mplayer-count/lib cp ./mplayer_install/bin/ ./mplayer-count/bin 3、打包生成文件 tar cjf mplayer-count.bz2 mplayer-count 4、查看压缩包的大小 du -hs mplayer-count.bz2 5、 把打包好的文件mplayer-count.bz2 复制开发板中 sftp 6、 在开发板上,进入工作目录并解压对应压缩包 tar -xf mplayer-count.bz2 7、 将解压后的文件进行处理 cd mplayer-count cp ./mplayer-count/lib/* /lib/ -rfap cp ./mplayer-count/bin/mplayer /bin 这样的开发板的环境就搭建好了。
在Linux系统上,有许多好用的开源视频播放软件,这里我们使用mplayer多媒体播放器,它可以用于各个主流操作系统如:Windows、Linux、MAC OS等
我们将资源包中的video_file.tar.gz解压放到rk3399开发板中,终端输入指令:
mplayer -vo fbdev2 -ao alsa -zoom -x 800 -y 1280 xm.mpg
如果可以正常播放视频就表示源码包移植成功
mplayer具体其他用法可以自行查找相关文档,本篇主要讲解自动售货机项目rk3399端实现,故不深入讨论mplayer使用方法
在开始代码编写之前,需要保证开发板中有以下红框圈起来的文件
接着,我们新建一个user.c文件,用来编写我们的项目代码
进入user.c加入所有需要的头文件
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <setjmp.h>
#include <jpeglib.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
代码刚开始运行时,需要搜索给定目录下的所有多媒体文件名并存放到结构体中
#define MAX_COUNT 50 //音频/视频/图片最大数量 //全局 //多媒体文件数量 typedef struct media_count{ int video_count; //视频数量 int pic_count; //图片数量 int audio_count; //音频数量 }MEDIA_COUNT; MEDIA_COUNT m_cnt; //多媒体文件名 typedef struct media_name{ char *video_name[MAX_COUNT]; //视频名 char *pic_name[MAX_COUNT]; //图片名 char *audio_name[MAX_COUNT]; //音频名 }MEDIA_NAME; MEDIA_NAME m_name;
//获取路径下的多媒体(音/视频/图片文件) void get_media(const char *path) { struct dirent *dir_info;//里面存储了路径下的文件名 int count = 0;//数量 DIR *dp = opendir(path);//打开路径 //读取路径下的文件名,一次只能读取一个,指针会自动偏移 while((dir_info = readdir(dp)) != NULL) { if(strstr(dir_info->d_name,".mp4") != NULL)//mp4文件 { m_name.video_name[m_cnt.video_count++] = dir_info->d_name; } else if(strstr(dir_info->d_name,".avi") != NULL)//avi文件 { m_name.video_name[m_cnt.video_count++] = dir_info->d_name; } else if(strstr(dir_info->d_name,".mpg") != NULL)//mpg文件 { m_name.video_name[m_cnt.video_count++] = dir_info->d_name; } else if(strstr(dir_info->d_name,".mp3") != NULL)//mp3文件 { m_name.audio_name[m_cnt.audio_count++] = dir_info->d_name; } else if(strstr(dir_info->d_name,".jpg") != NULL)//jpg文件 { m_name.pic_name[m_cnt.pic_count++] = dir_info->d_name; } else if(strstr(dir_info->d_name,".png") != NULL)//png文件 { m_name.pic_name[m_cnt.pic_count++] = dir_info->d_name; } } }
由于播放视频时,还需要时刻捕获触摸屏触点,故视频、音频、图片等都需要新建一个线程代码去实现,防止独自占用整个进程
//全局 pthread_mutex_t lock;//线程互斥锁 void video_play(void)//播放视频函数 { int i; pthread_t pid[MAX_COUNT];//存储视频播放的线程id create_pipe();//创建管道 //创建互斥锁 if(pthread_mutex_init(&lock,NULL) != 0) { perror("creat mutex failed\r\n"); exit(EXIT_FAILURE); } for(i = 0;i < m_cnt.video_count;i++) { //创建播放视频的线程 if(pthread_create(&pid[i],NULL,pthread_media_play,m_name.video_name[i]) < 0) { perror("pthread_create error\r\n"); exit(EXIT_FAILURE); } } //主线程中不断获取触摸屏触点 while(1) { if(1 == get_ts_coordinate())//获取触摸屏坐标 { pthread_mutex_destroy(&lock);//销毁线程锁 for(i = 0;i < m_cnt.video_count;i++) { pthread_cancel(pid[i]);//终止线程 } clean_fb(0);//清屏 system("killall -9 mplayer");//终止mplayer进程 break; } } } void *pthread_media_play(void *arg)//媒体播放线程函数(音频和视频) { char *mediaName = (char *)arg;//强转 char temp[150] = {"mplayer -slave -quiet -input file=./my_pipe "};//mplayer播放命令前缀 while(1) { pthread_mutex_lock(&lock);//上锁 if(strstr(mediaName,"mp3")!=NULL)//如果为音频 { system(strcat(temp,mediaName)); } else//视频 { strcat(temp,"-geometry 0:0 -zoom -x 1000 -y 800 -vf rotate=1 -vo fbdev2 ");//视频播放命令 system(strcat(temp,mediaName)); } pthread_mutex_unlock(&lock);//解锁 sleep(1); printf("mediaName:%s\r\n",mediaName); } }
mplayer用代码控制播放需要管道,故还需要写一个管道创建的函数
int create_pipe(void)//创建管道
{
system("unlink ./my_pipe");//删除管道
system("mkfifo ./my_pipe");//创建管道
int Pipe_fd = open("./my_pipe", O_RDWR);
if (Pipe_fd < 0)
{
perror("open pipe file ERROR!\n");
return Pipe_fd;
}
return Pipe_fd;
}
至此,视频播放就已经完成
如果我们想要单独播放音频文件(mp3),那么可以加入以下函数
/* 功能:播放音频 形参:int num:待播放音频在m_name.audio_name数组中的编号 返回:void */ void audio_play(int num)//播放音频函数 { int i; pthread_t pid;//音频播放线程id create_pipe();//创建管道 //创建互斥锁 if(pthread_mutex_init(&lock,NULL) != 0) { perror("creat mutex failed\r\n"); exit(EXIT_FAILURE); } if(pthread_create(&pid,NULL,pthread_media_play,m_name.audio_name[num]) < 0) { perror("pthread_create error\r\n"); exit(EXIT_FAILURE); } }
触摸屏设备节点文件存放在/dev/input/路径下,在使用之前我们需要先打开触摸屏设备节点文件,通常文件名为event0-4,本项目rk3399触摸屏设备节点文件名为event1
//全局 #define OPEN 1 #define CLOSE 0 #define LCD_LENGTH 1280 //LCD屏幕长度(像素) #define LCD_WIDTH 800 //LCD屏幕宽度(像素) #define LCD_SIZE (1280*800) //LCD屏幕尺寸 //触摸屏数据结构 typedef struct touch_screen_value{ int fd_ts;//触摸屏文件描述符 int x;//x轴绝对位置坐标 int y;//y轴绝对位置坐标 }TS_VALUE; TS_VALUE ts_value; /* 功能:打开/关闭触摸屏 形参:int flag:1 打开 0关闭 返回:成功返回0 失败返回非零 */ int open_or_close_ts(int flag) { switch(flag) { case OPEN: ts_value.fd_ts = open("/dev/input/event1",O_RDWR); if(ts_value.fd_ts < 0) { perror("open fd_ts error\r\n"); exit(EXIT_FAILURE); } break; case CLOSE: return close(ts_value.fd_ts); break; } return 0; }
我们需要知道,触摸屏为input子系统下的标准节点文件,它上报的是ABS(绝对坐标事件),下面为获取触摸坐标点函数
/* 功能:获取触摸屏坐标 形参:void 返回:int ret:获取到x,y坐标返回1 否则返回0 备注:EV_ABS:绝对坐标 多点触控(多点触控的电容屏) #define ABS_MT_POSITION_X 0x35 #define ABS_MT_POSITION_Y 0x36 #define ABS_MT_PRESSURE 0x3a */ int get_ts_coordinate(void) { struct input_event temp;//输入事件结构体 int ret = 0; ts_value.x = ts_value.y = 0;//每次进来都清零 while(1) { read(ts_value.fd_ts,&temp,sizeof(struct input_event));//获取输入事件 //获取x轴绝对位置坐标 //type为事件类型-绝对坐标 //code为数据项 //value为数据值 if(temp.type == EV_ABS && temp.code == ABS_MT_POSITION_X && temp.value < LCD_WIDTH && temp.value > 0) { ts_value.x = temp.value; } //获取y轴绝对位置坐标 else if(temp.type == EV_ABS && temp.code == ABS_MT_POSITION_Y && temp.value < LCD_LENGTH && temp.value > 0) { ts_value.y = temp.value; }else { //如果两个都大于0,就表示x和y均获取到 if(ts_value.x > 0 && ts_value.y > 0) { ret = 1; } break; } printf("x:%d y:%d\r\n",ts_value.x,ts_value.y); } return ret; }
图片显示这一模块,要考虑到我们既有bmp格式图片,也有其他类型格式图片比如jpg,与音视频播放相同,我们在显示图片的时候也需要获取触摸点,故也采用线程的方式来实现
在图片显示函数中,我还加入了摄像头调用接口(用于模拟人脸支付),如若你想加入真正的支付接口,也可以直接在本函数中修改代码即可
//全局 int choice = -1; //商品选择标志 /* 功能:显示图片(bmp/jpg) 形参:void 返回:void 购物窗口像素点分部: 选择: (0,1010) - (399,1280) 付款: (400,1010)- (799,1280) 图片: (0,163) - (799-1009) 货道: (0,0) - (569,162) 退出: (570,0) - (799,162) */ void pic_play(void) { pthread_t pid;//图片显示线程id int i = 0,flag = 1; int ret; while(flag) { //创建显示图片的线程 if(pthread_create(&pid,NULL,pthread_pic_play,m_name.pic_name[i]) < 0) { perror("pthread_create error\r\n"); exit(EXIT_FAILURE); } printf("%s has been display\r\n",m_name.pic_name[i]); pthread_join(pid,NULL);//等待线程结束,回收资源 while(1) { if(1 == get_ts_coordinate())//获取触摸点 { if(ts_value.x >= 0 && ts_value.x <=399 && ts_value.y >= 163 && ts_value.y <= 1009) //上一张图片 { i = (i==0) ? (m_cnt.pic_count-1) : (i-1); } else if(ts_value.x >= 400 && ts_value.x <=799 && ts_value.y >= 163 && ts_value.y <= 1009) //下一张图片 { i = (i == m_cnt.pic_count-1) ? 0 : (i+1); } else if(ts_value.x >= 570 && ts_value.x <=799 && ts_value.y > 0 && ts_value.y <= 162) //退出 { flag = 0; } else if(ts_value.x >= 0 && ts_value.x <=399 && ts_value.y > 1010 && ts_value.y <= 1280) //选择 { choice = i; } else if(ts_value.x >= 400 && ts_value.x <=799 && ts_value.y > 1010 && ts_value.y <= 1280) //付款 { if(choice < 0)//如果没有点击选择按钮 { audio_play(0);//播放提示音频,提示:请选择商品 continue; } ret = fork();//创建一个子进程来调用摄像头 if(ret == 0) { system("./v4l2_sample");//运行摄像头 exit(EXIT_SUCCESS); } else { char buf[10] = {0}; sleep(5);//等待5s printf("******************************\r\n"); system("killall -9 v4l2_sample");//关闭摄像头进程 wait(NULL);//等待进程结束,回收资源 sprintf(buf,"%d",*(m_name.pic_name[choice]+7)-'0');//取出货道号并放在buf中 printf("buf = %s\r\n",buf); send(clifd,buf,strlen(buf),0);//发送货道号到TCP_Client端---也就是MP08_ESP8285 recv(clifd,buf,sizeof(buf),0);//如果MP08接收到数据,则会返回一个'0' printf("recv:%s\r\n",buf);//打印查看是否正确发送 choice = -1;//切换状态为未选择 } } break; } } } }
在这里,我们可以用显存映射的方法,编写一个清屏函数
/* 功能:清屏函数 形参:int color:清屏颜色 返回:void */ void clean_fb(int color) { struct fb_fix_screeninfo fix_info;//固定数据,如显存等 struct fb_var_screeninfo var_info;//可变数据,如像素等 int *pfile = NULL;//指向映射后的显存地址 int fd_fb;//显示屏设备节点文件描述符 int x,y,ret; //打开LCD显示屏 fd_fb = open("/dev/fb0",O_RDWR); if(fd_fb < 0) { perror("open fd_fb error\r\n"); exit(EXIT_FAILURE); } //获取LCD设备信息 ret = ioctl(fd_fb,FBIOGET_FSCREENINFO,&fix_info); if(ret < 0) { perror("ioctl fix_info error\r\n"); exit(EXIT_FAILURE); } ret = ioctl(fd_fb,FBIOGET_VSCREENINFO,&var_info); if(ret < 0) { perror("ioctl var_info error\r\n"); exit(EXIT_FAILURE); } //打印信息检查参数 printf("fix_info.smem_len = %d, fix_info.line_length = %d, " "var_info.xres = %d, var_info.yres = %d, var_info.bits_per_pixel = %d\n", fix_info.smem_len, fix_info.line_length, var_info.xres, var_info.yres, var_info.bits_per_pixel); //映射显存地址 pfile = (int *)mmap(NULL,fix_info.smem_len,PROT_READ |PROT_WRITE,MAP_SHARED,fd_fb,0); if(pfile == MAP_FAILED) { perror("mmap error\r\n"); exit(EXIT_FAILURE); } for(y = 0;y < var_info.yres;y++) { for(x = 0;x < var_info.xres;x++) { *(pfile + x + y * var_info.xres) = color; } } //关闭LCD屏幕,释放资源。 close(fd_fb); //取消映射 munmap(pfile,fix_info.smem_len); }
我们在本地目录下,创建一个临时文件夹aaa
将资源包中的以下文件放到aaa中
修改lcd.c的114行,将该行注释掉,防止调用摄像头时清屏(如果想要清屏也可以不注释掉)
接着,运行Makefile文件
会生成/bin 和 /debug两个文件夹,查看/bin中的文件,生成了一个可执行文件v4l2_sample
将生成的v4l2_sample复制到跟user.c同一个路径下
运行示例
现象
我们在MP08-ESP8285中已经编写好了TCP客户端的代码,那么我们就需要在RK3399上编写TCP服务端的代码来与MP08进行通信
#define SERVER_IP "192.168.31.116" //存放rk3399开发板ip地址 #define PORT_NUM 6000 //TCP-port typedef struct sockaddr SA; typedef struct sockaddr_in SIN; int serfd,clifd; SIN seraddr,cliaddr; socklen_t addrlen; void connect_tcp(void)//连接tcp { int ret; //创建通信套接字 serfd = socket(AF_INET,SOCK_STREAM,0); if(serfd == -1) { perror("socket failed\r\n"); exit(0); } //编写服务器地址信息 bzero(&seraddr,sizeof(SIN)); //功能类似memset seraddr.sin_family=AF_INET; seraddr.sin_port = htons(PORT_NUM); seraddr.sin_addr.s_addr = inet_addr(SERVER_IP); //对IP地址进行转化 //绑定函数 int reuse=1; ret=setsockopt(serfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)); ret = bind(serfd,(SA*)&seraddr,sizeof(SA)); if(ret == -1) { perror("failed\r\n"); return -1; } //创建监听队列 ret = listen(serfd,5); if(ret == -1) { perror("listen failed\r\n"); return -1; } //等待被连接 clifd = accept(serfd,(SA*)&cliaddr,&addrlen); if(clifd == -1) { perror("accept failed\r\n"); return -1; } printf("accept sucess\r\n"); }
主函数就非常的简单,因为我们已经对各功能进行了模块化处理,只需要对各个功能进行调用就行了
int main(int argc,const char *argv[]) { if(argc != 2)//需要两个参数,第一个是可执行文件名,第二个是存放媒体文件的路径 { fprintf(stderr,"Usage:%s [media path]\r\n",argv[0]); exit(EXIT_FAILURE); } connect_tcp();//连接tcp get_media(argv[1]);//获取路径下的媒体文件 open_or_close_ts(OPEN);//打开触摸屏 while(1) { clean_fb(0);//清屏 video_play(); //播放广告视频 pic_play();//显示货道图片 } //通信结束 close(clifd); close(serfd); }
那么,至此整个项目就已经完成
整个项目由于时间较为紧迫,代码采用了大量的全局变量以保证程序尽量一遍就能写出来且没问题,我本人其实非常不喜欢用全局变量,故希望后来者能够利用更好的数据结构和算法来修改或者重构本项目代码,如有人写出来,可以随时联系我,与我一起交流
同时在本文最后也感谢胡工对我学习上、生活上的教导,给我提供大量丰富的资源,有机会接触到如此棒的项目,在今后工作上我也将继续努力,保持学习的积极主动性,随时为大家分享我工作上、生活中学习到的技术或其他知识
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。