找回密码
 立即注册

QQ登录

只需一步,快速开始

本帖最后由 lou 于 2020-4-30 16:33 编辑

利用树莓派Zero远程可视化喂鱼



222.png

眼看要过年了,回老家之后,养的小鱼用不了几天就要见马克思,想着用朋友送的zero来做一个远程喂鱼的小东西,应该不难。
思路:利用双路继电器分别控制灯和水泵,使用mjpg-streamer来获取摄像头的视频流,并在特定的时刻自动开闭继电器。
网络环境:有公网IP的家庭网络,利用路由器的ddns或者花生壳,树莓派作为tcpserver对外提供访问。但这个条件,目前已经很难满足了,一般网络都是大内网,这种情况可以让树莓派作为tcpclient主动请求服务器获取指令,本文介绍的是第一种情况。
鱼食槽暂时未完成,准备搞两个大一点的瓶盖,合起来热熔胶伺候,中间放鱼食,边缘开两个孔,最终固定到步进电机上,转一圈就能完成喂鱼动作。
树莓派的安装和配置,本文不再赘述,本文分“硬件部分”、“软件部分”、“自启动配置”来说明整个项目。

硬件部分

本项目中使用的硬件:
必不可少的大脑:

223.png

1. 双路继电器

使用 gpioreadall 指令来获取树莓派上的所有接口信息。
这里使用BCM方式来控制GPIO接口,选择BCM编号为18和27的插针,也就是GPIO1和GPIO2,作为两路继电器的信号控制,继电器的vcc和gnd,分别接到树莓派的5V和0V接口,先借个图,看起来清晰一点。

224.png

225.jpg

2. 步进电机及ULN2003控制模块

步进电机利用4步或8步脉冲信号来驱动电机转动,这里用双4步(ab bc cd da)来控制电机,可以获得比较强的扭矩,同时精度也比单4步要好,这个ULN2003控制模块有个缺点,就是控制间隔不能小于3ms,否则电机只震动,不转动。

226.jpg

连接也很简单,正负极接到zero上,控制脚使用BCM编号为2324 25 12的针脚,BCM编号见第一张图。

227.jpg

3. 兼容的USB摄像头

直接扔到usb集线器上就完事了,树莓派上使用lsusb查看,如果没有,基本是不兼容导致的。

228.png

229.png

4. 兼容树莓派的USB无线网卡

230.png

5. USB集线器

231.jpg

软件部分

软件也是主要三大块:
1. 继电器控制、定时控制、步进电机控制(代码文件保存到/home/pi/scripts/MyTcpControl.py)
2. 摄像头实时视频流部署(启动视频流服务的脚本保存到/home/pi/scripts/startCamera.sh)
3. 安卓远程控制APP>

1. 双路继电器控制、自动定时控制、步进电机控制

本模块使用Python语言编写。
1.     建立TCP服务器,通信端口为7654
2.     高低电平控制
由于使用的继电器写低为接通电路,所以代码中,使用GPIO.LOW来接通继电器电路,GPIO.HIGH来关闭继电器电路。
3.     电机步进序列控制。
步进电机使用双4步来控制GPIO的电平信号,具体为:

  1. 1,1,0,0
  2. 0,1,1,0
  3. 0,0,1,1
  4. 1,0,0,1
复制代码

MyTcpControl.py完整代码如下

  1. import sys
  2. import os
  3. import _thread
  4. import time
  5. import datetime
  6. from socket import *
  7. import RPi.GPIO as GPIO

  8. host = '0.0.0.0'
  9. port = 7654
  10. buffsize = 4096
  11. ADDR = (host,port)
  12. channel1 = 18
  13. channel2 = 27

  14. IN1 = 23
  15. IN2 = 24
  16. IN3 = 25
  17. IN4 = 12

  18. lightManual = False
  19. pumpManual = False
  20. lightStatus = 0
  21. pumpStatus = 0

  22. def main():
  23.     GPIO.setmode(GPIO.BCM)
  24.     GPIO.setwarnings(False)

  25.     GPIO.setup(channel1,GPIO.OUT,initial=GPIO.HIGH)
  26.     GPIO.setup(channel2,GPIO.OUT,initial=GPIO.HIGH)
  27.      
  28.     GPIO.setup(IN1,GPIO.OUT)
  29.     GPIO.setup(IN2,GPIO.OUT)
  30.     GPIO.setup(IN3,GPIO.OUT)
  31.     GPIO.setup(IN4,GPIO.OUT)
  32.      
  33.     _thread.start_new_thread(autoControlLight, ("light",1))
  34.     _thread.start_new_thread(autoControlPump, ("pump",1))

  35.     server = socket(AF_INET,SOCK_STREAM)
  36.     server.bind(ADDR)
  37.     server.listen(10)
  38.     print("MyControl TcpServer is started")
  39.     while True:
  40.         try:
  41.             client,addr = server.accept()
  42.             _thread.start_new_thread(onAccept, (client,addr))
  43.         except:
  44.             print('Server is interrupted')
  45.     #server.close()
  46.     #server.shutdown()

  47. def autoControlLight(tName,para):
  48.     global lightManual
  49.     global lightStatus
  50.     while True:
  51.         timeNow1 = datetime.datetime.now()
  52.         h = timeNow1.hour
  53.         m = timeNow1.minute
  54.         if h==0 and m==0:
  55.             lightManual = False
  56.         if h==8 and m==0 and lightManual==False:
  57.             GPIO.output(channel1,GPIO.LOW)
  58.             lightStatus = 1
  59.         if h==17 and m==0:
  60.             GPIO.output(channel1,GPIO.HIGH)
  61.             lightStatus = 0
  62.          
  63.         time.sleep(60)
  64.          
  65. def autoControlPump(tName,para):
  66.     global pumpManual
  67.     global pumpStatus
  68.     while True:
  69.         timeNow2 = datetime.datetime.now()
  70.         h = timeNow2.hour
  71.         m = timeNow2.minute
  72.         if h==0 and m==0:
  73.             pumpManual = False
  74.         if h==8 and m==0 and pumpManual==False:
  75.             GPIO.output(channel2,GPIO.LOW)
  76.             pumpStatus = 1
  77.         if h==17 and m==0:
  78.             GPIO.output(channel2,GPIO.HIGH)
  79.             pumpStatus = 0
  80.          
  81.         time.sleep(30)
  82.          
  83. def opDrive():
  84.     forwardDrive(0.008,512)
  85.     stopDrive()

  86. def onAccept(sock, addr):
  87.     recvData = sock.recv(buffsize).decode('gbk')
  88.     print('recvData:'+recvData) #print data
  89.     retInfo=""
  90.     global lightManual
  91.     global lightStatus
  92.     global pumpManual
  93.     global pumpStatus
  94.     try:
  95.         if recvData=="open_close":
  96.             retInfo = "opDrive success"
  97.             sock.send(retInfo.encode('gbk'))
  98.             sock.close()
  99.             opDrive()
  100.         else:
  101.             if recvData=="open1":
  102.                 GPIO.output(channel1,GPIO.LOW)
  103.                 lightManual = True
  104.                 lightStatus = 1
  105.                 retInfo = "light 1"
  106.             elif recvData=="close1":
  107.                 GPIO.output(channel1,GPIO.HIGH)
  108.                 lightManual = True
  109.                 lightStatus = 0
  110.                 retInfo = "light 0"
  111.             elif recvData=="open2":
  112.                 GPIO.output(channel2,GPIO.LOW)
  113.                 pumpManual = True
  114.                 pumpStatus = 1
  115.                 retInfo = "pump 1"
  116.             elif recvData=="close2":
  117.                 GPIO.output(channel2,GPIO.HIGH)
  118.                 pumpManual = True
  119.                 pumpStatus = 0
  120.                 retInfo = "pump 0"
  121.             elif recvData=="reboot":
  122.                 os.system("sudo reboot")
  123.                 retInfo = "reboot success"
  124.             elif recvData=="getStatus":
  125.                 retInfo=str(lightStatus)+","+str(pumpStatus)
  126.             elif recvData=="test":
  127.                 retInfo="test ok"
  128.             
  129.             sock.send(retInfo.encode('gbk'))
  130.             sock.close()
  131.     except Exception as err:
  132.         retInfo = str(err)
  133.         sock.send(retInfo.encode('gbk'))
  134.         sock.close()
  135.      
  136. def setStep(w1,w2,w3,w4):
  137.     GPIO.output(IN1,w1)
  138.     GPIO.output(IN2,w2)
  139.     GPIO.output(IN3,w3)
  140.     GPIO.output(IN4,w4)
  141.      
  142. def stopDrive():
  143.     setStep(0,0,0,0)
  144.          
  145. def forwardDrive(delay,steps):
  146.     for i in range(0,steps):
  147.         setStep(1,1,0,0)
  148.         time.sleep(delay)
  149.         setStep(0,1,1,0)
  150.         time.sleep(delay)
  151.         setStep(0,0,1,1)
  152.         time.sleep(delay)
  153.         setStep(1,0,0,1)
  154.         time.sleep(delay)

  155. if __name__ == '__main__':
  156.     main()
复制代码

2. 摄像头实时视频流部署

尝试了motion组件,发现巨卡,转而使用mjpg-streamer,很流畅,推荐使用!
(1)安装依赖库

  1. sudo apt-get install libjpeg62-dev

  2. 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项目

  1. sudo apt-get install cmake
  2. cd /home/pi/Downloads/mjpg-streamer-master/mjpg-streamer-experimental
  3. sudo make clean all
复制代码

编译完成后,复制相关文件到指定目录

  1. sudo cp mjpg_streamer /usr/local/bin
  2. sudo cp output_http.so input_uvc.so /usr/local/lib/
  3. sudo cp -R www /usr/local/www
复制代码

最后,使用指令来启动视频组件

  1. 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

232.png

3. 安卓远程控制APP

使用AndroidStudio作为IDE,利用webview控件作为人机交互,简单快速。

(1) fish.html文件,放入assets目录

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta charset="utf-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <link rel="shortcut icon" href="/favicon.ico" />
  7.     <link rel="bookmark" href="/favicon.ico" type="image/x-icon"   />
  8.     <title>远程喂鱼</title>
  9.     <link rel="shortcut icon" href="favicon.ico">
  10.     <link href="css/bootstrap.min.css?v=3.3.6" rel="stylesheet">
  11.     <link href="css/font-awesome.css?v=4.4.0" rel="stylesheet">
  12.     <link href="css/animate.css" rel="stylesheet">
  13.     <link href="css/style.css?v=4.1.0" rel="stylesheet">
  14. </head>

  15. <body class="gray-bg">
  16. <div class="wrapper wrapper-content" style="padding:10px;">

  17.     <div class="row">
  18.         <div class="col-sm-4">
  19.             <div class="ibox float-e-margins" style="margin-bottom:5px;">
  20.                 <div class="ibox-content no-padding">
  21.                     <div class="panel-body">
  22.                         8:00自动开灯和水泵,17:00自动关灯和水泵
  23.                     </div>
  24.                 </div>
  25.             </div>
  26.         </div>
  27.     </div>

  28.     <div class="row">
  29.         <div class="col-sm-4">
  30.             <div class="ibox float-e-margins" style="margin-bottom:5px;">
  31.                 <div class="ibox-title">
  32.                     <h5>实时视频</h5>
  33.                 </div>
  34.                 <div class="ibox-content no-padding">
  35.                     <div class="panel-body">
  36.                         <img style="width:100%;height:240px;" src="http://树莓派IP:12001/?action=stream" />
  37.                     </div>
  38.                 </div>
  39.             </div>
  40.         </div>
  41.     </div>

  42.     <div class="row">
  43.         <div class="col-sm-4">
  44.             <div class="ibox float-e-margins" style="margin-bottom:5px;">
  45.                 <div class="ibox-content no-padding">
  46.                     <div class="panel-body" style="text-align:center;">
  47.                         <button id="lightBtn" class="btn btn-w-m btn-success" type="button"></button>  
  48.                         <button id="pumpBtn" class="btn btn-w-m btn-success" type="button"></button>
  49.                         <!--<button class="btn btn-w-m btn-success" type="button" onclick="control('resetvideo')">重启视频</button>  -->
  50.                         <button class="btn btn-w-m btn-success" type="button" onclick="control('reboot')">重启控制器</button>  
  51.                         <button id="fishBtn" class="btn btn-w-m btn-success" type="button" onclick="control('open_close')">喂食</button>
  52.                     </div>
  53.                 </div>
  54.             </div>
  55.         </div>
  56.     </div>

  57. </div>
  58. <script src="js/jquery.min.js?v=2.1.4"></script>
  59. <script src="js/bootstrap.min.js?v=3.3.6"></script>
  60. <script>
  61.         function control(op) {
  62.             if (op == "open_close")
  63.                 $("#fishBtn").removeClass("btn-success").addClass("btn-default").attr('disabled', 'disabled');

  64.             var ret = "";
  65.             if (op == "resetvideo") {
  66.                 if (confirm("确定要重启视频模块吗?")) {
  67.                     ret = window.JSHook.execTcpCmd(op);
  68.                 }
  69.             }
  70.             else if (op == "reboot") {
  71.                 if (confirm("确定要重启控制器?")) {
  72.                     ret = window.JSHook.execTcpCmd(op);
  73.                 }
  74.             }
  75.             else
  76.                 window.setTimeout(function () {
  77.                     ret = window.JSHook.execTcpCmd(op);
  78.                     controlCallback(op, ret);
  79.                 }, 0);
  80.         }
  81.         function controlCallback(op, ret) {
  82.             if (op == "getStatus") {
  83.                 var lightStatus = ret.split(",")[0];
  84.                 var pumpStatus = ret.split(",")[1];
  85.                 if (lightStatus == "1")
  86.                     $("#lightBtn").removeClass("btn-default").addClass("btn-success").text("关灯").unbind("click").click(function () {
  87.                         control("close1");
  88.                     });
  89.                 else
  90.                     $("#lightBtn").removeClass("btn-success").addClass("btn-default").text("开灯").unbind("click").click(function () {
  91.                         control("open1");
  92.                     });
  93.                 if (pumpStatus == "1")
  94.                     $("#pumpBtn").removeClass("btn-default").addClass("btn-success").text("关水泵").unbind("click").click(function () {
  95.                         control("close2");
  96.                     });
  97.                 else
  98.                     $("#pumpBtn").removeClass("btn-success").addClass("btn-default").text("开水泵").unbind("click").click(function () {
  99.                         control("open2");
  100.                     });
  101.             }
  102.             else if (op == "open1" && ret == "light 1") { //开灯
  103.                 $("#lightBtn").removeClass("btn-default").addClass("btn-success").text("关灯").unbind("click").click(function () {
  104.                     control("close1");
  105.                 });
  106.             }
  107.             else if (op == "close1" && ret == "light 0") {//关灯
  108.                 $("#lightBtn").removeClass("btn-success").addClass("btn-default").text("开灯").unbind("click").click(function () {
  109.                     control("open1");
  110.                 });
  111.             }
  112.             else if (op == "open2" && ret == "pump 1") {//开水泵
  113.                 $("#pumpBtn").removeClass("btn-default").addClass("btn-success").text("关水泵").unbind("click").click(function () {
  114.                     control("close2");
  115.                 });
  116.             }
  117.             else if (op == "close2" && ret == "pump 0") {//关水泵
  118.                 $("#pumpBtn").removeClass("btn-success").addClass("btn-default").text("开水泵").unbind("click").click(function () {
  119.                     control("open2");
  120.                 });
  121.             }
  122.             else if (op == "open_close" && ret == "opDrive success") {
  123.                 alert("喂食成功");
  124.                 $("#fishBtn").removeClass("btn-default").addClass("btn-success").removeAttr("disabled");
  125.             }
  126.         }
  127.         control("getStatus");
  128.     </script>
  129. </body>
  130. </html>
复制代码

(2)Activity里就一个WebView组件,主窗体后端代码MainActivity.java

  1. package com.wszhoho.viewfish;

  2. import android.annotation.SuppressLint;
  3. import android.os.Bundle;
  4. import android.os.Vibrator;
  5. import android.support.v7.app.AppCompatActivity;
  6. import android.view.View;
  7. import android.view.WindowManager;
  8. import android.webkit.JavascriptInterface;
  9. import android.webkit.WebChromeClient;
  10. import android.webkit.WebSettings;
  11. import android.webkit.WebView;

  12. import java.lang.ref.WeakReference;
  13. import java.util.Random;

  14. public class MainActivity extends AppCompatActivity {
  15.     static WeakReference<WebView> _webView;
  16.     Vibrator vibrator;

  17.     @Override
  18.     protected void onCreate(Bundle savedInstanceState) {
  19.         super.onCreate(savedInstanceState);
  20.         setContentView(R.layout.activity_main);
  21.         vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
  22.         Random rnd = new Random(100);
  23.         int v = rnd.nextInt();
  24.         String webViewUrl = "file:///android_asset/fish.html?v=" + v;
  25.         initWebView(webViewUrl);
  26.         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  27.     }

  28.     @SuppressLint("SetJavaScriptEnabled")
  29.     private void initWebView(String url) {
  30.         _webView = new WeakReference<>(findViewById(R.id.webView));
  31.         //重新设置WebSettings
  32.         WebSettings webSettings = _webView.get().getSettings();
  33.         webSettings.setDisplayZoomControls(false);
  34.         webSettings.setSupportZoom(false);
  35.         webSettings.setAppCacheEnabled(true);
  36.         webSettings.setAllowFileAccess(true);
  37.         webSettings.setUseWideViewPort(true);
  38.         webSettings.setLoadWithOverviewMode(true);
  39.         webSettings.setSaveFormData(false);
  40.         webSettings.setDomStorageEnabled(true);
  41.         webSettings.setSupportMultipleWindows(true);
  42.         webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
  43.         webSettings.setJavaScriptEnabled(true);
  44.         _webView.get().addJavascriptInterface(this, "JSHook");
  45.         _webView.get().setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
  46.         _webView.get().canGoBack();
  47.         _webView.get().requestFocus();

  48.         _webView.get().setWebChromeClient(new WebChromeClient());
  49.         _webView.get().loadUrl(url);
  50.     }

  51.     @JavascriptInterface
  52.     public String execTcpCmd(String op) {
  53.         try {
  54.             if (!op.equals("getStatus"))
  55.                 vibrator.vibrate(100);
  56.             String ret = TcpClient.SendMsg(op);
  57.             return ret;
  58.         } catch (Exception ignored) {
  59.             return "-1";
  60.         }
  61.     }
  62. }
复制代码

(3)TcpClient.java

  1. package com.wszhoho.viewfish;

  2. import java.io.BufferedReader;
  3. import java.io.IOException;
  4. import java.io.InputStreamReader;
  5. import java.io.OutputStream;
  6. import java.net.Socket;
  7. import java.util.concurrent.atomic.AtomicReference;
  8. import java.util.concurrent.locks.ReentrantLock;


  9. class TcpClient {
  10.     private static ReentrantLock lock = new ReentrantLock();

  11.     static String SendMsg(String msg) {
  12.         lock.lock();
  13.         AtomicReference<String> retStr = new AtomicReference<>("");
  14.         new Thread(() -> {
  15.             Socket client = null;
  16.             try {
  17.                 client = new Socket(树莓派IP, 7654);

  18.                 BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));

  19.                 OutputStream os = client.getOutputStream();
  20.                 os.write(msg.getBytes("utf-8"));
  21.                 os.flush();

  22.                 retStr.set(in.readLine());
  23.             } catch (IOException e) {
  24.                 e.printStackTrace();
  25.             } finally {
  26.                 if (client != null) {
  27.                     try {
  28.                         client.close();
  29.                     } catch (IOException e) {
  30.                         e.printStackTrace();
  31.                     }
  32.                 }
  33.             }
  34.         }).start();
  35.         while (retStr.get().equals("")) {
  36.             try {
  37.                 Thread.sleep(20);
  38.             } catch (InterruptedException e) {
  39.                 e.printStackTrace();
  40.             }
  41.         }
  42.         lock.unlock();
  43.         return retStr.get();
  44.     }
  45. }
复制代码

(4)AndroidManifest.xml权限配置

  1. <uses-permission android:name="android.permission.INTERNET" />
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  3. <uses-permission android:name="android.permission.VIBRATE" />
复制代码

自启动配置

首先更改系统默认的python运行版本:

  1. sudo rm /usr/bin/python

  2. sudo ln -s /usr/bin/python3 /usr/bin/python
复制代码

进入/home/pi/.config目录,建立autostart文件夹,进入该文件夹,建立两个后缀名为”.desktop”的文件。
camera.desktop文件,内容为:

  1. [Desktop Entry]

  2. Type=Application

  3. Exec=/home/pi/scripts/startCamera.sh
复制代码

tcpserver.desktop文件,内容为:

  1. [Desktop Entry]

  2. Type=Application

  3. Exec=python /home/pi/scripts/MyTcpControl.py
复制代码

完成后,重启树莓派,所有配置全部完成。
最终完成情况:
盒子巨丑,好在空间大,够放

233.png

234.png

安卓APP,我家宝宝选的图标,巨喜欢 :-)

235.png

236.jpg
分享至 : QQ空间
收藏

0 个回复

您需要登录后才可以回帖 登录 | 立即注册