本帖最后由 lou 于 2020-4-30 16:33 编辑
利用树莓派Zero远程可视化喂鱼
眼看要过年了,回老家之后,养的小鱼用不了几天就要见马克思,想着用朋友送的zero来做一个远程喂鱼的小东西,应该不难。 思路:利用双路继电器分别控制灯和水泵,使用mjpg-streamer来获取摄像头的视频流,并在特定的时刻自动开闭继电器。 网络环境:有公网IP的家庭网络,利用路由器的ddns或者花生壳,树莓派作为tcpserver对外提供访问。但这个条件,目前已经很难满足了,一般网络都是大内网,这种情况可以让树莓派作为tcpclient主动请求服务器获取指令,本文介绍的是第一种情况。 鱼食槽暂时未完成,准备搞两个大一点的瓶盖,合起来热熔胶伺候,中间放鱼食,边缘开两个孔,最终固定到步进电机上,转一圈就能完成喂鱼动作。 树莓派的安装和配置,本文不再赘述,本文分“硬件部分”、“软件部分”、“自启动配置”来说明整个项目。
硬件部分
本项目中使用的硬件: 必不可少的大脑:
1. 双路继电器
使用 gpioreadall 指令来获取树莓派上的所有接口信息。 这里使用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编号为2324 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,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
复制代码
由于市面上大部分摄像头是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
使用AndroidStudio作为IDE,利用webview控件作为人机交互,简单快速。
(1) fish.html文件,放入assets目录
(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,我家宝宝选的图标,巨喜欢 :-)
|