基于树莓派的遥控开锁装置

news/2024/10/23 2:46:40/

基于树莓派的远程控制开锁装置

  • 前期准备
    • 前情提要
    • 基本思路
    • 材料清单
    • 技术清单
  • 开始工作
    • 服务器端
    • 客户端:
    • 服务器Web接口
    • GPIO控制
    • 设备安置
  • 效果
    • 测试
    • APP控制

前期准备

前情提要

由于在学校宿舍有四个人,当遇到有同学不带钥匙出门/隔壁宿舍来串门/外卖到了等情况需要我停下音乐,放下耳机,挣扎着从座位/床上爬出来然后开门真的是很难受,有时候一晚会重复上述情况数次,非常影响工作(游戏)效率,正巧前几个月买了个树莓派来玩看到各路大神也有做类似的东西于是便有了自己做一个远程开锁装置的想法.

基本思路

当时想的就是至少能满足2种场景
1.我坐在电脑前有人开门我在电脑上随便按点什么能打开宿舍的门
2.我回到宿舍门前可以用手机APP开门
于是想到用Web网页的接口进行控制,刚好我有个Web服务器,这样甚至能实现在任何地方只要有网络就能进行控制.
然后树莓派通过校园网wifi连接服务器,接收服务器信息,当接收到开锁的信息后通过GPIO控制电机进行开锁,具体怎么样实现到时候再说,先把材料准备好.

材料清单

树莓派3B*1
SG90陀机(升级版)*1
排插*1
基本工具箱(螺丝刀,剪线钳,双面胶,透明胶等)
杜邦线若干
一米导线*3
Web服务器*1
校园网wifi*1

技术清单

Web服务器搭建
PHP
Python
树莓派GPIO开发
基于Java的安卓开发

开始工作

鉴于网购的东西还没到,先把服务器和客户端写了.
按道理来说我的树莓派应该作为服务器,开个Web服务,然后通过网页访问树莓派执行操作.但是我实在没办法从外网连接到处于校园网的树莓派,只能选择服务器搭载云端,然后树莓派作为客户端访问服务器建立连接后监听服务器发送的数据,一旦服务器发送解锁指令,就调用GPIO控制电机解锁宿舍门.

不知道为什么觉得树莓派上用Python应该会好点,可能是因为我不想写C,于是只学过python基本语法的我搜集了一堆资料后拼凑出了这样的代码:

服务器端

#!/usr/bin/python3
# coding=utf-8
# filename:server.py
import select
import socket
# import queue
import sys
import os
import os.path
import time
import logging# 配置日志信息
LOG_FORMAT = "[%(asctime)s]%(levelname)s:%(message)s"
DATE_FORMAT = "%m-%d %H:%M:%S"
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)# 监视文件路径
fdir= '/www/wwwroot/file.txt'
# 创建 socket 对象
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取本地主机名和端口号
host = socket.gethostname()
port = 9989
# 设置为非阻塞
server.setblocking(False)
# 绑定端口号
while True:try:server.bind((host, port))except Exception as e:logging.warning('server boot error,retry in 3s:{}'.format(e))# 可能会遇到端口占用错误time.sleep(3)else:break
# 所有连接进来的对象都放在inputs
inputs=[server,]# 自己也要监控,因为server本身也是个对象
# 需要发送数据的对象
outputs=[]
# 对外发送数据的队列,记录到字典中
message_queues = {}
f_text = {}def start():# 设置并启动TCP监听器 设置最大连接数,超过后排队server.listen(1)logging.info("Server Ready.")check()# clear()仅仅是清除缓存和刷新当前连接,并非停止连接,需要开始输入start(),需要停止连接请再输入stop()
def clear():# 2018年11月14日 20:32:42测试通过,没事别乱改for s in outputs:inputs.remove(s)outputs.remove(s)s.close()logging.info('All connection refreshed! Use start() or stop() to continue.')def check():# 主循环采用select进行阻塞,内容主要参考https://www.cnblogs.com/bigberg/p/8044581.htmlwhile True:time.sleep(0.1)readable, writable, exceptional = select.select(inputs, outputs, inputs, 1)# ///???# 如果没有任何fd就绪,那程序就会一直阻塞在这里for s in readable:  # 每一个s就是有个socket# Readable list 中的socket 可以有3种可能状态,第一种是如果这个socket是main "server" socket,它负责监听客户端的连接,如果这个main server socket出现在readable里,那代表这是server端已经ready来接收一个新的连接进来了,为了让这个main server能同时处理多个连接,在下面的代码里,我们把这个main server的socket设置为非阻塞模式。 if s is server:# 别忘记,上面我们server自己也当做一个fd放在了inputs列表里,传给了select,如果这个s是server,代表server这个fd就绪了,# 就是有活动了, 什么情况下它才有活动? 当然 是有新连接进来的时候# 新连接进来了,接受这个连接(没有新连接根本不会select进来)conn, client_addr = s.accept()logging.info("new connection from:{}".format(client_addr))conn.setblocking(0)inputs.append(conn)outputs.append(conn)logging.info("Start checking...")f_text[conn] = open(fdir, mode='r').read()# 读取初始值# 为了不阻塞整个程序,我们不会立刻在这里开始接收客户端发来的数据, 把它放到inputs里, 下一次loop时,这个新连接# 就会被交给select去监听,如果这个连接的客户端发来了数据 ,那这个连接的fd在server端就会变成就续的,select就会把这个连接返回,# 返回到readable 列表里,然后你就可以loop readable列表,取出这个连接,开始接收数据了, 下面就是这么干的# message_queues[conn] = queue.Queue()# 接收到客户端的数据后,不立刻返回 ,暂存在队列里,以后发送else:   # s不是server的话,那就只能是一个与客户端建立的连接的fd了# 客户端的数据过来了,在这接收logging.info('recving')# 在客户端未接受服务器数据时断开会报错,故trytry:data = s.recv(1024)except Exception as e:logging.warning('recv error:{}'.format(e))data = Noneif data:logging.info('received [{}] from {}'.format(data,s.getpeername()[0]))# message_queues[s].put(data)  # 收到的数据先放到queue里,一会返回给客户端# if s not in outputs:#     outputs.append(s)  # 为了不影响处理与其它客户端的连接 , 这里不立刻返回数据给客户端else:  # 如果收不到data代表客户端断开了logging.info("client [{}] closed".format(s))if s in outputs:# 既然客户端都断开了,我就不用再给它返回数据了,# 所以这时候如果这个客户端的连接对象还在outputs列表中,就把它删掉outputs.remove(s)inputs.remove(s)  # 这个连接必然在inputs中,也删掉s.close()# 关闭的连接在队列中也删除# del message_queues[s]for s in writable:# 对于writable list中的socket,也有几种状态,如果这个客户端连接在跟它对应的queue里有数据,就把这个数据取出来再发回给这个客户端,否则就把这个连接从output list中移除,这样下一次循环select()调用时检测到outputs list中没有这个连接,那就会认为这个连接还处于非活动状态# 判断文件是否被更改if(os.path.exists(fdir)):ntext = open(fdir, mode='r').read()if(f_text[s] != ntext):f_text[s] = ntextchanged = Trueelse:changed = False#发送信息if(changed):try:s.send('asdas'.encode('utf-8'))except Exception as e:logging.warning('message send error:{}'.format(e))inputs.remove(s)outputs.remove(s)s.close()breakelse:logging.info("message send!")for s in exceptional:# 最后,如果在跟某个socket连接通信过程中出了错误,就把这个连接对象在inputs\outputs\message_queue中都删除,再把连接关闭掉logging.info('handling exceptional condition for:{}'.format(s.getpeername()[0]))# 从inputs中删除if s in inputs:inputs.remove(s)if s in outputs:outputs.remove(s)s.close()# 删除队列del message_queues[s]def stop():clear()server.close()logging.info('Server stoped please close the program!')# Check and send
def cas(clientsocket,msg):logging.info("Start checking...")ftext = open(fdir, mode='r').read()# 读取初始值while True:time.sleep(0.1)# 判断文件是否被更改if(os.path.exists(fdir)):ntext = open(fdir, mode='r').read()if(ftext != ntext):ftext = ntextchanged = Trueelse:changed = False#发送信息if(changed):try:clientsocket.send(msg.encode('utf-8'))except Exception as e:logging.warning('message send error:{}'.format(e))breakelse:logging.info("message send!")

服务器端监视’/www/wwwroot/file.txt’文件,若改文件改变,则发送信息给客户端.
运行该服务:
启动服务器

客户端:

#!/usr/bin/python3
# coding=utf-8
# filename:client.pyimport socket
import select
import sys
import time
import logging# 配置日志信息
LOG_FORMAT = "[%(asctime)s]%(levelname)s:%(message)s"
DATE_FORMAT = "%m-%d %H:%M:%S"
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT)# 设置主机名
host = '192.168.1.101'
# 设置端口号
port = 9989def link(funRun=None,funClose=None):while True:# 创建 socket 对象client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) #在客户端开启心跳维护client.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 20) #空闲时间client.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) #发送间隔client.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 3) #允许失败# 所有连接进来的对象都放在inputsinputs=[client,]# 自己也要监控,因为server本身也是个对象# 需要发送数据的对象outputs=[]# 对外发送数据的队列,记录到字典中message_queues = {}# 连接循环while True:try:logging.info('connecting...')# 连接服务,指定主机和端口client.connect((host, port))except socket.error as e:logging.warning("link error retry 3s later:{}".format(e))time.sleep(3)else:breaklogging.info('server connected!')client.setblocking(0)# 监听循环while True:try:time.sleep(0.1)logging.debug('slecting...')readable, writable, exceptional = select.select(inputs, outputs, inputs, 1)# 设置超时1以接受CTRL+Cexcept Exception as e:logging.warning("Exception:{}".format(e))# 防止未结束连接导致服务器端口占用client.shutdown(2)client.close()breakexcept:logging.warning("KeyboardInterrupt")# 防止未结束连接导致服务器端口占用client.shutdown(2)client.close()if(funClose!=None):funClose()returnif(readable):logging.debug('ready to recv')try:msg = client.recv(1024).decode('utf-8')except (socket.error) as e:logging.warning("BlockingIOError:{}".format(e))continueexcept:logging.warning("Unexpected error:{}".format(sys.exc_info()[0]))breakif(msg == "asdas"):if(funRun!=None):funRun()logging.info("run...")else:logging.warning("unexapable message,connecting close,start retrying")breakif(exceptional):logging.warning('handling exceptional condition for{}'.format(client.getpeername()[0]))breakclient.close()

客户端中的link方法用于连接服务器,那两个参数分别是接收到服务器指令后需要执行的函数和客户端结束时执行的清理函数.
用客户端连接服务器:
客户端连接服务器
尝试更改服务器监视的文件,服务器发送信息,客户端接受信息然后print了run…测试完成.
信息发送测试

服务器Web接口

服务器Web接口就用PHP写吧,主要功能就是访问这个接口后更改同一目录下的file.txt文件.

<?php
if(array_key_exists('key',$_REQUEST)){$key = $_REQUEST['key'];if($key=='key'){file_put_contents("file.txt", date("ymdhis"));echo 'file changed!';}
}
?>

丢到服务器上后通过浏览器访问该接口"192.168.1.101/test.php?key=key"发现服务器和客户端执行了相应行为,测试完成.

GPIO控制

陀机到了,SG90陀机用三根线控制,红色接GPIO的4接口,灰色接6,橙色接12
陀机接线
控制代码:

#!/usr/bin/python
# filename:base.py
import RPi.GPIO as GPIO
import time
import signal
import atexitGPIO.setwarnings(False)
GPIO.cleanup()
servopin = 12
GPIO.setmode(GPIO.BOARD)
GPIO.setup(servopin, GPIO.OUT, initial=False)
p = GPIO.PWM(servopin,50) #50HZdef run(t=1,s=2,a1=4,a2=10):p.start(0)for i in range(0,t):p.ChangeDutyCycle(a1)time.sleep(.5)p.ChangeDutyCycle(0)time.sleep(s)p.ChangeDutyCycle(a2)time.sleep(.5)p.ChangeDutyCycle(0)time.sleep(s)p.ChangeDutyCycle(0)def exit():GPIO.cleanup()

在python中运行run方法,正常情况陀机转动大约60度然后过2秒又转回来.
在测试时发现一个问题,用Python2运行良好而Python3会出现奇怪的错误,于是将就着用Python2来跑了.

将控制程序和客户端结合起来使用只需要启动python然后

import client as c
import base as b
c.link(b.run(),b.exit())

就可以打开网页测试了.

设备安置

测试成功后将陀机和门锁中某个结构用绳子绑起来,当陀机转动时会带动门锁开门(期间由于各种原因弄坏了2个SG90陀机,于是换了个SG90升级版(金属齿轮)希望不要再坏了).
用双面胶将陀机粘在门锁旁边,完成后如下图
陀机安置
测试完毕后就开始布线,将排插用双面胶黏在门旁边的墙壁上,排插上再粘一个盒子用来装树莓派,完成后如图:
树莓派安置
整体布局如图:
整体布局

效果

测试

主要使用方法就是访问服务器上的某网页,然后门就开了.
经过几星期的实际使用,发现了不少BUG并且成功修复了.之后试过连续使用一个星期不出故障,应该是没问题了.
其中一个比较严重的的问题是:
树莓派掉落
后来买了钉子把排插钉墙上了.

APP控制

由于舍友反馈说这玩意儿每次开门都要打开网页输入网址很麻烦,于是便想办法弄一个手机APP进行控制.
打开AndroidStudio建立工程
设计布局页面Activity_main.xml的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="com.example.table.MainActivity"><ImageViewandroid:id="@+id/Table"android:layout_width="0dp"android:layout_height="0dp"app:layout_constraintBottom_toTopOf="@+id/UnlockButton"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:srcCompat="@drawable/table" /><ImageButtonandroid:id="@+id/UnlockButton"android:layout_width="wrap_content"android:layout_height="wrap_content"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:srcCompat="@mipmap/ic_launcher_round" /></android.support.constraint.ConstraintLayout>

其中res/drawable/table是一张课程表的图片,让APP不会过于单调,UnlockButton的图片随便用了一张系统自带的.
然后以能用就行的原则写了以下的代码:
MainActivity.java的代码

package com.example.table;import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;import java.net.HttpURLConnection;
import java.net.URL;public class MainActivity extends AppCompatActivity {private ImageButton imageButton;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);//配置多线程StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectLeakedSqlLiteObjects().penaltyLog().penaltyDeath().build());setContentView(R.layout.activity_main);imageButton = (ImageButton)findViewById(R.id.UnlockButton);imageButton.setOnClickListener(new mClick());}class mClick implements OnClickListener {public void onClick(View v){imageButton.setEnabled(false);Connect();}private void Connect(){try {URL url = new URL("http://192.168.1.101/test.php?key=key");HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(10000);conn.setRequestMethod("GET");conn.setInstanceFollowRedirects(true);if (conn.getResponseCode() == 200){MainActivity.this.setTitle("发送成功");}} catch (Exception e) {MainActivity.this.setTitle("网络连接错误");}imageButton.setEnabled(true);}}
}

在res/AndroidManifest.xml文件的<manifest标签下添加以下语句以获取网络权限:

<uses-permission android:name="android.permission.INTERNET" />

测试的时候由于我用一台用了挺久的红米3手机进行测试,这个手机经常会出现某一个APP无法联网的情况.因为这个问题我用了一个下午来怀疑人生,直到手机没电重启后那个现象恢复了我才意识到这个问题.
最后实现了用手机打开APP点击一个按钮后就能开门了.


http://www.ppmy.cn/news/212881.html

相关文章

基于单片机的红外遥控密码锁系统设计(#0407)

在日常的生活和工作中, 住宅与部门的安全防范、单位的文件档案、财务报表以及一些个人资料的保存多以加锁的办法来解决。若使用传统的机械式钥匙开锁&#xff0c;人们常需携带多把钥匙, 使用极不方便, 且钥匙丢失后安全性即大打折扣。具有防盗报警等功能的电子密码锁代替密码量…

智能门锁人脸识别安全风险到底有多大?

【雷龙资讯】近日&#xff0c;国家市场监管总局发布智能门锁消费提示称&#xff0c;经风险监测发现&#xff0c;搭载人脸识别功能和远程开锁功能的智能门锁安全风险较高&#xff0c;建议消费者尽量不使用或关闭人脸识别功能和远程开锁功能。    看到市场上的智能门锁具品牌和…

esp32-cam摄像头+远程遥控小车

目录 1、esp32cam开发2、51单片机开发3、手机端开发4、总结 先来说说需要的器件&#xff0c;因为是心血来潮做的一个简单的实验&#xff0c;所以用的也都是最基础的东西。淘宝买的一个小车底板&#xff0c;外加四个带轮子的减速电机&#xff0c;一个51单片机最小系统开发板&…

基于机智云物联网平台与4G DTU远程车库门

一、项目内容及背景 随着物联网技术的飞速发展&#xff0c;逐渐进入了万物互联时代&#xff1b;本项目针对不支持手机远程开门的老旧门锁升级改装&#xff0c;对门锁进行简单升级&#xff0c;接入到机智云物联网云平台&#xff0c;实现手机远程操作。 二、项目材料 1.老旧…

不限距离4g/5g信号远程遥控小车

4g/5g不限距离遥控小车(1) 4g/5g不限距离遥控小车(2) 最开始学习编程也是源于一个想法, 无线遥控小车和飞机操作范围都是在几十米, 远的几百米, 再远的几公里, 那能不能把手机放在小车或飞机上, 利用手机的4g/5g信号来接收指令, 这样只要有手机信号的地方, 就可以不限距离的操作…

CI2451/CI2454无线收发SOC芯片2.4g内置MCU遥控门锁超高性价比

CI2451跟CI2454是一款2.4G的SOC无线收发芯片/集成无线收发器和 8 位 RISC&#xff08;精简指令集&#xff09;MCU,其中CI2451和ci2454在无线收发的特性是一样的&#xff0c;但是在MCU的资源上CI2454的资源会更加优秀&#xff0c;ci2451具有更高的性价比&#xff0c;对成本要求比…

【基于 Arduino 的 RFID门锁】

【基于 Arduino 的 RFID门锁】 1. 概述2. 射频识别的工作原理3. RFID 和 Arduino4. Arduino RFID门锁门禁项目5. 源代码 在本教程中&#xff0c;我们将了解什么是 RFID&#xff0c;它是如何工作的以及如何制作基于 Arduino 的 RFID 门锁。您可以观看以下视频或阅读下面的书面教…

基于STM32F407的智能门锁

智能门锁 一、项目背景 在消费升级渗透在各个领域的今天&#xff0c;国民消费发生着巨大的变化&#xff0c;与每个人息息相关的家居行业也是如此。现今&#xff0c;越来越多的智能家居产品出现在普通老百姓的生活中&#xff0c;智能照明、智能窗帘、智能扫地机器人等各种智能产…