本文首发于:本人博客【esp32】micropython实现自动登录校园网
上回书说到,笔者通过python在电脑上成功实现了自动登录校园网的脚本。在这之后,笔者开始思考如何在esp32上实现这一功能,并且实现一个可以无需依赖手机热点的物联网小项目。本文将首先介绍esp32联网所涉及的模块,并且对各个模块的用法进行阐释,再回顾一下登录校园网脚本的步骤,最后总结一下整个过程中的与出现的问题与解决办法并将各个部分的代码整合起来给出完整的示例代码。
一、涉及模块
esp连接校园网需要用到network模块进行wifi的连接,并通过urequests模块进行http数据收发,utime模块进行超时检测。
1. network模块
esp32c3模块提供了wifi模块并且可以通过调用network模块进行wifi的连接。esp32提供的网络接口有两种,分别是`network.STA_IF`与`network.AP_IF`,其中前者是station接口的简称,用于连接其他wifi,后者是access point的简称,用于开启热点并与其他wifi client建立连接。
关于wlan类有以下几个比较重要的函数:
class network.WLAN(interface_id) # 用于获取创建wlan接口对象
WLAN.active([is_active]) # 获取或设置wlan接口激活状态
WLAN.connect(ssid=None, key=None, *, bssid=None) # 连接到指定无线网络
WLAN.disconnect() # 断开当前无线网络连接
WLAN.scan() # 在STA接口上扫描可用的无线网络
WLAN.status([param]) # 返回无线连接的状态
WLAN.isconnected() # 返回是否与其他无线网络建立连接状态
WLAN.ifconfig([(ip, subnet, gateway, dns)]) # 获取IP级网络接口参数(一个包含IP 地址、子网掩码、网关和 DNS 服务器的元组)
WLAN.config('param') # 获取或设置通用网络接口参数
更多有关wlan类的函数和解释可以查看micropython文档中的wlan类介绍。
此处我们用到的是station接口,先获取接口示例`wlan = network.WLAN(network.STA_IF)` ,随后激活wifi并进行连接,并且利用utime记录时间防止连接超时。最后利用`ifconfig()`函数获取本地ip。
在连接好wifi后还需要进行是否能够接入互联网的检测,在python中可以利用subprocess模块去ping一个公网地址,不过在micropython中既没有subprocess也没有ping的功能,笔者想到通过向某个网站发送一个http请求并且分析返回内容是否正确的方法来确认是否成功连接互联网。
def ping():
rq = requests.get("http://www.example.com") # 向example.com发送get请求
if rq.status_code != 200 or "Example Domain" not in rq.text: # 分析返回内容是否正确
print("Unconnected to the Internet!")
rq.close() # 注意此处有必要关闭response对象,同时开启过多的response对象会导致报错OSError 32
return False
else:
print("Connected to the Internet!")
rq.close()
return True
2. urequests模块
urequests网络请求模块与python中requests模块的使用方法比较像,可以实现后者大部分的功能,不过仍然有一定的不同。下面给出urequests中比较重要的函数。
urequests.request(method, url, data=None, json=None, headers={})
urequests.head(url, \*\*kw)
urequests.get(url, \*\*kw)
urequests.post(url, \*\*kw)
urequests.patch(url, \*\*kw)
urequests.delete(url, \*\*kw)
先回顾一下requests的使用方法:
def get(url, params=None, **kwargs)
`requests.get()`支持把字典作为参数传给`params`,requests会自动将字典转化成字符串的形式。这个操作也可以通过`urllib.parse.urlencode()`来完成,不过micropython中没有内置的urllib,第三方库的urllib也是出现了各种问题,导致必须手动实现一个`urlencode()`函数或者把所有的字典预处理成字符串。
def post(url, data=None, json=None, **kwargs)
与`get()`类似,`requests.post()`支持把字典传给`data`参数,不过micropython不允许这样做,所以必须先把字典处理成字符串再传入。其实这里讲字符串是不准确的,因为把str类型传给`params`和`data`时会报错`Error: Object with buffer protocol required`,正确的操作是传入一个bytes类型的字符串。
micropython除了不支持对字典进行url编码、对参数类型要求格外严苛之外,还有几处很奇妙的不同。
其一是micropython的requests模块的所有函数都会自动向headers中添加一项"Content-Length",注意是自动!这个特性令我痛苦的debug了好久,明明headers和data都是完全正确的,post数据包加了content-length就是Bad Request 400,get数据包啥都不加都返回错误码Internal Server Error 500(~~真不知道学校的垃圾服务器为什么会对有问题的数据包返回一个500~~),也不能靠抓包来细致的查看是发送的哪个环节出现了问题。最后还好在网上找到了urequests的源代码,直接搬下来一行行纠错,发现原来会自动添加正文长度。
其二是micropython不支持gbk,所以所有的encode、decode函数都失效,所以`'字符串'.encode('gbk')`实际上返回的还是一个utf-8编码的bytes。本来这点没什么,但是有意思的在下面。学校的辣鸡网络通是用gbk编码的,然后urequests的`response.text()`是这样实现的:
def text(self):
return str(self.content, self.encoding)
然后这~~两坨答辩~~就碰撞出了美妙而几近天衣无缝的bug:一个返回gbk,一个没法解码gbk,直接报错`'utf-8' codec can't decode byte 0xb5 in position 4: invalid start byte` ,并且还定位不到错误位置,几乎无解。最后还是通过更改urequests源代码,把encoding去掉的方式解决的。
更多有关requests库和urequests的内容可以查看requests官方文档和micropython文档中对urequests的描述。笔者更改过后的urequests模块源代码放在这里。
二、校园网登录
利用python实现的校园网自动登录脚本详见上一篇文章,这里只做简单的回顾。
校园网自动登录有两种方式,webdriver模拟浏览器法和requests模拟请求法,其中前者并不受micropython的支持,因此想要实现模拟登陆只能通过urequests模块。校园网络通登录并连接互联网需要进行两次请求,先进行一次post以提交用户信息并让服务端记录本地ip,再发送一次get请求开通网络。
post与get的请求头都通过浏览器抓包获取并且制作成字典,数据正文则可以在浏览器抓包后点击view source按钮,然后复制正文直接放进字符串。
三、实现代码
这样就可以就可以让esp32成功登陆校园网啦!
下面放出实现代码
import network
import requests
import utime
import ujson
import re
class Wifi(object):
CONNECT_TIMEOUT = 5000 # ms
def connect(self, ssid, password):
#获取站点WiFi接口的实例并将其存储在变量上
self.wifi = network.WLAN(network.STA_IF)
if self.wifi.isconnected() == True:
self.wifi.disconnect()
#使用存储在前文所述的变量中的凭证来激活网络接口并执行实际连接。
self.wifi.active(True)
self.wifi.connect(ssid, password)
connect_time = utime.ticks_ms()
while self.wifi.isconnected() == False:
if utime.ticks_ms() - connect_time >= self.CONNECT_TIMEOUT:
print("Connect time out!")
return False
else:
pass
self.ipaddr = self.wifi.ifconfig()[0]
print("Connection successful, IP Address: {}".format(self.ipaddr))
return True
def disconnect(self):
self.wifi.disconnect()
def ping():
rq = requests.get("http://www.example.com")
if rq.status_code != 200 or "Example Domain" not in rq.text:
print("Unconnected to the Internet!")
rq.close()
return False
else:
print("Connected to the Internet!")
rq.close()
return True
class CampusWifi(Wifi):
def __init__(self, ssid, password, userid, userpassword):
self.ssid = ssid
self.password = password
self.uid = userid
self.upassword = userpassword
def connect(self):
ret = super().connect(self.ssid, self.password)
if not ret:
print("Fail to connect to campus wifi!")
return False
Wifi.ping()
print("Logging into campus wifi·····")
ret = self._login()
if ret:
print("Login Success!")
else:
return False
print("Submmitting·····")
ret = self._submit()
if ret:
print("Submit Success!")
else:
return False
print("Testing for the Internet·····")
Wifi.ping()
print("Logging out·····")
ret = self._logout()
if ret:
print("Logout Success!")
else:
return False
print("Testing for the Internet·····")
Wifi.ping()
return True
def _login(self):
login_url = "http://202.38.64.59/cgi-bin/ip"
login_data = "cmd=login&url=URL&ip={ip}&name={uid}&password={upassword}&go=%B5%C7%C2%BC%D5%CA%BB%A7".format(ip = self.ipaddr, uid = self.uid, upassword = self.upassword)
login_headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
#"Content-Length": str(len_test), #str(len(login_data)),
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": "name=; password=",
"Host": "202.38.64.59",
"Origin": "http://202.38.64.59",
"Referer": "http://202.38.64.59/cgi-bin/ip",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
}
ret = 1
rq = requests.request(method = "POST", url = login_url, headers=login_headers, data = login_data)
if rq.status_code != 200:
print("In _login(): \n login failed with status code {sc}, \n headers:\n{hd} \n text:\n{tx}".\
format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
ret = 0
if "Set-Cookie" in rq.headers:
self.cookie_rn = rq.headers["Set-Cookie"]
else :
print("Login Failed!\nNo cookie in headers! \n headers:\n{hd}\n text:\n{tx}".format(hd = rq.headers, tx = rq.text))
ret = 0
rq.close()
return ret
def _submit(self):
try:
self.cookie_rn = self.cookie_rn
except :
raise ValueError("Please Login() before Submit()!")
submit_url = r"http://202.38.64.59/cgi-bin/ip?cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+"
submit_headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Cookie': 'name=; password=; {rn}'.format(rn = self.cookie_rn),
'Host': '202.38.64.59',
'Referer': 'http://202.38.64.59/cgi-bin/ip',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36'
}
submit_data = "cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+"
ret = 1
rq = requests.get(submit_url, headers = submit_headers, data = submit_data)
if rq.status_code != 200:
print("In _submit(): \n submit failed with status code {sc}, \n headers:\n{hd} \n text:\n{tx}".\
format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
ret = 0
rq.close()
return ret
def _logout(self):
try:
self.cookie_rn = self.cookie_rn
except :
raise ValueError("Please Login() before Logout()!")
logout_url = "http://202.38.64.59/cgi-bin/ip?cmd=logout"
logout_headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'keep-alive',
'Cookie': 'name=; password=; {rn}'.format(rn = self.cookie_rn),
'Host': '202.38.64.59',
'Referer': 'http://202.38.64.59/cgi-bin/ip?cmd=set&url=URL&type=8&exp=0&go=+%BF%AA%CD%A8%CD%F8%C2%E7+',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',
}
logout_data = "cmd=logout"
ret = 1
rq = requests.get(logout_url, headers = logout_headers, data = logout_data)
if rq.status_code != 200:
print("In _logout(): \n logout failed with status code {sc}, \n headers:\n{hd} \n text:\n{tx}". format(sc = rq.status_code, hd = rq.headers, tx = rq.text))
ret = 0
rq.close()
return ret
if __name__ == "__main__":
ssid = "ustcnet"
password = ""
user_name = "username"
user_password = "userpassword"
campuswifi = CampusWifi(ssid, password, user_name, user_password)
campuswifi.connect()
四、总结
经过本次esp32连接校园网项目的实践,笔者对于urequests库及http协议相关知识有了更深的理解,同时也认识到了micropython在某些方面的缺陷,为以后相关的开发找到了解决方案。下面列举并总结了本项目实现过程中遇到的一些问题及解决办法:
- Wifi Internal Error是wifi模块内部错误,reset无用,需要把板子断点重连一下
- OSError 32:套接字连接过多导致,注意将response对象关闭(`response.close()`)即可
- micropython 中不支持gbk编码,encode、decode仅支持str与bytes之间的utf-8转换
- HTTP 500 Internal Server Error 也可能是由于请求格式不对导致的
- urequests有自动添加`content-length`、自动解码`response.text`的bug,需要更改源码解决
以上就是esp32自动登录校园网的项目实现过程及源码,希望能给读者带来帮助~