前言
很早之前就想做一个这样的装置,但一直没有实现,因为硬件一直没加技能点
环境和硬件
-
宿舍wifi需要门户认证,不认证就没网,但能分配到ip,使用上文的iphone作为中转,就可以实现上网
-
ESP8266带cp2102模块 + MG996r舵机 +一个已经认证过的iphone + 任意一个充电宝
思路
ESP8266的代码很简单,问题在于网络部分,如果只想看成品请跳转
想法1
使用frp直接通信,iphone作为frp客户端,有公网ip的服务器作为服务端,把开门端口暴露到公网上,只需要向这个服务器的特定端口发包即可直接开门
这是最简单的,但最开始我用frp收到的数据含有乱码,解决未果于是使用下一个思路
最后解决了frp乱码问题,直接使用了此方案,后面的最终版用抓包分析出校园网门户的登录请求,用esp8266登陆了校园网,可以访问外网了,直接使用了云平台通信。如不想看别的思路请跳转
想法2
如下图
左边的ARUBA为我校园网的主路由
右边的nonebot_pc为我的qq机器人,之后会作为开门的一个渠道
最上面server的是我的阿里云服务器
中间的路由代表广域网
我想把ESP8266作为客户端,连接已经认证过的iphone,这样数据就能通信到广域网了
想法很单纯,但很难实现
我最开始是ESP8266做客户端,iphone做服务端
同时iphone做客户端和阿里云通信
pc作为客户端和阿里云通信,发送开门信号到阿里云服务器上
阿里云服务器收到信号后,在将命令传到已经建立好tcp连接的客户端iphone上
iphone再把数据传到已经建立好tcp连接的esp8266上实现开门
这样做有一个弊端,需要处理两次断开的链接,分别在阿里云和iphone之间处理断开的链接
在iphone和esp8266之间处理断开的链接
那问题就很大了
在硬刚了600行代码以后,还是没法处理断开的链接和重连机制,我决定换一个思路
想法3
这是最可行的
ESP8266作为服务器
iphone作为客户端
阿里云作为服务器
只需要处理一次连接断开,iphone收到包直接向ESP8266发包,不用处理断开问题
但我写到一半发现了frp乱码的问题解决,弃用了此方案
姑且也贴一下代码
#include <Servo.h>
#include <ESP8266WiFi.h>
int LED = LED_BUILTIN; // LED引脚定义,只要选对模块就能编译通过
const char* ssid = "WHUT-DORM"; // wifi名称
const int servoPin = D2; // pwm引脚定义
Servo myservo; // 舵机模块初始化
WiFiServer server(80); // 监听端口80
void setup()
{
pinMode(LED, OUTPUT); // 设置LED引脚为输出模式
digitalWrite(LED, HIGH); // 我的HIGH是关闭灯
pinMode(servoPin, OUTPUT); // 定义pwm引脚为输出
myservo.attach(servoPin, 500, 2500); //修正脉冲宽度
Serial.begin(115200); // 串口输出 115200波特率
WiFi.begin(ssid); // 链接wifi
myservo.write(0); // 初始化舵机角度为0
Serial.print("Connecting to WiFi...");
Serial.println(ssid);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(LED, LOW);
delay(1000);
digitalWrite(LED, HIGH);
}
for (int i = 0;i<5;i++){
digitalWrite(LED, LOW);
delay(1000);
digitalWrite(LED, HIGH);
}
Serial.println("Connected to WiFi");
Serial.print("ESP8266's IP is");
Serial.println(WiFi.localIP());
server.begin();
}
void loop()
{
WiFiClient client = server.available();
if (client){
Serial.println("New client connected!");
String req = client.readStringUntil('\r');
Serial.print("Received data:");
Serial.println(req);
if(req=="a"){ // 发送a为开门
digitalWrite(LED, LOW);
client.print("1");
myservo.write(180);
delay(5000);
myservo.write(0);
digitalWrite(LED, HIGH);
}
else{
client.print("2");
}
client.flush();
}
}
import socket
import datetime
if __name__ == '__main__':
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_client_socket.connect(("pursuecode.cn", 50001))
door_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
door_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
door_server_socket.bind(("0.0.0.0", 7777))
door_server_socket.listen(128)
door_server_conn_socket, ip_port = door_server_socket.accept()
while True:
recv_data = tcp_client_socket.recv(1024)
data = recv_data.decode()
now_time = str(datetime.datetime.now())[:-7]
print(f"{now_time}\t收到服务器消息{data}")
if data == "a":
door_server_conn_socket.send("a".encode()) # 对应ESP8266的'a'
data = door_server_conn_socket.recv(1).decode()
print(data)
if data == "1":
tcp_client_socket.send("OK".encode())
else:
tcp_client_socket.send("NO".encode())
conn_socket.close()
import socket
import datetime
if __name__ == '__main__':
robot_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
robot_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
robot_server_socket.bind(("0.0.0.0", 50000))
robot_server_socket.listen(128) # 从机器人来的链接
door_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
door_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
door_server_socket.bind(("0.0.0.0", 50001))
door_server_socket.listen(128) # 从舵机来的链接
while True:
door_server_conn_socket, ip_port = door_server_socket.accept()
while True:
try:
robot_server_conn_socket, ip_port = robot_server_socket.accept() # 如果机器人来链接,即发来开门指令
data = robot_server_conn_socket.recv(1024).decode()
now_time = str(datetime.datetime.now())[:-7]
print(f"{now_time}\t收到机器人消息{data}")
if data == "open the door!":
door_server_conn_socket.send("a".encode()) # 发送开门指令
print(f"{now_time}\t发送开门指令")
robot_result = door_server_conn_socket.recv(1).decode() # 等待回复
print(f"{now_time}\t收到单片机消息{robot_result}")
if robot_result == "O":
robot_server_conn_socket.send("OK".encode())
else:
robot_server_conn_socket.send("NO".encode())
else: # 不是机器人
robot_server_conn_socket.send("FUCK YOU".encode())
except Exception as e:
now_time = str(datetime.datetime.now())[:-7]
print(f"{now_time}\t单片机离线,消息发送失败")
from nonebot import on_command
from nonebot.permission import SUPERUSER
from .config import Config
import socket
open_door_command = on_command("开门", permission=SUPERUSER, priority=1)
@open_door_command.handle()
async def open_door_command_handler():
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
tcp_client_socket.connect(("pursuecode.cn", 50000))
tcp_client_socket.settimeout(10)
tcp_client_socket.send("open the door!".encode())
recv_data = tcp_client_socket.recv(2)
tcp_client_socket.close()
data = recv_data.decode()
except ConnectionRefusedError as e:
await open_door_command.finish("服务器离线")
except Exception as e:
await open_door_command.finish(str(e))
print(data)
if data == "1":
await open_door_command.finish("已开门")
elif data == "0":
await open_door_command.finish("开门失败")
else:
await open_door_command.finish("单片机离线")
成品
iphone 编译frpc
由于之前在文章里编译过相应的go二进制,frpc可以直接套用
环境请见ios编译go二进制
git clone https://github.com/fatedier/frp.git
cd frp/cmd/frpc
go build -o frpc
拷贝到越狱后的iphone上
chmod a+x ./frpc
ldid -S ./frpc
解决乱码问题
如果带上proxy_protocol_version = v2
那么一定会出现乱码
测试程序如下
import socket
import datetime
if __name__ == '__main__':
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
tcp_server_socket.bind(("0.0.0.0", 8888))
tcp_server_socket.listen(1)
tcp1, addr = tcp_server_socket.accept()
print(tcp1.recv(1024))
tcp1.close()
import socket
import datetime
if __name__ == '__main__':
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
tcp_server_socket.connect(("xxx", 50000))
tcp_server_socket.send("qweqwe".encode())
tcp_server_socket.close()
输出如下
haihaihaideiphone:~/python root# python3 server.py
b'\r\n\r\n\x00\r\nQUIT\n!\x11\x00\x0cq9\xedM\xc0\xa8\x01\x05\xbah\xc3P'
使用配置
[common]
server_addr = domain # 和证书匹配的域名
server_port = 43399
token = xxxxxxx # 链接密码
tls_enable = true
tls_cert_file = /etc/frp/fullchain.cer
tls_key_file = /etc/frp/xxx.key
tls_trusted_ca_file = /etc/frp/ca.cer
use_recover = true
login_fail_exit = false
[door]
type = tcp
local_ip = 10.91.20.79 # ESP8266的ip
local_port = 80
remote_port = 50001
#proxy_protocol_version = v2 # 这个选项在iphone上不能带,不然会有乱码问题
// ESP8266代码同上不变
#include <Servo.h>
#include <ESP8266WiFi.h>
int LED = LED_BUILTIN; // LED引脚定义,只要选对模块就能编译通过
const char* ssid = "WHUT-DORM"; // wifi名称
const int servoPin = D2; // pwm引脚定义
Servo myservo; // 舵机模块初始化
WiFiServer server(80); // 监听端口80
void setup()
{
pinMode(LED, OUTPUT); // 设置LED引脚为输出模式
digitalWrite(LED, HIGH); // 我的HIGH是关闭灯
pinMode(servoPin, OUTPUT); // 定义pwm引脚为输出
myservo.attach(servoPin, 500, 2500); //修正脉冲宽度
Serial.begin(115200); // 串口输出 115200波特率
WiFi.begin(ssid); // 链接wifi
myservo.write(0); // 初始化舵机角度为0
Serial.print("Connecting to WiFi...");
Serial.println(ssid);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(LED, LOW);
delay(1000);
digitalWrite(LED, HIGH);
}
for (int i = 0;i<5;i++){
digitalWrite(LED, LOW);
delay(1000);
digitalWrite(LED, HIGH);
}
Serial.println("Connected to WiFi");
Serial.print("ESP8266's IP is");
Serial.println(WiFi.localIP());
server.begin();
}
void loop()
{
WiFiClient client = server.available();
if (client){
Serial.println("New client connected!");
String req = client.readStringUntil('\r');
Serial.print("Received data:");
Serial.println(req);
if(req=="a"){ // 发送a为开门
digitalWrite(LED, LOW);
client.print("1");
myservo.write(180);
delay(5000);
myservo.write(0);
digitalWrite(LED, HIGH);
}
else{
client.print("2");
}
client.flush();
}
}
iphone只需要开启frpc,然后从公网xxx:50001访问就能开机
from nonebot import on_command
from nonebot.permission import SUPERUSER
from .config import Config
import socket
open_door_command = on_command("开门", permission=SUPERUSER, priority=1)
@open_door_command.handle()
async def open_door_command_handler():
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
tcp_client_socket.connect(("wwww", 50001))
tcp_client_socket.settimeout(10)
tcp_client_socket.send("a\r".encode())
recv_data = tcp_client_socket.recv(2)
tcp_client_socket.close()
data = recv_data.decode()
except ConnectionRefusedError as e:
await open_door_command.finish("服务器离线")
except Exception as e:
await open_door_command.finish(str(e))
print(data)
if data == "1":
await open_door_command.finish("已开门")
elif data == "0":
await open_door_command.finish("开门失败")
else:
await open_door_command.finish("单片机离线")
<?php
header('Content-Type:application/json; charset=utf-8');
$password = $_POST['pwd'];
if ($password == "your_password"){
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, "xxx", "50001"); // 对应ip, port
socket_write($socket, "a\r", 2);
$recv_data = socket_read($socket, 1);
socket_close($socket);
http_response_code(200);
if ($recv_data=="1"){
$recv_data = "开门成功";
}
else{
$recv_data = "开门失败";
}
$json = json_encode(array("code"=>200, "message"=>$recv_data));
exit($json);
}
else{
http_response_code(401);
$json = json_encode(array("code"=>401, "message"=>"密码错误"));
exit($json);
}
?>
<!DOCTYPE html>
<html style="height: 100%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>门锁页面</title>
</head>
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#door_form{
height: 100px;
}
</style>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" type="text/javascript"></script>
<script>
window.onload = function() {
var element = document.getElementById('door_form');
var yOffset = element.getBoundingClientRect().top + window.pageYOffset;
window.scrollTo({ top: yOffset, behavior: 'smooth' });
};
</script>
<body>
<div class="container">
<form name="input" id="door_form">
开门密码: <input type="password" name="pwd" id="pwd">
<input type="submit" value="开门">
</form>
</div>
<script>
$("#door_form").submit(function(event){
var formData = $(this).serialize();
event.preventDefault();
$.ajax({
url: "door_identify.php",
type: "POST",
data: formData,
success: function(data){
alert(data.message);
},
error: function(xhr){
alert(xhr.responseJSON.message);
}
})
})
</script>
</body>
</html>
效果如下
第一次开门
QQ开门
扫码输密码开门