当前位置:   article > 正文

【esp32】espremote实现OTA在线更新程序_esp32配网更新网页

esp32配网更新网页

近日做了一个自动关灯的小东西,放在宿舍里可以避免断电后忘记关灯导致第二天"怀民亦未寝.jpg"。不过有一个问题,这东西是粘在墙上的,想要调试的话总不能搬个电脑蹲在灯旁边debug一个下午吧。正当笔者苦恼于又要买一个3m超长数据线的时候,灵光一现,想到python作为一种脚本语言,是否可以在运行时更新代码呢?  

说干就干,先想想怎么写出一个可以自己更新自己的python代码。  

一、蟒蛇的自我更新

诶,上网一查,已经有前人想出这鬼点子了,并且还给出了部分的代码(
https://www.zhihu.com/question/626768033/answer/3255532727)。  

  1. # option begin
  2. max = 200
  3. # option end
  4. def updateOption(max):
  5.   with open(__file__, 'r+') as f:
  6.     arrLines = f.readlines()
  7.     idxOptionBegin = arrLines.index('# option begin\n')
  8.     idxOptionEnd = arrLines.index('# option end\n')
  9.     for idx, optionLine in enumerate(arrLines):
  10.       if optionLine.startswith('max = ') and idxOptionBegin < idx < idxOptionEnd:
  11.         arrLines[idx] = 'max = ' + str(max) + '\n'
  12.     f.seek(0)
  13.     f.writelines(arrLines)
  14. """
  15. Author: 中等难度的贪吃蛇
  16. """

我们来copy一小段学习一下,不得不说这位答主的码风很让人赏心悦目,把python的语法糖用的恰到好处。话归正题,这段代码用注释`#option begin/end`标注了可修改的区间,`__file__`是python内置变量,即为文件自身的名字,总体上先读取所有代码行,把待修改行进行修改后全部输出到源文件中去。  

复制粘贴好是好,但是不实操总有东西弄不懂。比方如果在执行过程中把后面的语句删除了,那么他还会执行吗?  

简单写下代码

  1. with open(__file__, 'w+') as f:
  2.     f.write("111\n" * 3)
  3.     f.write("222")

运行一下会发现原文件变成如下

  1. 111
  2. 111
  3. 111
  4. 222

可见python在运行之初,会将所有的代码纳入内存之中,即使修改代码文件,也不会对运行结果造成影响。  

那现在再来看一下esp32上面的micropython是否支持这一特性吧。毕竟micropython有些库不太稳定,让笔者曾一度怀疑它的实力。简单修改一下上面”贪吃蛇“的代码,写了一段更好看的小段代码。

  1. # option begin
  2. param = 0
  3. # option end
  4. def updateOption(**kwargs):
  5.     with open(__file__, 'r+') as f:
  6.         arrLines = f.readlines()
  7.         idxOptionBegin = arrLines.index('# option begin\n')
  8.         idxOptionEnd = arrLines.index('# option end\n')
  9.         for idx, optionLine in enumerate(arrLines):
  10.             if not idxOptionBegin < idx < idxOptionEnd:
  11.                 continue
  12.             
  13.             for key in kwargs:
  14.                 if optionLine.startswith(key + ' = '):
  15.                     arrLines[idx] = key + ' = ' + str(kwargs[key]) + '\n'
  16.             
  17.     with open(__file__, "w+") as f:
  18.         f.writelines(arrLines)
  19. updateOption(param = param + 1)
  20. print(param)

这段代码在pc上运行的结果是每次运行param都会加1,将其改名为“main.py”,放在esp32之中尝试运行后重新打开文件,`param = 1`!看来mipy是支持这种热修改的,太棒了。  

二、Link Start!

那么一切顺利,我们现在可以考虑如何让电脑端传出的待更新代码传到esp32上面去。  

有几种方法,一是把代码放到公网上,然后esp32再从公网上将代码取下来;二是局域网通信,esp32和pc端直接进行通信。乍一看似乎法一好些,毕竟可以身处天涯而心系esp32,在哪里都可以更新,但其实有很大的弊端:首先存放代码的公网服务器的延迟都不低,效率和稳定性不如局域网,再者局域网可以实现指令的即时传递,进而实现对esp32文件系统更加便捷的控制,最后就是那个代码桶的网站是境外的,小esp32翻不了墙。  

局域网通信的话就又有问题要考虑了。公网上的ip总是固定的,但局域网内的ip每次断电重连都会变化。总不能从127.0.0.0到255.255.255.255挨个试吧。诶,255.255.255.255?广播!我们可以用udp把自己的地址广播出去(应该不会收到二向箔吧),然后等待另一方的连接。  

好,那么我们就可以初步利用udp的广播和tcp协议实现局域网未知ip匹配并进行消息传递的功能。  

下面给出的是esp32端的核心代码

  1. # 在之前需要让esp32连接wifi,获取本地ip,并定义好通信的PORT
  2. # 至于如何连接wifi可以见我之前发的博客
  3. udp_addr = ('255.255.255.255', PORT)
  4. udp_sock = socket(AF_INET, SOCK_DGRAM)
  5. tcp_addr = (ip, PORT)
  6. tcp_listen_sock = socket(AF_INET, SOCK_STREAM)
  7. tcp_listen_sock.settimeout(1)  # 这里由于esp32在连接上位机之外还要进行其他操作,所以设置了超时
  8. tcp_listen_sock.bind(tcp_addr)
  9. tcp_listen_sock.listen(1)
  10. conn, addr = None, None
  11. print("waiting for connect...")
  12. while True:
  13.     # 广播
  14.     # 其实没必要把自己的ip放进数据中,广播的接收方可以接收到发送方的ip的
  15.     message = "[HI]"
  16.     udp_sock.sendto(message, udp_addr) 
  17.     # 接收tcp连接
  18.     try:
  19.         conn, addr = tcp_listen_sock.accept()
  20.         print(conn, addr, "connected")
  21.         break
  22.     except Exception as exc:
  23.         # etimedout是阻塞超时异常,eagain是非阻塞没接收到信息抛出的异常
  24.         if str(exc) == "[Errno 116] ETIMEDOUT" or str(exc) == "[Errno 11] EAGAIN":
  25.             pass
  26.         else:
  27.             raise OSError(exc)
  28. if (conn, addr) == (None, None):
  29.     udp_sock.close()
  30.     sys.exit()
  31. # 连接成功
  32.     
  33. conn.settimeout(5.0)
  34. while True:
  35.     data = conn.recv(1024)
  36.     if len(data) == 0:     #判断客户端是否断开连接
  37.         print("close socket")
  38.         conn.close()
  39.         break
  40.     print(data)
  41.     ret = conn.send(data)
  42. udp_sock.close()

接下来是pc端的核心代码:

  1. address = ('', PORT)
  2. s = socket(AF_INET, SOCK_DGRAM)
  3. s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
  4. s.bind(address)
  5. # udp sock等待接收广播
  6. while True:
  7.     print(' wait recv...')
  8.     data, address = s.recvfrom(1024)
  9.     print(' [recv form %s:%d]:%s' % (address[0], address[1], data))
  10.     s.close()
  11.     if data.decode() == "[HI]":
  12.         break
  13.     
  14. IPADDR = address[0]
  15. # tcp连接
  16. tcp_client_socket = socket(AF_INET, SOCK_STREAM)
  17. server_ip, server_port = IPADDR, PORT
  18. tcp_client_socket.connect((server_ip, server_port))
  19. # 连接成功
  20. send_data = input("请输入要发送的数据:")
  21. tcp_client_socket.send(send_data.encode())
  22. recvData = tcp_client_socket.recv(1024)
  23. print('接收到的数据为:', recvData.decode())
  24. tcp_client_socket.close()

三、抽象,抽象,更加抽象

在成功实现广播连接之后,实现OTA在线更新的大厦已经落成,所剩只是一点修饰的工作了。两小朵乌云之一是定义好各个指令的协议,并将这个esp32上位机模块和esp32OTA模块封装起来,之二且待后文详说。  

首先我们要将recv、send等等函数封装一下,因为发送接收时会出现各种问题,不是掉链接,没回应,超时,就是回应的很奇怪,不符合预期。而这些问题的重要性也都不同,掉链接的话可以直接结束ota程序了,其他的话有的可以再试试,有的根本不用在意。  

所以我们先定义了几种返回值,然后对两个关键函数封装了一下。

  1. SUCCESS = 0
  2. NOTICE = 1
  3. WARNING = 2
  4. ERROR = 3
  5. CRITICAL = 4
  6. class UpperComputer(object):
  7.     def recv_signal(self, description_of_this_time, expected_dat = None):
  8.         try:
  9.             dat = self.conn.recv(1024).decode()
  10.             if len(dat) == 0:
  11.                 log.print_only("upper comp lost connection when {}".format(description_of_this_time))
  12.                 return "", CRITICAL
  13.             
  14.             if expected_dat != None and dat != expected_dat:
  15.                 log.error("Response Invalid when {}. (res: {})".format(description_of_this_time, dat))
  16.                 return dat, ERROR
  17.             return dat, SUCCESS
  18.         
  19.         except Exception as exc:
  20.             log.error("No Response when {}.(exc: {})".format(description_of_this_time, exc))
  21.             return "", ERROR
  22.         
  23.     def send_signal(self, dat):
  24.         try:
  25.             self.conn.send(dat.encode())
  26.             return True, SUCCESS
  27.         except Exception as exc:
  28.             log.error("Send Failed {}. {}".format(dat, exc))
  29.             return False, CRITICAL

这样封装的好处是每次不需要进行繁复的try-except,只需要判断返回值是否正常即可。同时还避免了重复的log语句。  

接下来是初始化函数,对udpsock和tcpsock分别进行初始化;广播连接函数已经介绍过了,之后是指令处理函数。需要注意tcp协议下如果连续发送消息,那么接收方可能一次recv收到对方两次send的内容,就可能会导致invalid response或者no reponse。为了避免这种情况,应该双方一唱一和,A发完消息B要回复收到。所以对于上位机发送的有后续补充信息的指令,要回复“ready”,没有补充信息的,就只需要回复"finish"或者查询的值即可。下面是esp32上用于和上位机通信的模块,pc端用于通信的模块类似,只是没有operation_handler()。  

  1. class UpperComputer (object):
  2.     def __init__(self, self_ip, port):
  3.         self.port = port
  4.         self.ip = self_ip
  5.         
  6.         self.usock = socket(..)
  7.         self.tsock = socket(..)
  8.         
  9.     def broadcast_and_connect(self):
  10.         # udp broadcast
  11.         # tcp connect
  12.         return True
  13.     
  14.     def operation_handler(self):
  15.         
  16.         dat, ret = self.recv_signal("ready to recv operation")
  17.         if ret >= WARNING:
  18.             return ret
  19.         
  20.         if dat == "[UPDATE]":
  21.             self.send_signal("[READY]")
  22.             return self.update()
  23.         # ...
  24.         elif dat == '[GETFILELIST]':
  25.             return self.get_file_list()
  26.         else:
  27.             self.conn.send(dat.encode()) # echo
  28.             return NOTICE

经过这样的抽象处理,OTA的功能就已经实现了,并且使用起来很简便。

  1. FILE_ROOT = "D:\\我的文档\\学习\\ESP32\\update\\"
  2. SEND_FILE_DICT = {"下位机程序.py": "main.py"} # key代表pc端的文件名称,value代表需要保存到esp32中的位置
  3. SAVE_PATH = "D:\\我的文档\\学习\\ESP32\\download\\"
  4. DOWNLOAD_LIST = ['log.txt', 'main.py']
  5. if __name__ == "__main__":
  6.     esp32 = ESP32Ctrl(PORT)
  7.     esp32.connect()
  8.     # esp32.update(FILE_ROOT, SEND_FILE_DICT)
  9.     # esp32.reboot()
  10.     # esp32.download(DOWNLOAD_LIST, SAVE_PATH)
  11.     file_list, _ = esp32.get_file_list()
  12.     print(file_list)
  13.     esp32.delete("log.txt")
  14.     
  15.     esp32.close()

四、ESPREMOTE

但是每次更新都需要去改pc端的代码,比方需要上传或下载什么文件、想要获取文件列表还要重新改代码运行一次,颇有些麻烦。笔者想到初次接触到esp32时,用到了一个模块叫做ampy。这个模块利用命令行实现了对esp32上放置、移除文件、运行代码等操作,我们是否也可以把我们的上位机功能做成命令行呢?  

python在实现命令行中os模块非常有用,os.path可以对文件路径进行诸多操作,os.chdir还可以自动记录当前所在文件夹,不用手动判断文件夹是否存在了。

  1. class COMMANDER(object):
  2.     def command_handler(self, command):
  3.         arg_list = command.split(" ")
  4.         op = arg_list [0].lower()
  5.         # cd 进入文件夹
  6.         if op in ['cd']:
  7.             try:
  8.                 command = command[len(op + " ") : ]
  9.                 command.strip("\"\'")
  10.                 os.chdir( './' + command)
  11.                 
  12.                 self.update_cwd()
  13.             except Exception as exc:
  14.                 print(Fore.RED + f"ERROR: {exc}" + Fore.RESET)
  15.             return
  16.         
  17.         # elif ..:
  18.         # else:

我们现将本模块称为espremote,调用指令为espremote command [args]。下面给出了espremote的实现方法和效果展示。

  1.         elif op in ['espremote']:
  2.             if len(arg_list) == 1 or arg_list[1] == 'help':
  3.                 print(help_info['help'])
  4.                 return
  5.             op2 = arg_list[1].lower()
  6.             arg_list = arg_list[2:]
  7.             
  8.             if op2 in ['conn']:
  9.                 esp32.connect()
  10.                 return
  11.             # elif ..:
  12.             # else:


Espremote模块是一个能够给予特定协议远程控制esp32开发板的实用工具。利用Espremote可以远程连接板子、管理文件系统以及上传、下载文件。  

该模块的pc端、esp32端模块和示例程序已经放在本人的仓库里了,欢迎大家取用。

本文首发于个人博客,欢迎访问。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号