订阅本站
收藏本站
微博分享
QQ空间分享

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

alen 分类:树莓派 时间:2018/12/23 02:20:40 浏览: 评论: 加入收藏

眼看要过年了,回老家之后,养的小鱼用不了几天就要见马克思,想着用朋友送的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语言编写。

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

MyTcpControl.py完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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)安装依赖库

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

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

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

1
2
3
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

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

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

3. 安卓远程控制APP
使用Android Studio作为IDE,利用webview控件作为人机交互,简单快速。
(1) fish.html文件,放入assets目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<!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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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权限配置

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

自启动配置

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

1
2
sudo rm /usr/bin/python
sudo ln -s /usr/bin/python3 /usr/bin/python

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

1
2
3
[Desktop Entry]
Type=Application
Exec=/home/pi/scripts/startCamera.sh

tcpserver.desktop文件,内容为:

1
2
3
[Desktop Entry]
Type=Application
Exec=python /home/pi/scripts/MyTcpControl.py

完成后,重启树莓派,所有配置全部完成。

最终完成情况:
盒子巨丑,好在空间大,够放!


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

作者:wszhoho
项目主页:http://maker.quwj.com/project/86

TAG:树莓派

文章评论

留言与评论(共有 0 条评论)
   
验证码: