This is the third article in a series about using ESP-Now to implement an ESP32-based master-slave network. Part one describes the main network comms class and part two describes how to follow the system router when it channel-hops. In this final article I'll describe an implementation of over-the-air (OTA) updates to the Python and other files held on the devices.
In part one of this series, when a message is received it is handed to a message handler, which carries out the needed action(s) and returns a result. The following is that message handler. Most of it is directly applicable to any similar application, since there are only so many things a network slave is able to do.
import asyncio,machine,os
from binascii import unhexlify
from files import readFile,writeFile,renameFile,deleteFile,createDirectory
class Handler():
def __init__(self,config):
self.config=config
config.setHandler(self)
self.relay=config.getRelay()
self.saveError=False
def handleMessage(self,msg):
# print('Handler:',msg)
bleValues=self.config.getBLEValues()
response=f'OK {self.config.getUptime()} :{bleValues}'
if msg=='uptime':
pass
elif msg == 'on':
response=f'{response} ON' if self.relay.on() else 'No relay'
elif msg == 'off':
response=f'{response} OFF' if self.relay.off() else 'No relay'
elif msg == 'relay':
try:
response=f'OK {self.relay.getState()}'
except:
response='No relay'
elif msg=='reset':
self.config.reset()
response='OK'
elif msg == 'ipaddr':
response=f'OK {self.config.getIPAddr()}'
elif msg=='channel':
response=f'OK {self.config.getChannel()}'
elif msg[0:8]=='channel=':
channel=msg[8:]
self.config.setChannel(channel)
response=f'OK {channel}'
elif msg[0:7]=='environ':
environ=msg[8:]
self.config.setEnviron(environ)
response=f'OK {environ}'
elif msg=='temp':
response=f'OK {self.config.getTemperature()}'
elif msg=='pause':
self.config.pause()
response=f'OK paused'
elif msg=='resume':
self.config.resume()
response=f'OK resumed'
elif msg[0:6]=='delete':
file=msg[7:]
response='OK' if deleteFile(file) else 'Fail'
elif msg[0:4]=='part':
# Format is part:{n},text:{text}
part=None
text=None
items=msg.split(',')
for item in items:
item=item.split(':')
label=item[0]
value=item[1]
if label=='part': part=int(value)
elif label=='text': text=value
if part!=None and text!=None:
text=text.encode('utf-8')
text=unhexlify(text)
text=text.decode('utf-8')
if part==0:
self.buffer=[]
self.pp=0
self.saveError=False
else:
if self.saveError:
return 'Save error'
else:
if part==self.pp+1:
self.pp=part
else:
self.saveError=True
return f'Sequence error: {part} {self.pp+1}'
self.buffer.append(text)
response=f'{part} {str(len(text))}'
elif msg[0:4]=='save':
if len(self.buffer[0])>0:
fname=msg[5:]
tname=f't-{fname}'
print(f'Save {tname}')
size=0
f = open(tname,'w')
for n in range(0, len(self.buffer)):
f.write(self.buffer[n])
size+= len(self.buffer[n])
f.close()
# Check the file against the buffer
if self.checkFile(self.buffer, tname):
response=str(size)
print('File saved')
else: response='Bad save'
else: response='No data'
text=None
elif msg=='update':
for tname in os.listdir('.'):
if tname[0:2]=='t-':
fname=tname[2:]
print(f'Update {fname}')
deleteFile(fname)
renameFile(tname,fname)
response='OK'
elif msg[0:5]=='mkdir':
path=msg[6:]
print(f'mkdir {path}')
response='OK' if createDirectory(path) else 'Fail'
else: response=f'Unknown message: {msg}'
# print('Handler response:',response)
return response
def checkFile(self, buf, file):
try:
with open(file, 'r') as f:
i = 0 # index in lst
pos = 0 # position in current list item
while True:
c = f.read(1)
if not c:
# End of file: check if we've also finished the list
while i < len(buf) and pos == len(buf[i]):
i += 1
pos = 0
return i == len(buf)
if i >= len(buf) or pos >= len(buf[i]) or c != buf[i][pos]:
return False
pos += 1
if pos == len(buf[i]):
i += 1
pos = 0
except OSError:
return False
__init__()
The system makes extensive use of a Config
class, which holds all the system-specific configuration data such as which I/O pins do what. It also acts as a central message interchange, with functions that either return an object that has the required functionality or call the objects themselves, avoiding the need for every class to know about all the others. So in this Handler
class, the relay object supplied by Config
is a class that has the ability to turn the relay on and off and to return the current relay state.
handleMessage()
This is where most of the work is done. The function handles a set of commands, most of which should be fairly obvious, though some will be more relevant than others. For example, elsewhere in the code there's a Bluetooth (BLE) module that collects occasional transmissions from specific devices and for some message types appends these values to the string that is returned to the caller. In my system these are temperature values, but not everyone will want this feature.
The most interesting part of this is the OTA file updater, which works like this. The maximum size of an ESP-Now data packet is something over 200 bytes, so few Python scripts can be transferred in a single message. I chose to send 100 bytes at a time, with the text data encoded as hex values. The file is sent as a series of messages, each containing 100 bytes, and the messages are prefixed by the command part
and a part number starting with zero. The function splits up the packet to extract the part number and the data itself. If the part number matches what was expected, the text is added to a list, otherwise the function returns an error. For a successfully received part, the function returns the part number and the size of the data, which the sender can use to verify that it has been received successfully.
When all the parts have been sent, the list can be saved to a temporary file. This is done by the save
command.
checkFile(()
Now we need to check the data has not been corrupted. This function reads the file back, one character at a time, and compares it with the contents of the list. If no errors are found, the new file can be assumed to be good.
However, a problem comes when several Python files all need to be updated together. In many cases, if only some of the files are updated and then an error occurs or for some other reason the update stops, there will be a mismatch between the various parts and the program will not run after the next reboot. So rather than update files one by one they should instead be done as a batch. Only when all of the files have been saved and checked is it then safe to rename the entire batch. This is done by the update
command.
In a practical implementation, when an error is discovered and reported back to the sender, the entire save can be retried. Although for the most part OTA updates proceed without a hitch, in some cases, particularly where the signal strength isn't great or suffers from interference, it can take several attempts to transfer a large file. I allow up to 10 retries before abandoning the save.
Top comments (0)