




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
视觉随动摄影和移动端播放系统设计摘要随着无线网络的普及,网络传输速度的提升,移动设备所占市场份额越来越大,通过移动设备进行控制的网络视频监控发展迅速。监控视频的播放操作不仅局限在个人电脑上,而且在个人移动设备上占据更大份额。如今市场上有很多移动视频监控的产品和配套App,对视频监控的需求很大。本设计主要研究如何通过树莓派微型电脑控制摄像头,树莓派传输监控视频流到云端服务器,同时,编写特定Android应用程序,远程Android设备上对视频流进行接收并且播放,同时应用程序内提供控制摄像头的操作,可转动摄像头达到监控移动物体或人的目的。本设计主要包括树莓派GPIO控制,舵机控制,内网穿透技术和Android应用程序的编写。关键词:树莓派,视频传输,Android系统,HTTP协议AbstractWiththepopularitycoverageofWi-Finetwork,theincreaseofnetworkspeedandmoreusageofmobiledevices,onlinenetworkmonitoringwithmobiledevicesisdevelopingrapidly.Nowadays,peopledon’tonlywatchthemonitorontheirPCs,butalsopreferviewingthevideostreamontheircellphones.Also,therearemanysoftwareandcorrespondingappsonmobilemonitoring,whichshowsagreatmarket.ThisdesignfocusesonhowtocontrolacamerabyRaspberryPi,streamreal-timevideotoacloudserver,developadesiredAndroidapplicationtoreceivethevideostreamandplayitonAndroiddevices,withaccesstothecamerathatcanchangecameradegreetomonitormovingobjectsorpeople.ThedesignwillinvolveGPIOcontrolonRaspberryPi,servocontrol,NATtraversalandAndroidapplicationdevelopment.Keywords:RaspberryPi,videostreaming,Android,HTTP目录1. 绪论 71.1. 设计背景和目的 71.2. 本设计涉及的相关技术 72. 方案设计与论证 82.1. 基本介绍 82.2. 实现目标 82.3. 方案选择 82.4. 整体结构图 92.5. 材料清单 102.6. 成品图 103. 树莓派系统的初始化和设置 113.1. 树莓派简介 113.2. 树莓派系统安装和开机 113.3. 树莓派无线网络设置 113.4. 树莓派SSH连接 133.5. 树莓派远程桌面控制 164. 摄像头模块 184.1. 安装步骤: 184.2. 摄像头安装后成品图 184.3. 摄像头操作命令 195. 视频流处理部分 205.1. 软件选择 205.2. MJPG-streamer的安装和配置 206. 摄像头舵机部分 236.1. 舵机的简介 236.2. 舵机的控制原理 246.3. 舵机的安装 256.4. 舵机的操控 267. 内网穿透部分 297.1. 内网穿透简介 297.2. frp在服务端的安装配置 297.3. frp在客户端的安装配置 317.4. 总体结构图和运行测试 328. 移动端App的编写 338.1. Android系统简介 338.2. Android系统架构 338.3. AndroidApp入门 348.4. activity生命周期 368.5. 编写App界面 388.6. HTTP工具类的编写 418.7. activity代码的编写 428.8. 自定义view的编写 458.9. App发送指令操控舵机 478.10. 总体测试与完善 50总结与展望 52参考文献 53致谢 54附录 55MjpegView代码: 55HttpUtils代码 70绪论设计背景和目的社会科技水平不断发展,网速提示幅度越来越大。远程、便携式的实时视频监控发展速度也是非常惊人。视频传输和监控或者在各行各业已经成为一个很普遍的需求。对于日常家庭,对家庭居住环境的监控也是日益增长的需求,各式各样的家庭监控产品层出不穷。智能手机逐渐普及,智能手机不论系统平台是iOS或者Android,一般都配备有摄像头和麦克风,能够满足视频监控的需要,智能手机的普及为视频监控系统带来了新的趋势,基于智能手机设计移动视频监控系统可以不受空间的限制,它的实时性、便携性和可扩展性很好地满足了人们对视频监控系统的需求。目前来说,人们使用移动手机平台大多数是Android系统和iOS。当前移动设备系统的市场份额中,市场占有率大的依旧是Android系统。根据以往经验,现在打开应用市场搜索,可以找到很多远程监控app。例如我们可以使用一部手机安装采集端app,作为监控采集端,另一手机安装接收端app,作为监控接收端,观看监控视频内容。对于这类app,因为选用的手机摄像头作为采集端,一般只能放在某一个固定的位置,监控的画面是固定的,不能移动。针对于这一类需求,本设计探讨的主要是如何自己搭建一个基于Android的视频传输系统,选择使用树莓派控制的摄像云台进行视频采集和传输,同时监控的条件不再满足于使用本地局域网完成,同样做到外网可以访问并控制该摄像云台,可以远程观察到监控的情况,同时可转动摄像头,多方位监控。本设计涉及的相关技术 本设计主要完成基于android的视觉传输系统,使用云端通讯接口和服务器,控制树莓派的摄像云台运动,监控内容传到服务器,移动端app访问数据进行播放,移动监控画面。具体涉及的技术方面有树莓派系统的操作,python语言编程控制树莓派GPIO,MJPG-Streamer视频服务器的搭建,内网穿透的应用,和满足本设计需求的Android应用程序的编写。方案设计与论证基本介绍本设计主要由两个部分组成:控制摄像云台的树莓派微型电脑,和Android移动端的应用程序。实现目标树莓派驱动摄像头模块,获取实时的监控视频流树莓派操控摄像云台的转动传输视频流到云端服务器,在外网可以访问到该实时视频流,同时同一局域网也可以访问对应的视频接收端即android应用程序,编写UI界面,能实时观看到该视频流android应用程序同时提供控制摄像云台的操作,通过云端服务器发送控制命令到树莓派,移动显示的监控画面。方案选择对于摄像头的选择,有网络USB摄像头或树莓派配套的官方摄像头两种可以选择,这两个在树莓派上都可以使用。本设计中选择树莓派摄像头,通过CSI接口软排线方式接入到树莓派,整个模块大小为20x25x10mm,体积小巧。树莓派有内置python摄像头开发包Picamera,能对成像或视频的曝光度,亮度等摄像参数进行方便地调节,对应的python代码也简单。同时树莓派摄像头有500万像素,最高支持1080P/30FPS的视频录制,亦能满足常规的需要。对于摄像头操作模块的选择,步进电机或者舵机是我们比较好的选择。最普遍用到的伺服电机系统是舵机,同时也是低端的伺服电机系统。舵机对应的英文是Servomotor,人们一般简称为Servo。舵机将PWM信号与滑动变阻器的电压相比对,通过硬件电路实现固定控制增益的位置控制。电机、传感器和控制器,构成完整的伺服电机系统。舵机价格不贵,结构小但是比较合一,那相应的缺点也是存在的,就是精度低,位置镇定能力较差,但对于实现低端需求,是足够的,包括本设计的需求。所以本设计选择舵机,主要原因在于价格便宜,淘宝上一个9G舵机价格12块,可节省成本。对于云端服务器,现在大型互联网公司都有提供云产品服务,例如阿里云,腾讯云,国外的有亚马逊AWS。本设计出于成本考虑,笔者购买了阿里云学生优惠的轻量应用服务器,上限带宽有10MB/s,作为学习目的。对于移动端应用程序,本应用对性能要求不是很高,主流android系统版本也都可以运行本应用。对于树莓派的选择,选择的是当前最新版的树莓派版本3B+,不用插入无线网卡,内置了Wi-Fi模块,可以直接接入Wi-Fi网络,与手机或其他家庭设备处于同一局域网下。对于树莓派中对视频传输和视频流的处理,有motion,mjpg-streamer,本设计选用mjpg-streamer,因为相比motion,能控制的视频参数和输入输出比较多,同时画面延迟低。让视频通过外网访问则使用内网穿透技术,用的是frp库,比较简单,一般家庭也没有可用的公网IP。控制树莓派摄像头转动的命令,树莓派端使用flask这一pythonweb框架,接收命令,Android端使用HTTP协议,发送POST请求到树莓派整体结构图材料清单一台树莓派3B+(5V电源、16G内存卡),一个树莓派配套摄像头两个TowerProSG90舵机,一个双轴尼龙云台支架(用于固定摄像头和转动),若干条杜邦线一台Android手机阿里云轻量应用服务器成品图树莓派系统的初始化和设置树莓派简介树莓派,可以说是世界上最小的台式机或微型电脑,由英国的慈善组织“树莓派基金会”研发出来,艾·迩濮敦是该开发项目的领导人。在2012年3月,树莓派正式发售,大小尺寸就一张银行卡差不多,拥有的基本功能却和普通电脑丝毫不差。树莓派英文名是RaspberryPi,基于ARM机构,以普通存储卡为存储硬盘,但树莓派这一块很薄的主板却配备有几个USB接口和一个网线接口,所以树莓派能外接网线、鼠标和键盘,同时还配备有高清HDMI输出口和视频模拟信号的输出口。因此,像在树莓派上自己开发一个小的程序或者是小游戏,再自己外接上显示器,便可以实现玩游戏的功能,播放视频或音乐也是非常简单,功能丰富。树莓派系统安装和开机因为新到手的树莓派不会带有存储卡,需要自己额外购买,那么没有硬盘这一存储介质,自然也没有内置的操作系统,用户需要自己下载镜像文件,手动安装树莓派系统。第一步先去树莓派官网下载系统镜像文件,同时需要准备一张16G内存卡作为树莓派的硬盘,内存卡容量可以更大,但至少要16G比较好,16G能满足本设计的需求。系统下载完后,需要使用win32diskimager镜像安装工具,对刚下载完的镜像文件刷入到SD卡中(需要SD卡读卡器),刷系统之前,一般需要对SD卡进行快速格式化操作。刷入系统到SD卡后,会发现SD卡所显示的容量低于预期。这是因为在Windows系统中只能显示出FAT格式的boot分区,只有几十个MB,更大的分区是Linux分区,Windows系统是无法看到的,这并不影响树莓派系统的工作。树莓派无线网络设置刷好系统后,先不直接把SD卡插入树莓派中,还需要对系统中的一些文件加以修改。因为树莓派直接开机的话,在没有外接显示器的情况下无法对之操作,而想通过远程桌面控制树莓派的话需要先让树莓派处于同一局域网下。那么,需要做的就是让树莓派接入当前Wi-Fi网络下。树莓派在为开启的状态下,用户需要事先修改树莓派boot目录下的wpa_supplicant.conf文件,在该文件里配置Wi-Fi的SSID和密码。配置完成后,树莓派开机启动后会自行读取wpa_supplicant.conf配置文件,连接上Wi-Fi。操作方法如下:先将树莓派系统刷入SD卡,电脑上读该SD卡,在boot分区,也就是电脑上可以看到的boot目录下,直接新建一个文件,命名为wpa_supplicant.conf,以下面的配置模式写入内容,然后保存wpa_supplicant.conf文件:country=CNctrl_interface=DIR=/var/run/wpa_supplicantGROUP=netdevupdate_config=1network={ssid="WiFi-A"psk="12345678"key_mgmt=WPA-PSKpriority=1}network={ssid="WiFi-B"psk="12345678"key_mgmt=WPA-PSKpriority=2scan_ssid=1}#ssid:WiFi网络的ssid,即WiFi显示出来的名称#psk:WiFi的连接密码#priority:当存在多个可用WiFi时,优先级判断,数字越大连接上该WiFi的优先级越高(不要填写负数,负数没有意义)#scan_ssid:连接隐藏(即没有开启ssid广播)的Wi-Fi时需要指定该值为1如果Wi-Fi没有设置连接密码network={ssid="你的无线网络名称(ssid)"key_mgmt=NONE}如果你的Wi-Fi使用WPA/WPA2加密network={ssid="你的无线网络名称(ssid)"key_mgmt=WPA-PSKpsk="你的Wi-Fi密码"}树莓派SSH连接配置好网络这一部分,还需要再开启树莓派的SSH服务(默认是关闭的)。没开启该服务之前,想通过SSH与树莓派建立通信自然是失败的。手动启用SSH的方法是:在boot目录新建一个空白的、不要有文件类型后缀的、命名为ssh的文件。树莓派启动运行后,系统能扫描检测到该文件,则开始启用SSH连接服务。然后我们自己用软件或路由器管理页面获取树莓派内网IP地址,应用SSH协议,与树莓派建立通信。Wi-Fi信息配置好之后,可以正式地把SD卡插入到树莓派,连接电源,开机。开机一段时间后,树莓派就会根据我们之前的Wi-Fi配置,自动连接上当前的Wi-Fi网络,这样我们同一网络下的设备便有机会访问到该树莓派。我们可以使用网段扫描工具如AdvancedIPScanner,这类软件会自动检测电脑所在的局域网下所有的设备,对应名称和制造商都能显示出来。笔者电脑使用的安全防护软件有提供扫描同一网络下设备的功能,笔者便直接使用,如下图可以看到树莓派的地址是78因为要开始对树莓派进行操作,在没有外接显示屏的情况下,我们采用远程桌面的方式连接并控制树莓派。虽然树莓派现在处于开机状态了,但是还没有开启远程桌面所需要的相关服务,还无法进入到树莓派的桌面进行操控。那么,在这里我们要和树莓派建立最初的一个连接就是通过SSH连接树莓派,之前的新建ssh文件就是为了这一步而准备。SSH是由IETF网络工作组开发的SecureShell的首字母缩写,它就是一种协议,是安全的且基于应用层。SSH最初出现于UNIX系统,SSH几乎可以在所有UNIX平台上运行,例如包括Linux,AIX,Solaris,DigitalUNIX等,后来很快移植扩展到其他操作平台,多个平台上都有SSH客户端。Windows电脑上下载一个SSH终端软件,笔者在这里使用的是PUTTY。界面如下:输入树莓派的地址,即刚才获取到的78端口填写为22,即树莓派默认暴露出来的SSH端口。信息填写好,点击连接,会弹出一个警告框,点击确定。然后进入黑窗口命令行界面,以初始用户名pi和密码raspberry登录会话,建立通信。登陆后如图:树莓派远程桌面控制接下来一步,便是启动树莓派的远程桌面服务,因为树莓派系统本身已经有自带REALVNCServer的服务,我们要做的就是启动它,输入命令vncserver-2这个命令的意思就是启动一个vncserver桌面号制定为2,同时还可以跟上其他参数,如桌面的分辨率等等。然后,我们在电脑上下载vncviewer,再次输入树莓派的ip内网地址加上开启的桌面号2:如图,这样就进入了树莓派的桌面,我们就能使用电脑上的鼠标和键盘来操控树莓派了。为了后期方便,我们打算让树莓派开机就自启动该远程桌面服务,而不用每次都先连接SSH,输入命令去开启这个服务。这样每次开机后,便可以直接打开vncviewer操控树莓派。配置方法如下:首先sudonano/etc/init.d/vncserver(即在/etc/init.d/目录下创建编辑一个名为vncserver的文件)然后复制下面的内容右键粘贴进去#!/bin/sh#SettheUSERvariabletothenameoftheusertostartvncserverunderexportUSER='pi'(这里就是对应的登录用户的名字,默认pi)evalcd~$USERcase"$1"instart)#启动命令行。此处自定义分辨率、控制台号码或其它参数。su$USER-c'/usr/bin/vncserver-depth16-geometry1024x768:2'echo"StartingVNCserverfor$USER";;stop)#终止命令行。此处控制台号码与启动一致。su$USER-c'/usr/bin/vncserver-kill:2'echo"vncserverstopped";;*)echo"Usage:/etc/init.d/vncserver{start|stop}"exit;;esacexit然后Ctrl+O回车保存,Ctrl+X退出文本编辑器然后修改权限:sudochmod755/etc/init.d/vncserver然后添加开机启动项:sudoupdate-rc.dvncserverdefaults最后重启树莓派:sudoreboot至此,开机自启动的工作便完成了。摄像头模块安装步骤:1. 找到CAMERA接口(主板上印有该字眼),掀起深色胶带。2. 用手指轻轻撬动起两边的接口挡板。3. 撕掉摄像头模块上的塑料保护膜。确保黄色部分的PCB(有字的一面)正确安装。4. 将排线插入CAMERA接口,把有蓝色的一面,对向网线接口方向。摄像头安装后成品图摄像头安装好后如图:工作时,led红灯会亮,如图:注意:把摄像头的排线接入树莓派的时候,不要“热插拔”。因为摄像头对静电很敏感,安装摄像头时要确保树莓派是断电状态,摄像头操作命令接着执行:$sudoraspi-config通过键盘方向键,去启用摄像头模块,重启树莓派。设置完成后,可以对摄像头进行测试工作。树莓派提供了三个相关的操作命令:raspistill、raspivid、raspistillyuv。常用的是这两个:raspistill和raspivid,前者用于获取图像,后者用于获取视频。1)用rasptill获取一张图片,如:raspistill-oimage.jpg-rot180参数o输出到文件,参数rot让图片旋转180度。2)用raspivid获取视频raspivid-ovideo.h264-t10000捕捉10秒H.264压缩格式的视频,保存到文件video.h264。视频流处理部分软件选择树莓派系统下有两个对视频处理比较好的软件:motion和mjpg-streamer:motion是一个免费开源的Linux系统下的视频监控软件,用C语言写成,可用于运动检测。它可以监视一个或多个摄像头的视频信号,并且能够检测到图像的主要部分是否已经改变,当它检测到正在发生运动时还可以保存视频)。motion是命令行驱动的,占用空间小,CPU使用率低,可以作为守护进程运行。MJPG-streamer可以从Linux-UVC兼容的网络摄像头,文件系统或其他输入插件中获取JPG,并通过HTTP将它们作为M-JPEG流式传输到网页浏览器上、VLC和其他软件。经过资料查证,motion的卡顿较大,二十多秒的延迟基本无法接受。而MJPG-streamer就好很多。虽然motion实现更简单,但最终选用MJPG-streamer实现视频流的传输。MJPG-streamer的安装和配置首先安装相应的依赖库sudoapt-getinstallsubversionlibjpeg8-devimagemagicklibv4l-devcmakegit从GitHub下载MJPG-streamer,并编译,安装gitclone/jacksonliam/mjpg-streamer.gitcdmjpg-streamer/mjpg-streamer-experimental/makeallsudomakeinstallMJPG-streamer提供了多个输入源和输出源的选择,支持的输入源有6种:input_file,input_http,input_opencv,input_ptp2,input_raspicam,input_uvc支持的输出源有4种:output_file,output_http,output_viewer,output_zmqserver例如,如果接上普通的免驱动USB摄像头,执行命令:./mjpg_streamer-i"./input_uvc.so"-o"./output_http.so-w./www"如果是树莓派摄像头则使用以下命令./mjpg_streamer-i"./input_raspicam.so"-o"./output_http.so-w./www"对于树莓派摄像头的输入源控制,MJPG-streamer也提供了很多参数:[-fps|--framerate]...:设置视频帧率,默认为5帧/秒
[-x|--width]:帧捕获的宽度,默认为640
[-y|-height]:帧捕获高度,默认480
[-quality]:设置JPEG质量0-100,默认为85
[-usestills]:使用静止模式而不是视频模式
[-preview]:启用全屏预览
-ISO:设置捕获ISO
-vs:打开视频稳定功能
-ev:设置EV补偿
-ex:设置曝光模式(参见raspistillnotes)
-awb:设置AWB模式(参见raspistill说明)
-rot:设置图像旋转(0-359)-stats:计算每张图片的图像统计数据(降低噪点)
-drc:动态范围补偿级别(参见raspistillnotes)
-hf:设置水平翻转
-vf:设置垂直翻转笔者最终输入的命令是:./mjpg_streamer-i"./input_raspicam.so-vs-x320-y240"-o"./output_http.so-w./www",即启用打开视频稳定功能,虽然可能丢帧,但确保了视频传输的流畅性,宽度和高度分别为320和240,原因是减少网络带宽的压力。输出是以HTTP形式访问,即最终会在树莓派架设起一个本地服务器,以这种形式输出也方便以后的内网穿透服务,将视频暴露到外网中,使用外网网址也可以访问到视频流。执行命令后,输入78:8080则可以看到实时视频。笔者78是我树莓派局域网内的IP地址。如图:也可以输入78:8080/?action=stream直接获取实时视频流的网址进行观看,可在网页浏览器或VLC播放器中直接输入,该地址以后会用到。效果如图:如果有多个输入源,可以通过下面的方式独立地访问每个视频流:78:8080/?action=stream_078:8080/?action=stream_1如果是查看当前时刻的静态JPEG图像,可以直接访问http://78:8080/?action=snapshot摄像头舵机部分舵机的简介舵机,主要是由外壳、电路板、驱动马达、减速器与位置检测元件所构成,是一种位置伺服驱动器,适用于那些需要角度不断变化并可以保持的控制场景。它的工作原理是由接收机向转向器发出信号,通过电路板上的IC驱动无核心马达开始转动,通过减速器将动力传递给摆臂,通过位置检测器传递回信号,判断是否已经到达定位。位置检测器实际上是一个可变电阻器,转动伺服时,电阻值会发生变化。通过检测电阻值,可以知道旋转角度。舵机有两个主要的性能指标:扭力和转速,齿轮组和电机关乎这两个性能的重要因素。扭力是指舵机能转动或扭动产生的力有多少。在5V的电压下,标准舵机的扭力是5.5千克/厘米(75盎司/英寸)。转速是指从舵机一个角度转到另一个角度要多长时间。在5V电压下,舵机标准转速是0.2秒移动60度。标准舵机图解舵机的控制原理在了解如果控制舵机之前,需要知道PWM(PulseWidthModulation)技术,全名也叫做脉冲宽度控制技术。PWM是一种对模拟信号电平进行数字编码的方法。通过使用高分辨率计数器,方波的占空比被调制以编码特定模拟信号的电平。PWM信号仍然是数字信号,因为在任何给定时间,满量程DC电源要么完全,要么完全关闭。电压或电流源以重复脉冲序列ON或OFF施加到模拟负载。当DC电源施加到负载时,电源关闭时断电。只要带宽足够,任何模拟值都可以使用PWM进行编码。舵机控制信号为50Hz,即脉宽调制(PWM)信号的周期为20ms,脉冲宽度为0.5ms-2.5ms,相对应舵盘的位置为0-180度,线性变化。也就是说,为了给它提供一定的脉冲宽度,其输出轴将保持相应的角度,无论外部扭矩如何变化,它都会改变输出直到给出另一个宽度的脉冲信号。角度到新的相应位置。转向器内部有一个参考电路,用于产生周期为20ms,宽度为1.5ms的参考信号。同时存在一个比较器,其将所施加的信号与参考信号进行比较以确定方向和尺寸,从而产生电动机的旋转信号。我们想在树莓派上操控舵机转动,树莓派本身又无法直接输出模拟电信号,我们便可以使用PWM(脉宽调制)方法来模拟这一点。树莓派或者是其他单片机一个引脚只能输出两个特定的电平——高电平和低电平。在树莓派上大多的GPIO都是高电平3.3V,低电平0V。如果想要一个输入2V,树莓派是没有办法直接设置引脚的电平来达到输出2V的,那么我们就要使用PWM来输出介于低电平和高电平之间的电压。我们制作一个固定频率的数字信号,在那里我们将改变脉冲宽度,将“转换”改为“平均”输出电压的电平。频率本身不是重点,而是“占空比”,即脉冲“高”的时间除以波周期之间的关系。例如,假设我们在树莓派的GPIO上产生一个50Hz的脉冲频率。周期(p)将是频率的倒数或20ms(1/f)。本设计使用的TowerProSG90舵机,这是市面(淘宝)上比较常见的一款舵机转角,从0度到180度。SG90舵机有三条不同颜色的线:红线,棕线和黄线。这三条线的作用是:红线VCC,棕线GND,黄线控制线。所以我们是操控黄线来控制舵机。查看TowerProSG90文档后我们可以得知其占空比与转动角度的关系:在周期20ms下:角度占空比0.5ms0度;2.5%1.0ms45度;5.0%1.5ms90度;7.5%2.0ms135度;10.0%2.5ms180度;12.5%占空比关系图舵机的安装通过在淘宝购置相应的塑料云台,以及购买的舵机配套的塑料摆臂,用钳子对一些过长的摆臂进行长度的修改,用螺丝组装在一起,一个拥有两自由度的舵机便可以搭设完成。因为摄像头有一定重量,只靠塑料云台的重量压不住摄像头,于是笔者再取用一块小的万能电路板于云台底部组装在一起,加重底部重量,当舵机转动时,保证舵机本身不会被甩动。最终摄像云台成品图如下:注意的是,为了方便定位舵机,笔者在把两个舵机组装进去之前,先直接接入树莓派,用编程驱动把两个舵机的角度都调在90度的位置(控制代码在下面介绍),然后再调正云台。这样的舵机初始位置比较好操控和辨别。舵机的操控通过编程树莓派GPIO口,提供电源,可以接地,输出控制,三个引脚实现对某一个舵机转动的操控。树莓派的GPIO引脚集成在树莓派的电路板上。用户或开发者可以控制它们的行为,以允许它们从传感器读取数据,并控制LED,电机和显示器等组件。老型号的树莓派有26个GPIO引脚,而较新的型号例如笔者使用的树莓派3B+都有40个。下图是树莓派40Pin引脚图,其中有些IO口如0V和5V这种无法用于编程。操作GPIO引脚的话,python代码中需指定以什么模式查找引脚编号,不同模式对应的引脚编号,如上图写着的Physical,如果以Physical模式的引脚号去查找,对应就是GPIO.setmode(GPIO.BOARD)。如果以BCM模式的引脚号去查找,代码即是GPIO.setmode(GPIO.BCM)。对于两个舵机,我分别选用了BCM模式下6和13的IO口,通过公母杜邦线接入到树莓派。如图:下面编写对舵机操控的测试代码,让某一个舵机来回不停地从0度旋转到180度。从上面的分析可以得知改变占空比的话,0度对应2.5,180度对应12.5,那么传入ChangeDutyCycle()的参数数值就在2.5-12.5之间。具体代码如下:#-*-coding:utf-8-*-#!/usr/bin/envpythonimportRPi.GPIOasGPIOimporttimeimportsignalimportatexitatexit.register(GPIO.cleanup)servopin=6GPIO.setmode(GPIO.BCM)#选用BCM模式GPIO.setup(servopin,GPIO.OUT,initial=False)p=GPIO.PWM(servopin,50)#50HZp.start(0)#time.sleep(2)while(True):foriinrange(0,181,3):#以3度为变化的间隔p.ChangeDutyCycle(2.5+10*i/180)#设置转动角度time.sleep(0.02)#等该20ms周期结束p.ChangeDutyCycle(0)#归零信号time.sleep(0.02)foriinrange(181,0,-3):#循环进行p.ChangeDutyCycle(2.5+10*i/180)time.sleep(0.02)p.ChangeDutyCycle(0)time.sleep(0.02)至此,舵机正常运行,在下文的代码中,会根据特定指令调整舵机方向,则需要往程序中传入参数。这部分代码会下文中提及。我们的视频流服务器和摄像云台已经架设起来,树莓派本机只要输入网址或者同一局域网下的其他设备输入该网址都可以访问。但为了外网设备也可以远程访问到局域网内的服务器,我们需要用到内网穿透技术。内网穿透部分内网穿透简介内网穿透(NATtraversal),也叫做NAT穿透。内网穿透主要解决了TCP/IP中的一个常见问题:使用了NAT设备的私有TCP/IP网络中的主机之间如何建立连接。目前有许多用于内联网渗透的技术,但它们都不是毫无缺点,因为内网穿透的行为是没有一个标准。这些技术中的每一种通常都具有外部网络可访问的云服务器,并且服务器使用可以从世界上任何地方访问的IP地址。内网穿透常用的工具有:Ngrok,Natapp,Frp,Lanproxy,Spike,花生壳。本设计中选用有提供中文技术文档的frp。 对于本设计,主要就是将在局域网内的视频流服务器暴露到外网,通过在云端服务器开启端口,再对接到本地视频服务器的端口,再设置一个外部访问云服务器的端口,即可实现外网收看内网视频流的效果。frp在服务端的安装配置首先从github上下载frp的release版本,阿里云服务器(服务端)和树莓派(客户端)都要安装,同时确保两边安装的版本是一致的。然后,在阿里云服务器上配置端口,除去已有的端口,笔者新增了三个端口,分别是7000,6081,9999,协议都是TCP。如图所示:我们可以尝试先直接访问6081这个端口,当然现在是不可用的。(48是我服务器的公网IP)解压目录,有6个文件,分别是frpc、frpc_full.ini和frpc.ini属于客户端用到的文件,本设计中即在树莓派中要用到;frps、frps_full.ini和frps.ini属于服务端用的文件,本设计中即在阿里云服务器中要用到。我们将frps及frps.ini放到具有公网IP(阿里云服务器)的机器上。将frpc及frpc.ini放到处于内网环境(树莓派)的机器上。所以在阿里云服务器上面,我们只需要配置frps.ini文件就行,打开编辑如下内容:[common]bind_port=7000vhost_http_port=60817000和6081都是笔者在服务器新开设的端口,7000会作为服务器和树莓派通信的一个端口,6081端口是服务器暴露在外面供访问的端口。随后启动启动frps,在frp的目录下运行以下命令:./frps-c./frps.ini此时再次访问48:6081,浏览器返回的页面应该如下图,证明服务端已经成功运行,但是客户端还没有相应的配置好,故frp返回页面找不到的提示信息,是正常的反馈提示。frp在客户端的安装配置树莓派客户端也是相同的配置方法,修改frpc.ini文件,已知我服务器的公网IP为48,那么在文件中,配置如下:[common]server_addr=48server_port=7000[ssh]type=tcplocal_port=22remote_port=6000[web_cam]type=httplocal_port=8080custom_domains=48可以看到common里面的server_port配置的便是7000,于frps配置中的bind_port=7000相对应。在web_cam里面,type为hhtp,local_port为8080,是本地视频服务器的访问端口。custom_domains属性是服务器的域名网址(如),如果该服务器有申请域名的话,但本设计中笔者并没有为该服务器再额外购买域名,则该属性仍然填上服务器的公网IP地址。同样,在frp的目录下运行以下命令:./frpc-c./frpc.ini如果客户端已经正确配置并且运行的话,会得到以下信息:总体结构图和运行测试frp架构图web_cam已成功开启proxy外网访问到视频流已经实现移动端App的编写Android系统简介Android是大家熟识的基于Linux内核、开源移动操作系统,由Google的开放手持设备联盟引领并且进行长期开发和维护。主要应用于智能手机和、平板电脑与其他便携式设备和触摸屏设备。在2010的时候,Android操作系统已经崭露头角,非常火热,超越当时的诺基亚Symbian系统,成为全球最大的智能手机操作系统。Android系统版本从2008年最初的1.0,一路更新到现在的9.0版本,在系统安全,权限管理,功能特性上,有越来越多的更新和支持。在Android编程中,涉及到的编程语言有C,Java,Kotlin,Python等。Android系统架构Android系统大致划分为四层架构:Linux内核层、系统运行库层、应用框架层和应用层。其中内核层,系统运行库层,应用框架层对于常规app开发者来说接触的地方不多,对于ROM开发者或底层开发者可能比较常用,相对来说编程开发难度也比较大,需要对整个系统架构有比较深的认识才能据此进行二次开发。对于我们常规的app开发者来说,需要更注重应用层的东西。所谓应用层,我们安装在手机上或者买来手机就自带的所有应用程序,都属于该层。例如系统一般都自带有联系人app、拨号盘app,短信app等,或是我们自己在应用市场下载的app,亦或是开发人员开发的应用程序。Android系统架构图AndroidApp入门对于本设计的App,即属于上面提及到的应用层,大多数时候的开发也是基于这一层进行的。对于Android应用的开发,开发者使用的IDE(集成开发环境)有Eclipse和AndroidStudio。不过Eclipse已经过时很久,Google很早就停止了对Eclipse的支持,Google官方开发的AndroidStudio虽然早期有较多问题,但现在已经是很成熟的一个IDE。安装完开发AndroidStudio,开发Android应用还需要JDK,AndroidSDK,安装过程在此不赘述,而且安装AndroidStudio的时候有提供这些选项,基本可以一步到位。在新建完一个新的Android项目后,可以看到项目目录如图:项目的目录有很多,大的目录可以看到有manifests,java,generatedJava,res和Gradle。manifests存放AndroidManifest.xml,这个文件是项目配置的清单文件,我们在这个清单文件中注册我们定义的四大组件,同时也需要给应用程序添加各种各样的权限(如网络权限,存储读写权限)。java目录是我们存放所有java代码的地方(当然现在Android开发也支持使用Kotlin语言开发,但本设计使用java),该目录是我们会主要使用的一个目录。generatedJava是IDE为我们自动生成的java文件,开发中我们并不需要对它做改到。res目录下则存放应用程序所需要的布局文件,各种资源,如图片,字符串的国际化等。Gradle是AndroidStudio用来构建项目的工具,基于Groovy的领域特定语言来声明项目设置,抛弃了传统基于XML的配置方式。对于Android开发,需要大致了解下Android有四大组件,分别为activity、service、contentprovider、broadcastreceiver。应用程序中,一个Activity通常就是独立的可见视图,它上面可显示一些控件,可以监听并处理用户的单击、长按等点击事件。如果在Activity之间传递数据或一个Activity想启动另外的Activity,需要用到Intent。一个service,相比Activity来说,一般拥有更长生命周期,但是用户是看不到界面的,service可用来开发后台类程序。典型的例子有后台播放语音导航信息的地图app或者正在后台下载视频的下载器。地图导航app中的某个activity在被用户按home键之后,可调用Context.startService()来启动一个service,从而保持虽然app不可见,但是后台进行着导航路线的计算,同时播放导航语言。系统也将保持这个service一直执行,直到这个service运行结束。另外,开发中也可以调用Context.bindService()方法,绑定到另外的service上(如果该service未运行则将启动它)。很多是市面上的流氓app,特别是国产应用,如百度全家桶那些,很有可能开了很多进程或服务相互保活,一个被kill了,另外一个还在,并且重新激活刚被kill的服务,所以会极大增大android手机的耗电量。当连接到一个service之后,还可以service提供的接口与它进行通讯。注意这里有一个对应关系,如果启动服务用的是startService()方法,那要停止该服务就要用Context.stopService()方法。ContentProviders管理对结构化数据集的访问。它可以封装这类数据,并提供类型安全的数据机制。Contentproviders是标准的接口,它能将一个线程中的数据与其他线程中的运行的代码进行连接。也就是说Contentproviders支持IPC(跨进程间)的访问。当我们想要在从Contentprovider中拿到自己开发应用的数据或是其他app的数据,需要使用ContentResolver对象与Contentprovider的子类对象相连接。该provide对象会接收数据,resolver对象会响应请求,返回结果。BroadcastReceiver是一种在android开发中普遍用的传播信息的机制,可以在不同应用程序之间传播。主要用来监听系统或者应用发出的广播信息,像上文提到的流氓app进程长驻,互相保活需要用的就是注册广播,在有一个进程被杀死时,通知另一个进程,另一个进程根据广播信息做相应的逻辑或业务处理,例如再次激活被杀死进程。BroadcastReceiver正常来讲用来传输较小型的数据。例如,android开机时,开机了是一种信息,很多程序要做到开机自启动服务,就需要去监听“开机”这一广播信息。还有如网络状态切换,如Wi-Fi断开,变成4G数据网络在连接,直播类app可能就会弹出提示,通知用户是否继续使用数据流量观看直播。BroadcastReceiver并没有可视化界面,但是当它收到某个通知或信息之后,BroadcastReceiver可以通过推送一条通知,启动某个服务,或启动某个Activity,达到提醒用户的目的。activity生命周期本设计中,主要用到的四大组件就是activity。在此,需要着重介绍下activity的生命周期。对于某一个Android应用,对用户来说,最直观就是看到android应用的界面,或者说可见的窗口。与用户直接进行交互的,一般也就是Activity。对于android应用中每一个Activity,都必须要在AndroidManifest.xml配置文件中声明,否则系统无法启动该Activity,因为识别不了。当只有一个单一activity的app启动时,activity的生命周期的流程如下:正常情况下,一个活动启动到结束会执行的方法依次为:onCreate()onStart()onResume()onPause()onStop()onDestroy()(1)onCreate:这是Activity生命周期进行的第一个方法,也是我们在android开发中非常常用、以及很必需的生命周期方法。它本身进行Activity的一些初始化工作,比如使用setContentView加载布局,对一些控件和变量进行初始化等。此时Activity还在后台,不可见。(2)onStart:因为一个东西创建出来,还不一定启动了,activity也是如此。这是继onCreate之后activity生命周期会执行的第二个方法,但是启动后此时对用户来说暂时是看不到的,用户无法点击触控,无法交互Activity。我们可以将一些Activity的初始化工作放在这个方法里面,但是初始化工作放在onCreate方法中仍然是更常见的做法以及开发习惯。(3)onResume:resume是继续,从字面意思理解,也可以知道这个方法的用处。此时Activity经过前两个阶段的初始化工作已经完成。Activity在这个阶段已经出现在前台,用户可以见到。(4)onPause:pause当Activity被切出去,例如用户点击了home键,或者用户自己的操作,要跳到另一个Activity,亦或是用户点击返回键,应用正常退出时都会执行这个方法。此时Activity在前台而且对用户来说还是可见的。在这个方法里面开发者简单地保存下数据,但必须是小型数据。因为onPause方法必须在半秒内没有执行完成,否则Activity会被强制关闭。高级优化开发时,可以多关注这个方法。(5)onStop:停止,此时Activity已经不可见了,但是具体某个Activity的实例仍然占用着内存空间,内存还没有被释放。这个方法的主要工作是做一些资源的回收工作。(6)onDestroy:这个阶段Activity被销毁,对用户来说已经不可见了,可以释放资源。(7)onRestart:Activity在这时再次可见,可能会触发的场景如:重新当用户按Home键切出app后又切回来。编写App界面对于我们app的界面,首先必不可少的就是接收视频的一个view,我们看到视频中的画面,想要移动画面时,界面中也需要有上下左右这4个方向按钮,通过点击这4个按钮来操控舵机,同时移动画面。App的最终截图大致如此:对于实现这样的效果,笔者选择根布局为线性布局,5个按钮的形式以gridlayout的形式组合起来,嵌套在根布局中,和观看视频的自定义view是同一个层级。界面代码大致如下:<LinearLayoutxmlns:android="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><com.noname.cameraviewer.MjpegViewandroid:id="@+id/cameraview"android:layout_width="match_parent"android:layout_height="0dp"android:background="#000"/><GridLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:layout_weight="1"android:columnCount="3"android:rowCount="3"><Buttonandroid:id="@+id/bt_capture_img"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_row="1"android:layout_column="1"android:text="@string/capture"/><Buttonandroid:id="@+id/bt_up"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_row="0"android:layout_column="1"android:text="@string/up"/><Buttonandroid:id="@+id/bt_down"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_row="2"android:layout_column="1"android:text="@string/down"/><Buttonandroid:id="@+id/bt_left"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_row="1"android:layout_column="0"android:text="@string/left"/><Buttonandroid:id="@+id/bt_right"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_row="1"android:layout_column="2"android:text="@string/right"/></GridLayout></LinearLayout>布局中我们使用了自定义view,关于自定义view的java代码会在下文提及,这里先直接引入,引入的时候需要全包名加类名的方式来使用这个view。同时对所有view都添加一个id,id的作用是为了在java代码中能查找到这些view,才能操控这些view。例如点击事件触发时,判断是哪个按钮被点击,或者对mjpegview的设置。gridlayout的使用,一开始是设置为纵3列,横3列,即刚好是一个九宫格的布局。在本界面中,我们只有5个按钮,对应的按钮位置设置,例如UP按钮,设置android:layout_row="0"和android:layout_column="1",因为这两个属性的起始值都是从0开始,0是代表了第一行或第一列。在界面代码中,gridlayout的节点设置了android:layout_weight="1"的属性,在mjpegview不用设置,该属性默认便为0。这样做的目的是让控制按钮的部分在始终能在activity中显示出来,因为在小屏幕设备上,可能由于高度不够,先渲染了mjpegview后,下面的按钮也可以显示出来,但会显示不全,底部按钮可能在activity之外,无法点击。那么,layout_weight属性在线性布局中,表示了某布局在该线性布局中的占比。因为gridlayout为1,观看视频的mjpegview占比为0,那会优先渲染gridlayout,确保gridlayout可以始终先占有空间,gridlayout显示完成后,再根据屏幕剩余的空间去加载视频view,而视频的view我们可以通过传进去的参数调整分辨率或view高度来控制。HTTP工具类的编写在app中我们通过HTTP请求方法中的POST方法,向树莓派发送命令。那么,我们写一个简单的HTTP工具类对网络操作进行封装。把该类命名为HttpUtils。因为是工具类,方法都定义为static类型,可以直接调用。里面编写三个方法:postData(Map<String,String>params,Stringencode,StringtargetUrl)调用该方法发送POST请求到服务器并返回服务器信息。参数params是请求体内容,encode是参数编码格式,targetUrl是目标URL。方法返回的是服务器的返回信息。getRequestData(Map<String,String>params,Stringencode)调用该方法来包装请求信息。参数param是请求体的参数,encode是编码格式,方法返回的是请求体信息。handleResponseResult(InputStreaminputStream)调用该方法来处理服务器的响应结果(将输入流转换为字符串)。参数inputStream是服务器的响应输入流,方法返回的是服务器响应的结果字符串。该工具类完整代码详见附录。activity代码的编写MainActivity实现View.OnClickListener接口,实现接口,就要重写接口里定义的方法,在这里具体需要实现的是onClick(Viewview)方法,响应app内所有按钮的点击事件。然后定义对应的IP地址(这里我们先直接填写树莓派内网IP作为测试),以及四个String常量,即我们最终POST到树莓派的指令,树莓派通过判断哪个字母进行舵机哪个方向的转动。privatefinalstaticStringSERVO_UP="U";privatefinalstaticStringSERVO_DOWN="D";privatefinalstaticStringSERVO_LEFT="L";privatefinalstaticStringSERVO_RIGHT="R";privateStringcommand;//传递方向指令的参数privateStringservoUrl="78:5000/";接下来在onCreate方法中,执行加载布局文件的方法setContentView()。查找到布局中所有view的id,查找到这些按钮。如对UP按钮的设置:ButtonbtUp=findViewById(R.id.bt_up);btUp.setOnClickListener(this);同时对视频view进行参数设置,这些参数在view中定义为public方法,所以外部可以访问到。cameraView.setAdjustHeight(true);cameraView.setMode(MjpegView.MODE_FIT_WIDTH);cameraView.setUrl("78:8080/?action=stream");cameraView.setRecycleBitmap(true);同时,我们再重写activity方法中的onResume,onPause,onStop,分别执行的camerview的开启流和停止流的方法。@OverrideprotectedvoidonResume(){cameraView.startStream();super.onResume();}@OverrideprotectedvoidonPause(){cameraView.stopStream();super.onPause();}@OverrideprotectedvoidonStop(){cameraView.stopStream();super.onStop();}我们再定义控制舵机转动的方法,注意因为该方法涉及到网络操作。而在android系统中,网络请求被认为是一个耗时的操作,如果直接就在UI主进程执行的话,是会抛出异常的。所以在activity执行该方法时,需要在开启一个子线程,包住方法代码。按钮点击后调用controlServo(),controlServo()会开启一个新线程,然后我们调用工具类HttpUtils中的静态方法postData(),该方法会向服务器提交表单,表单中只有一个字段ctrl(与之对应的服务器代码在下面介绍),即代码中的params.put("ctrl",command),去传递代表舵机转动方向的字符串数据给服务器,同时在log中打印出服务器返回给我们的响应结果。publicvoidcontrolServo(){newThread(newRunnable(){@Overridepublicvoidrun(){Map<String,String>params=newHashMap<String,String>();params.put("ctrl",command);try{Stringpost_result=null;post_result=HttpUtils.postData(params,"utf-8",servoUrl);Log.i("POST_RESULT",post_result);}catch(MalformedURLExceptione){e.printStackTrace();}}}).start();}最后重写onClick方法,通过swich语句加view.getId()方法判断哪个按钮被点击,对command进行相应赋值。@OverridepublicvoidonClick(Viewview){switch(view.getId()){caseR.id.bt_capture_img:break;caseR.id.bt_up:command=SERVO_UP;controlServo();break;caseR.id.bt_down:command=SERVO_DOWN;controlServo();break;caseR.id.bt_left:command=SERVO_LEFT;controlServo();break;caseR.id.bt_right:command=SERVO_RIGHT;controlServo();break;}}}至此,activity代码介绍基本完成。自定义view的编写笔者新建一个了自定义view来播放视频。该view可以接收url参数,可以设置填充的模式(如原始的画面比例,或者是高度适应,或者是宽度适应)。这里的技术要点是以Bitmap对象保存视频当前帧的图像,并通过一个继承Thread的内部类,不断更新该bitmap对象,实现当前帧不断更新,达到视频实时更新呢和播放的效果。本项目中,直接在MainActivity同个包目录下新建class(本项目工程量比较小,如果实际项目比较大,应该划分好不同的包,作为一个好的开发习惯。如自定义view应该新建一个包名为view,并将该class放在该目录下)。对于自定义view,首先就是要继承父类View。该自定义view定义的类成员变量主要有:privateContextcontext;privateStringurl;privateBitmaplastBitmap;privateMjpegDownloaderdownloader;privatefinalObjectlockBitmap=newObject();privatePaintpaint;//定义绘制view的画笔privateRectdst;privateintmode=MODE_ORIGINAL;//默认模式privateintdrawX,drawY,vWidth=-1,vHeight=-1;privateintlastImgWidth,lastImgHeight;privatebooleanadjustWidth,adjustHeight;privatebooleanisRecycleBitmap;privatebooleanisUserForceConfigRecycle;根据变量名,可以得知该变量对应的作用。对于每一个view,无论是android系统提供的还是我们自己实现的,Android系统都是会去执行view类里三个重要的方法,这三个方法让view在屏幕上计算位置,绘制渲染,在app中显示出来。这三个方法是onMeasure()、onLayout()和onDraw()。我们自定义view时,一般也需要重写这三个很重要的方法。measure过程根据View的类型分为两种情况:单一View时:只测量自身一个View;ViewGroup时:对ViewGroup视图中所有的子View都进行测量。本设计继承的是view,即单一view的情况。单一View的measure过程如下示:measure():基本测量逻辑的判断;调用onMeasure()。onMeasure方法代码比较多,MjpegView的完整代码详见附录。measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。不过对该view来说,这里的场景不需要重写onLayout方法,接着继续重写onDraw方法:@OverrideprotectedvoidonDraw(Canvasc){synchronized(lockBitmap){//同步该代码块,阻塞其他访问该对象的线程if(c!=null&&lastBitmap!=null&&!lastBitmap.isRecycled()){if(isInEditMode()){}elseif(mode!=MODE_ORIGINAL){c.drawBitmap(lastBitmap,null,dst,paint);//绘制bitmap}else{c.drawBitmap(lastBitmap,drawX,drawY,paint);}}else{Log.d(tag,"Willnotdraw,canvasisnullorbitmapisnotreadyyet");}}}内部类MjpegDownloader中重写run方法,涉及到网络操作,用BufferedInputStream接收输入流,以及还有对请求header的正则匹配操作。内部类代码比较多,完整代码见附录。App发送指令操控舵机通过post命令,树莓派再次借助frp这一内网穿透的服务,同时再开启一个服务器,同时暴露出一个端口,用来接收来自手机端的POST命令。那么为了再运行一个本地服务器
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 个人借贷抵押合同范例
- 一般买房合同范例
- 城乡小学美术教育对比调查研究
- 石材机械相关行业投资规划报告
- PFA相关行业投资方案
- 工作要求对幼儿教师职业幸福感的影响-职业使命感和职业自我效能感的链式中介作用
- 适应高一生活
- 圣诞社区活动策划
- 暖通中央空调水机常见案例解析
- 房地产开发工程部机电工程师述职报告
- 户主变更协议书
- 2024年阜阳职业技术学院单招职业适应性测试题库附答案
- 《打草惊蛇》课件
- 围手术期管理课件
- 虾皮shopee新手卖家考试题库及答案
- 公路隧道竖井施工技术规程(征求意见稿)
- 五年级口算1000题(打印版)
- 《孔乙己》教案(4篇)
- 铝合金压铸件PFMEA分析
- 2023年全国高考体育单招考试英语试卷试题真题(精校打印版)
- 经络及任督二脉
评论
0/150
提交评论