前言

很早之前就想做一个这样的装置,但一直没有实现,因为硬件一直没加技能点

环境和硬件

  1. 宿舍wifi需要门户认证,不认证就没网,但能分配到ip,使用上文的iphone作为中转,就可以实现上网

  2. 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开门

扫码输密码开门