Add file upload helper script for console module.

Plus, it can serve as a reference for any IDEs which may need/want
updating.
This commit is contained in:
Jade Mattsson 2024-10-25 15:24:14 +11:00
parent a001e41dd8
commit 0e2a757e80
3 changed files with 197 additions and 1 deletions

View File

@ -12,6 +12,10 @@ shell (REPL — Read-Execute-Print Loop). It also provides a hook for listening
in on the data received on the console programatically, and the interactivity in on the data received on the console programatically, and the interactivity
may also be disabled (and re-enabled) programatically if so desired. may also be disabled (and re-enabled) programatically if so desired.
There is a helper script (`scripts/upload-file.py`) which can be used to
easily upload files to NodeMCU via this module. The script may also be used
as inspiration for integrating such functionality into IDEs.
## console.on() ## console.on()
Used to register or deregister a callback function to handle console events. Used to register or deregister a callback function to handle console events.

View File

@ -8,7 +8,11 @@ The [UART](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver/transmi
If the UART is in use as the system console, it is unavailable for use by this If the UART is in use as the system console, it is unavailable for use by this
module. Instead, refer to the `console` module. module. Instead, refer to the `console` module.
Before using a UART, you must call `uart.setup` and `uart.start` to set them up. If your IDE does not yet support uploading files via the `console` module,
consider using the utility script `scripts/upload-file.py`, e.g.
`scripts/upload-file.py init.lua` (use `scripts/upload-file.py -h` for help).
Before using a UART, you must call `uart.setup` and `uart.start` to set it up.
## uart.on() ## uart.on()

188
scripts/upload-file.py Executable file
View File

@ -0,0 +1,188 @@
#!/usr/bin/env python3
# A helper utility to allow uploading of files to NodeMCU versions which use
# the 'console' module, rather than having the console multiplexed via the
# 'uart' module.
import argparse
import serial
import sys
import atexit
STX = 0x02
ETX = 0x03
DLE = 0x10
# The loader we send to NodeMCU so that we may upload a (binary) file safely.
# Uses STX/ETX/DLE framing and escaping.
loader = b'''
(function()
local function transmission_receiver(chunk_cb)
local inframe = false
local escaped = false
local done = false
local STX = 2
local ETX = 3
local DLE = 16
local function dispatch(data, i, j)
if (j - i) < 0 then return end
chunk_cb(data:sub(i, j))
end
return function(data)
if done then return end
local from
local to
for i = 1, #data
do
local b = data:byte(i)
if inframe
then
if not from then from = i end -- first valid byte
if escaped
then
escaped = false
else
if b == DLE
then
escaped = true
dispatch(data, from, i-1)
from = nil
elseif b == ETX
then
done = true
to = i-1
break
end
end
else -- look for an (unescaped) STX to sync frame start
if b == DLE then escaped = true
elseif b == STX and not escaped then inframe = true
else escaped = false
end
end
-- else ignore byte outside of framing
end
if from then dispatch(data, from, to or #data) end
if done then chunk_cb(nil) end
end
end
local function file_saver(name)
local f = io.open(name, "w")
return function(chunk)
if chunk then f:write(chunk)
else
f:close()
console.on("data", 0, nil)
console.mode(console.INTERACTIVE)
end
end
end
console.on("data", 0, transmission_receiver(file_saver("@FILENAME@")))
console.mode(console.NONINTERACTIVE)
end)()
'''
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="NodeMCU file uploader.")
parser.add_argument("file", help="File to read data from.")
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("-b", "--bitrate", type=int, default=115200, help="Bitrate (default: 115200).")
return parser.parse_args()
def load_file(filename):
"""Open a file and read its contents into memory."""
try:
with open(filename, "r") as f:
data = f.read()
return data
except IOError as e:
print(f"Error reading file {filename}: {e}")
sys.exit(1)
def wait_prompt(ser):
"""Wait until we see the '> ' prompt, or the serial times out"""
buf = bytearray()
b = ser.read()
while b != b'':
buf.extend(b)
if buf.find(b'> ') != -1:
return True
b = ser.read()
return False
def sync(ser):
"""Get ourselves to a clean prompt so we can understand the output"""
ser.write(b'\x03\x03\n')
wait_prompt(ser)
ser.write(b"print('sync')\n")
line = ser.readline()
while line != b"sync\n":
line = ser.readline()
return wait_prompt(ser)
def cleanup():
"""Cleanup function to send final data and close the serial port."""
if ser:
# Ensure we don't leave the console in a weird state if we get
# interrupted.
ser.write(ETX)
ser.write(ETX)
ser.write(b"\n")
ser.readline()
ser.close()
def line_interactive_send(ser, data):
"""Send one line at a time, waiting for the prompt before sending next"""
for line in data.split(b'\n'):
ser.write(line)
ser.write(b'\n')
wait_prompt(ser)
def transmission(data):
"""Perform STX/ETX/DLE framing and escaping of the data"""
out = bytearray()
out.append(STX)
for b in data:
if b == STX or b == ETX or b == DLE:
out.append(DLE)
out.append(ord(b))
out.append(ETX)
return bytes(out)
if __name__ == "__main__":
args = parse_args()
upload_name = args.name if args.name else args.file
file_data = load_file(args.file)
try:
ser = serial.Serial(args.port, args.bitrate, timeout=1)
except serial.SerialException as e:
print(f"Error opening serial port {args.port}: {e}")
sys.exit(1)
print("Synchronising serial...")
if not sync(ser):
print("NodeMCU not responding\n")
sys.exit(1)
print(f'Uploading "{args.file}" as "{upload_name}"')
atexit.register(cleanup)
print("Sending loader...")
line_interactive_send(
ser, loader.replace(b"@FILENAME@", upload_name.encode()))
print("Sending file contents...")
ser.write(transmission(file_data))
wait_prompt(ser)
ser.close()
ser = None
print("Done.")