眼看要过年了,回老家之后,养的小鱼用不了几天就要见马克思,想着用朋友送的zero来做一个远程喂鱼的小东西,应该不难。
思路:利用双路继电器分别控制灯和水泵,使用mjpg-streamer来获取摄像头的视频流,并在特定的时刻自动开闭继电器。
网络环境:有公网IP的家庭网络,利用路由器的ddns或者花生壳,树莓派作为tcpserver对外提供访问。但这个条件,目前已经很难满足了,一般网络都是大内网,这种情况可以让树莓派作为tcpclient主动请求服务器获取指令,本文介绍的是第一种情况。
鱼食槽暂时未完成,准备搞两个大一点的瓶盖,合起来热熔胶伺候,中间放鱼食,边缘开两个孔,最终固定到步进电机上,转一圈就能完成喂鱼动作。
树莓派的安装和配置,本文不再赘述,本文分“硬件部分”、“软件部分”、“自启动配置”来说明整个项目。
硬件部分
本项目中使用的硬件:
必不可少的大脑:
1. 双路继电器
使用 gpio readall 指令来获取树莓派上的所有接口信息。
这里使用BCM方式来控制GPIO接口,选择BCM编号为18和27的插针,也就是GPIO1和GPIO2,作为两路继电器的信号控制,继电器的vcc和gnd,分别接到树莓派的5V和0V接口,先借个图,看起来清晰一点。
2. 步进电机及ULN2003控制模块
步进电机利用4步或8步脉冲信号来驱动电机转动,这里用双4步(ab bc cd da)来控制电机,可以获得比较强的扭矩,同时精度也比单4步要好,这个ULN2003控制模块有个缺点,就是控制间隔不能小于3ms,否则电机只震动,不转动。
连接也很简单,正负极接到zero上,控制脚使用BCM编号为23 24 25 12的针脚,BCM编号见第一张图。
3. 兼容的USB摄像头
直接扔到usb集线器上就完事了,树莓派上使用lsusb查看,如果没有,基本是不兼容导致的。
4. 兼容树莓派的USB无线网卡
5. USB集线器
软件部分
软件也是主要三大块:
1. 继电器控制、定时控制、步进电机控制 (代码文件保存到/home/pi/scripts/MyTcpControl.py)
2. 摄像头实时视频流部署 (启动视频流服务的脚本保存到/home/pi/scripts/startCamera.sh)
3. 安卓远程控制APP>
1. 双路继电器控制、自动定时控制、步进电机控制
本模块使用Python语言编写。
- 建立TCP服务器,通信端口为7654
- 高低电平控制
由于使用的继电器写低为接通电路,所以代码中,使用GPIO.LOW来接通继电器电路,GPIO.HIGH来关闭继电器电路。 - 电机步进序列控制。
步进电机使用双4步来控制GPIO的电平信号,具体为:
1,1,0,0 0,1,1,0 0,0,1,1 1,0,0,1
MyTcpControl.py完整代码如下
import sys import os import _thread import time import datetime from socket import * import RPi.GPIO as GPIO host = '0.0.0.0' port = 7654 buffsize = 4096 ADDR = (host,port) channel1 = 18 channel2 = 27 IN1 = 23 IN2 = 24 IN3 = 25 IN4 = 12 lightManual = False pumpManual = False lightStatus = 0 pumpStatus = 0 def main(): GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) GPIO.setup(channel1,GPIO.OUT,initial=GPIO.HIGH) GPIO.setup(channel2,GPIO.OUT,initial=GPIO.HIGH) GPIO.setup(IN1,GPIO.OUT) GPIO.setup(IN2,GPIO.OUT) GPIO.setup(IN3,GPIO.OUT) GPIO.setup(IN4,GPIO.OUT) _thread.start_new_thread(autoControlLight, ("light",1)) _thread.start_new_thread(autoControlPump, ("pump",1)) server = socket(AF_INET,SOCK_STREAM) server.bind(ADDR) server.listen(10) print("MyControl TcpServer is started") while True: try: client,addr = server.accept() _thread.start_new_thread(onAccept, (client,addr)) except: print('Server is interrupted') #server.close() #server.shutdown() def autoControlLight(tName,para): global lightManual global lightStatus while True: timeNow1 = datetime.datetime.now() h = timeNow1.hour m = timeNow1.minute if h==0 and m==0: lightManual = False if h==8 and m==0 and lightManual==False: GPIO.output(channel1,GPIO.LOW) lightStatus = 1 if h==17 and m==0: GPIO.output(channel1,GPIO.HIGH) lightStatus = 0 time.sleep(60) def autoControlPump(tName,para): global pumpManual global pumpStatus while True: timeNow2 = datetime.datetime.now() h = timeNow2.hour m = timeNow2.minute if h==0 and m==0: pumpManual = False if h==8 and m==0 and pumpManual==False: GPIO.output(channel2,GPIO.LOW) pumpStatus = 1 if h==17 and m==0: GPIO.output(channel2,GPIO.HIGH) pumpStatus = 0 time.sleep(30) def opDrive(): forwardDrive(0.008,512) stopDrive() def onAccept(sock, addr): recvData = sock.recv(buffsize).decode('gbk') print('recvData:'+recvData) #print data retInfo="" global lightManual global lightStatus global pumpManual global pumpStatus try: if recvData=="open_close": retInfo = "opDrive success" sock.send(retInfo.encode('gbk')) sock.close() opDrive() else: if recvData=="open1": GPIO.output(channel1,GPIO.LOW) lightManual = True lightStatus = 1 retInfo = "light 1" elif recvData=="close1": GPIO.output(channel1,GPIO.HIGH) lightManual = True lightStatus = 0 retInfo = "light 0" elif recvData=="open2": GPIO.output(channel2,GPIO.LOW) pumpManual = True pumpStatus = 1 retInfo = "pump 1" elif recvData=="close2": GPIO.output(channel2,GPIO.HIGH) pumpManual = True pumpStatus = 0 retInfo = "pump 0" elif recvData=="reboot": os.system("sudo reboot") retInfo = "reboot success" elif recvData=="getStatus": retInfo=str(lightStatus)+","+str(pumpStatus) elif recvData=="test": retInfo="test ok" sock.send(retInfo.encode('gbk')) sock.close() except Exception as err: retInfo = str(err) sock.send(retInfo.encode('gbk')) sock.close() def setStep(w1,w2,w3,w4): GPIO.output(IN1,w1) GPIO.output(IN2,w2) GPIO.output(IN3,w3) GPIO.output(IN4,w4) def stopDrive(): setStep(0,0,0,0) def forwardDrive(delay,steps): for i in range(0,steps): setStep(1,1,0,0) time.sleep(delay) setStep(0,1,1,0) time.sleep(delay) setStep(0,0,1,1) time.sleep(delay) setStep(1,0,0,1) time.sleep(delay) if __name__ == '__main__': main()
2. 摄像头实时视频流部署
尝试了motion组件,发现巨卡,转而使用mjpg-streamer,很流畅,推荐使用!
(1)安装依赖库
sudo apt-get install libjpeg62-dev sudo apt-get install libjpeg8-dev
(2)树莓派浏览器访问https://github.com/jacksonliam/mjpg-streamer 下载源码,默认到/home/pi/Downloads目录,完成后解压缩。
由于市面上大部分摄像头是YUYV格式输出,所以要修改mjpg-streamer项目的代码文件,让其默认支持此格式的摄像头。
使用nano指令,或TextEditor打开mjpg-streamer-experimental/plugins/input_uvc/input_uvc.c这个文件,找到input_init函数,修改
“format = V4L2_PIX_FMT_MJPEG” 为
“format = V4L2_PIX_FMT_YUYV”。
(3) 编译、部署mjpg-streamer项目
sudo apt-get install cmake cd /home/pi/Downloads/mjpg-streamer-master/mjpg-streamer-experimental sudo make clean all
编译完成后,复制相关文件到指定目录
sudo cp mjpg_streamer /usr/local/bin sudo cp output_http.so input_uvc.so /usr/local/lib/ sudo cp -R www /usr/local/www
最后,使用指令来启动视频组件
LD_LIBRARY_PATH=/usr/local/lib mjpg_streamer -i "input_uvc.so -r 320x240 -f 12" -o "output_http.so -p 12001 -w /usr/local/www"
在谷歌浏览器中,就可以看到视频了,预览地址为 http://树莓派IP:12001/?action=stream
3. 安卓远程控制APP
使用Android Studio作为IDE,利用webview控件作为人机交互,简单快速。
(1) fish.html文件,放入assets目录
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" href="/favicon.ico" /> <link rel="bookmark" href="/favicon.ico" type="image/x-icon" /> <title>远程喂鱼</title> <link rel="shortcut icon" href="favicon.ico"> <link href="css/bootstrap.min.css?v=3.3.6" rel="stylesheet"> <link href="css/font-awesome.css?v=4.4.0" rel="stylesheet"> <link href="css/animate.css" rel="stylesheet"> <link href="css/style.css?v=4.1.0" rel="stylesheet"> </head> <body class="gray-bg"> <div class="wrapper wrapper-content" style="padding:10px;"> <div class="row"> <div class="col-sm-4"> <div class="ibox float-e-margins" style="margin-bottom:5px;"> <div class="ibox-content no-padding"> <div class="panel-body"> 8:00自动开灯和水泵,17:00自动关灯和水泵 </div> </div> </div> </div> </div> <div class="row"> <div class="col-sm-4"> <div class="ibox float-e-margins" style="margin-bottom:5px;"> <div class="ibox-title"> <h5>实时视频</h5> </div> <div class="ibox-content no-padding"> <div class="panel-body"> <img style="width:100%;height:240px;" src="http://树莓派IP:12001/?action=stream" /> </div> </div> </div> </div> </div> <div class="row"> <div class="col-sm-4"> <div class="ibox float-e-margins" style="margin-bottom:5px;"> <div class="ibox-content no-padding"> <div class="panel-body" style="text-align:center;"> <button id="lightBtn" class="btn btn-w-m btn-success" type="button"></button> <button id="pumpBtn" class="btn btn-w-m btn-success" type="button"></button> <!--<button class="btn btn-w-m btn-success" type="button" onclick="control('resetvideo')">重启视频</button> --> <button class="btn btn-w-m btn-success" type="button" onclick="control('reboot')">重启控制器</button> <button id="fishBtn" class="btn btn-w-m btn-success" type="button" onclick="control('open_close')">喂食</button> </div> </div> </div> </div> </div> </div> <script src="js/jquery.min.js?v=2.1.4"></script> <script src="js/bootstrap.min.js?v=3.3.6"></script> <script> function control(op) { if (op == "open_close") $("#fishBtn").removeClass("btn-success").addClass("btn-default").attr('disabled', 'disabled'); var ret = ""; if (op == "resetvideo") { if (confirm("确定要重启视频模块吗?")) { ret = window.JSHook.execTcpCmd(op); } } else if (op == "reboot") { if (confirm("确定要重启控制器?")) { ret = window.JSHook.execTcpCmd(op); } } else window.setTimeout(function () { ret = window.JSHook.execTcpCmd(op); controlCallback(op, ret); }, 0); } function controlCallback(op, ret) { if (op == "getStatus") { var lightStatus = ret.split(",")[0]; var pumpStatus = ret.split(",")[1]; if (lightStatus == "1") $("#lightBtn").removeClass("btn-default").addClass("btn-success").text("关灯").unbind("click").click(function () { control("close1"); }); else $("#lightBtn").removeClass("btn-success").addClass("btn-default").text("开灯").unbind("click").click(function () { control("open1"); }); if (pumpStatus == "1") $("#pumpBtn").removeClass("btn-default").addClass("btn-success").text("关水泵").unbind("click").click(function () { control("close2"); }); else $("#pumpBtn").removeClass("btn-success").addClass("btn-default").text("开水泵").unbind("click").click(function () { control("open2"); }); } else if (op == "open1" && ret == "light 1") { //开灯 $("#lightBtn").removeClass("btn-default").addClass("btn-success").text("关灯").unbind("click").click(function () { control("close1"); }); } else if (op == "close1" && ret == "light 0") {//关灯 $("#lightBtn").removeClass("btn-success").addClass("btn-default").text("开灯").unbind("click").click(function () { control("open1"); }); } else if (op == "open2" && ret == "pump 1") {//开水泵 $("#pumpBtn").removeClass("btn-default").addClass("btn-success").text("关水泵").unbind("click").click(function () { control("close2"); }); } else if (op == "close2" && ret == "pump 0") {//关水泵 $("#pumpBtn").removeClass("btn-success").addClass("btn-default").text("开水泵").unbind("click").click(function () { control("open2"); }); } else if (op == "open_close" && ret == "opDrive success") { alert("喂食成功"); $("#fishBtn").removeClass("btn-default").addClass("btn-success").removeAttr("disabled"); } } control("getStatus"); </script> </body> </html>
(2)Activity里就一个WebView组件,主窗体后端代码MainActivity.java
package com.wszhoho.viewfish; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Vibrator; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import java.lang.ref.WeakReference; import java.util.Random; public class MainActivity extends AppCompatActivity { static WeakReference<WebView> _webView; Vibrator vibrator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); Random rnd = new Random(100); int v = rnd.nextInt(); String webViewUrl = "file:///android_asset/fish.html?v=" + v; initWebView(webViewUrl); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @SuppressLint("SetJavaScriptEnabled") private void initWebView(String url) { _webView = new WeakReference<>(findViewById(R.id.webView)); //重新设置WebSettings WebSettings webSettings = _webView.get().getSettings(); webSettings.setDisplayZoomControls(false); webSettings.setSupportZoom(false); webSettings.setAppCacheEnabled(true); webSettings.setAllowFileAccess(true); webSettings.setUseWideViewPort(true); webSettings.setLoadWithOverviewMode(true); webSettings.setSaveFormData(false); webSettings.setDomStorageEnabled(true); webSettings.setSupportMultipleWindows(true); webSettings.setJavaScriptCanOpenWindowsAutomatically(true); webSettings.setJavaScriptEnabled(true); _webView.get().addJavascriptInterface(this, "JSHook"); _webView.get().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); _webView.get().canGoBack(); _webView.get().requestFocus(); _webView.get().setWebChromeClient(new WebChromeClient()); _webView.get().loadUrl(url); } @JavascriptInterface public String execTcpCmd(String op) { try { if (!op.equals("getStatus")) vibrator.vibrate(100); String ret = TcpClient.SendMsg(op); return ret; } catch (Exception ignored) { return "-1"; } } }
(3)TcpClient.java
package com.wszhoho.viewfish; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; class TcpClient { private static ReentrantLock lock = new ReentrantLock(); static String SendMsg(String msg) { lock.lock(); AtomicReference<String> retStr = new AtomicReference<>(""); new Thread(() -> { Socket client = null; try { client = new Socket(树莓派IP, 7654); BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); OutputStream os = client.getOutputStream(); os.write(msg.getBytes("utf-8")); os.flush(); retStr.set(in.readLine()); } catch (IOException e) { e.printStackTrace(); } finally { if (client != null) { try { client.close(); } catch (IOException e) { e.printStackTrace(); } } } }).start(); while (retStr.get().equals("")) { try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } lock.unlock(); return retStr.get(); } }
(4)AndroidManifest.xml权限配置
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.VIBRATE" />
自启动配置
首先更改系统默认的python运行版本:
sudo rm /usr/bin/python sudo ln -s /usr/bin/python3 /usr/bin/python
进入/home/pi/.config目录,建立autostart文件夹,进入该文件夹,建立两个后缀名为”.desktop”的文件。
camera.desktop文件,内容为:
[Desktop Entry] Type=Application Exec=/home/pi/scripts/startCamera.sh
tcpserver.desktop文件,内容为:
[Desktop Entry] Type=Application Exec=python /home/pi/scripts/MyTcpControl.py
完成后,重启树莓派,所有配置全部完成。
最终完成情况:
盒子巨丑,好在空间大,够放!
安卓APP,我家宝宝选的图标,巨喜欢 :-)
我就想问 不换水么?
鱼缸不用老换水,有过滤系统
请问你视频是如何推流跟收流的
厉害
楼主代码能否没注译,并给出原理框架?
zero,自己就带无线网卡了,还要USB网卡做什么?
请问会不会出现socket连接不上或者手机上的喂食按钮不管用的情况呀
为什么我的内网穿透后帧率只有1-2fps