Make upload-file.py work better on CDC-ACM console.

This commit is contained in:
Jade Mattsson 2024-10-27 14:34:15 +11:00
parent 2e2d231237
commit f9875d0361
1 changed files with 99 additions and 29 deletions

View File

@ -15,12 +15,15 @@ DLE = 0x10
# The loader we send to NodeMCU so that we may upload a (binary) file safely. # The loader we send to NodeMCU so that we may upload a (binary) file safely.
# Uses STX/ETX/DLE framing and escaping. # Uses STX/ETX/DLE framing and escaping.
# The CDC-ACM console gets overwhelmed unless we throttle the send by using
# an ack scheme. We use a fake prompt for simplicity's sake for that.
loader = b''' loader = b'''
(function() (function()
local function transmission_receiver(chunk_cb) local function transmission_receiver(chunk_cb)
local inframe = false local inframe = false
local escaped = false local escaped = false
local done = false local done = false
local len = 0
local STX = 2 local STX = 2
local ETX = 3 local ETX = 3
local DLE = 16 local DLE = 16
@ -30,6 +33,11 @@ loader = b'''
end end
return function(data) return function(data)
if done then return end if done then return end
len = len + #data
while len >= @BLOCKSIZE@ do
len = len - @BLOCKSIZE@
console.write("> ")
end
local from local from
local to local to
for i = 1, #data for i = 1, #data
@ -38,10 +46,7 @@ loader = b'''
if inframe if inframe
then then
if not from then from = i end -- first valid byte if not from then from = i end -- first valid byte
if escaped if escaped then escaped = false else
then
escaped = false
else
if b == DLE if b == DLE
then then
escaped = true escaped = true
@ -57,8 +62,7 @@ loader = b'''
else -- look for an (unescaped) STX to sync frame start else -- look for an (unescaped) STX to sync frame start
if b == DLE then escaped = true if b == DLE then escaped = true
elseif b == STX and not escaped then inframe = true elseif b == STX and not escaped then inframe = true
else escaped = false else escaped = false end
end
end end
-- else ignore byte outside of framing -- else ignore byte outside of framing
end end
@ -70,17 +74,19 @@ loader = b'''
local function file_saver(name) local function file_saver(name)
local f = io.open(name, "w") local f = io.open(name, "w")
return function(chunk) return function(chunk)
if chunk then f:write(chunk) if chunk then f:write(chunk) else
else
f:close() f:close()
console.on("data", 0, nil) console.on("data", 0, nil)
console.mode(console.INTERACTIVE) console.mode(console.INTERACTIVE)
console.write("done")
end end
end end
end end
console.on("data", 0, transmission_receiver(file_saver("@FILENAME@"))) console.on("data", 0, transmission_receiver(file_saver(
"@FILENAME@")))
console.mode(console.NONINTERACTIVE) console.mode(console.NONINTERACTIVE)
console.write("ready")
end)() end)()
''' '''
@ -91,6 +97,7 @@ def parse_args():
parser.add_argument("name", nargs="?", help="Name to upload file as.") parser.add_argument("name", nargs="?", help="Name to upload file as.")
parser.add_argument("-p", "--port", default="/dev/ttyUSB0", help="Serial port (default: /dev/ttyUSB0).") parser.add_argument("-p", "--port", default="/dev/ttyUSB0", help="Serial port (default: /dev/ttyUSB0).")
parser.add_argument("-b", "--bitrate", type=int, default=115200, help="Bitrate (default: 115200).") parser.add_argument("-b", "--bitrate", type=int, default=115200, help="Bitrate (default: 115200).")
parser.add_argument("-s", "--blocksize", type=int, default=80, help="Block size of file data, tweak for speed/reliability of upload (default: 80)")
return parser.parse_args() return parser.parse_args()
def load_file(filename): def load_file(filename):
@ -103,26 +110,51 @@ def load_file(filename):
print(f"Error reading file {filename}: {e}") print(f"Error reading file {filename}: {e}")
sys.exit(1) sys.exit(1)
def wait_prompt(ser): def xprint(msg):
print(msg, end='', flush=True)
def wait_prompt(ser, ignore):
"""Wait until we see the '> ' prompt, or the serial times out""" """Wait until we see the '> ' prompt, or the serial times out"""
buf = bytearray() buf = bytearray()
b = ser.read() b = ser.read()
while b != b'': timeout = 5
buf.extend(b) while timeout > 0:
if b == b'':
timeout -= 1
xprint('!')
else:
buf.extend(b)
if not ignore and buf.find(b'Lua error:') != -1:
xprint(buf.decode())
line = ser.readline()
while line != b'':
xprint(line.decode())
line = ser.readline()
sys.exit(1)
if buf.find(b'> ') != -1: if buf.find(b'> ') != -1:
return True return True
b = ser.read() b = ser.read()
xprint(buf.decode())
return False
def wait_line_match(ser, match, timeout):
"""Wait until the 'match' string is found within a line, or times out"""
line = ser.readline()
while timeout > 0:
if line.find(match) != -1:
return True
elif line == b'':
timeout -= 1
xprint('!')
return False return False
def sync(ser): def sync(ser):
"""Get ourselves to a clean prompt so we can understand the output""" """Get ourselves to a clean prompt so we can understand the output"""
ser.write(b'\x03\x03\n') ser.write(b'\x03\x03\n')
wait_prompt(ser) if not wait_prompt(ser, True):
return False
ser.write(b"print('sync')\n") ser.write(b"print('sync')\n")
line = ser.readline() return wait_line_match(ser, b'sync', 5) and wait_prompt(ser, True)
while line != b"sync\n":
line = ser.readline()
return wait_prompt(ser)
def cleanup(): def cleanup():
"""Cleanup function to send final data and close the serial port.""" """Cleanup function to send final data and close the serial port."""
@ -140,7 +172,27 @@ def line_interactive_send(ser, data):
for line in data.split(b'\n'): for line in data.split(b'\n'):
ser.write(line) ser.write(line)
ser.write(b'\n') ser.write(b'\n')
wait_prompt(ser) if not wait_prompt(ser, False):
return False
xprint('.')
return True
def chunk_data(data, size):
"""Split a data block into chunks"""
return (data[0+i:size+i] for i in range(0, len(data), size))
def chunk_interactive_send(ser, data, size):
"""Send the data chunked into blocks, waiting for an ack in between"""
n=0
for chunk in chunk_data(data, size):
ser.write(chunk)
if len(chunk) == size and not wait_prompt(ser, False):
print(f"failed after sending {n} blocks")
return False
xprint('.')
n += 1
print(f" ok, sent {n} blocks")
return True
def transmission(data): def transmission(data):
"""Perform STX/ETX/DLE framing and escaping of the data""" """Perform STX/ETX/DLE framing and escaping of the data"""
@ -159,30 +211,48 @@ if __name__ == "__main__":
upload_name = args.name if args.name else args.file upload_name = args.name if args.name else args.file
file_data = load_file(args.file) file_data = load_file(args.file)
print(f"Loaded {len(file_data)} bytes of file contents")
blocksize = bytes(str(args.blocksize).encode())
try: try:
ser = serial.Serial(args.port, args.bitrate, timeout=1) ser = serial.Serial(port=args.port, baudrate=args.bitrate, timeout=1)
except serial.SerialException as e: except serial.SerialException as e:
print(f"Error opening serial port {args.port}: {e}") print(f"Error opening serial port {args.port}: {e}")
sys.exit(1) sys.exit(1)
print("Synchronising serial...") print("Synchronising serial...", end='')
if not sync(ser): if not sync(ser):
print("NodeMCU not responding\n") print("\nNodeMCU not responding\n")
sys.exit(1) sys.exit(1)
print(f'Uploading "{args.file}" as "{upload_name}"') print(f' ok\nUploading "{args.file}" as "{upload_name}"')
atexit.register(cleanup) atexit.register(cleanup)
print("Sending loader...") xprint("Sending loader")
line_interactive_send( ok = line_interactive_send(
ser, loader.replace(b"@FILENAME@", upload_name.encode())) ser, loader.replace(
b"@FILENAME@", upload_name.encode()).replace(
b"@BLOCKSIZE@", blocksize))
print("Sending file contents...") if ok:
ser.write(transmission(file_data)) xprint(" ok\nWaiting for go-ahead...")
wait_prompt(ser) ok = wait_line_match(ser, b"ready", 5)
if ok:
xprint(f" ok\nSending file contents (using blocksize {args.blocksize})")
ok = chunk_interactive_send(
ser, transmission(file_data), int(blocksize))
if ok:
xprint("Waiting for final ack...")
ok = wait_line_match(ser, b"done", 5)
ser.write(b"\n")
if not ok or not wait_prompt(ser, False):
print("transmission timed out")
sys.exit(1)
ser.close() ser.close()
ser = None ser = None
print("Done.") print(" ok\nUpload complete.")