Skip to content

Commit f9875d0

Browse files
committed
Make upload-file.py work better on CDC-ACM console.
1 parent 2e2d231 commit f9875d0

File tree

1 file changed

+99
-29
lines changed

1 file changed

+99
-29
lines changed

scripts/upload-file.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515

1616
# The loader we send to NodeMCU so that we may upload a (binary) file safely.
1717
# Uses STX/ETX/DLE framing and escaping.
18+
# The CDC-ACM console gets overwhelmed unless we throttle the send by using
19+
# an ack scheme. We use a fake prompt for simplicity's sake for that.
1820
loader = b'''
1921
(function()
2022
local function transmission_receiver(chunk_cb)
2123
local inframe = false
2224
local escaped = false
2325
local done = false
26+
local len = 0
2427
local STX = 2
2528
local ETX = 3
2629
local DLE = 16
@@ -30,6 +33,11 @@
3033
end
3134
return function(data)
3235
if done then return end
36+
len = len + #data
37+
while len >= @BLOCKSIZE@ do
38+
len = len - @BLOCKSIZE@
39+
console.write("> ")
40+
end
3341
local from
3442
local to
3543
for i = 1, #data
@@ -38,10 +46,7 @@
3846
if inframe
3947
then
4048
if not from then from = i end -- first valid byte
41-
if escaped
42-
then
43-
escaped = false
44-
else
49+
if escaped then escaped = false else
4550
if b == DLE
4651
then
4752
escaped = true
@@ -57,8 +62,7 @@
5762
else -- look for an (unescaped) STX to sync frame start
5863
if b == DLE then escaped = true
5964
elseif b == STX and not escaped then inframe = true
60-
else escaped = false
61-
end
65+
else escaped = false end
6266
end
6367
-- else ignore byte outside of framing
6468
end
@@ -70,17 +74,19 @@
7074
local function file_saver(name)
7175
local f = io.open(name, "w")
7276
return function(chunk)
73-
if chunk then f:write(chunk)
74-
else
77+
if chunk then f:write(chunk) else
7578
f:close()
7679
console.on("data", 0, nil)
7780
console.mode(console.INTERACTIVE)
81+
console.write("done")
7882
end
7983
end
8084
end
8185
82-
console.on("data", 0, transmission_receiver(file_saver("@FILENAME@")))
86+
console.on("data", 0, transmission_receiver(file_saver(
87+
"@FILENAME@")))
8388
console.mode(console.NONINTERACTIVE)
89+
console.write("ready")
8490
end)()
8591
'''
8692

@@ -91,6 +97,7 @@ def parse_args():
9197
parser.add_argument("name", nargs="?", help="Name to upload file as.")
9298
parser.add_argument("-p", "--port", default="/dev/ttyUSB0", help="Serial port (default: /dev/ttyUSB0).")
9399
parser.add_argument("-b", "--bitrate", type=int, default=115200, help="Bitrate (default: 115200).")
100+
parser.add_argument("-s", "--blocksize", type=int, default=80, help="Block size of file data, tweak for speed/reliability of upload (default: 80)")
94101
return parser.parse_args()
95102

96103
def load_file(filename):
@@ -103,26 +110,51 @@ def load_file(filename):
103110
print(f"Error reading file {filename}: {e}")
104111
sys.exit(1)
105112

106-
def wait_prompt(ser):
113+
def xprint(msg):
114+
print(msg, end='', flush=True)
115+
116+
def wait_prompt(ser, ignore):
107117
"""Wait until we see the '> ' prompt, or the serial times out"""
108118
buf = bytearray()
109119
b = ser.read()
110-
while b != b'':
111-
buf.extend(b)
120+
timeout = 5
121+
while timeout > 0:
122+
if b == b'':
123+
timeout -= 1
124+
xprint('!')
125+
else:
126+
buf.extend(b)
127+
if not ignore and buf.find(b'Lua error:') != -1:
128+
xprint(buf.decode())
129+
line = ser.readline()
130+
while line != b'':
131+
xprint(line.decode())
132+
line = ser.readline()
133+
sys.exit(1)
112134
if buf.find(b'> ') != -1:
113135
return True
114136
b = ser.read()
137+
xprint(buf.decode())
138+
return False
139+
140+
def wait_line_match(ser, match, timeout):
141+
"""Wait until the 'match' string is found within a line, or times out"""
142+
line = ser.readline()
143+
while timeout > 0:
144+
if line.find(match) != -1:
145+
return True
146+
elif line == b'':
147+
timeout -= 1
148+
xprint('!')
115149
return False
116150

117151
def sync(ser):
118152
"""Get ourselves to a clean prompt so we can understand the output"""
119153
ser.write(b'\x03\x03\n')
120-
wait_prompt(ser)
154+
if not wait_prompt(ser, True):
155+
return False
121156
ser.write(b"print('sync')\n")
122-
line = ser.readline()
123-
while line != b"sync\n":
124-
line = ser.readline()
125-
return wait_prompt(ser)
157+
return wait_line_match(ser, b'sync', 5) and wait_prompt(ser, True)
126158

127159
def cleanup():
128160
"""Cleanup function to send final data and close the serial port."""
@@ -140,7 +172,27 @@ def line_interactive_send(ser, data):
140172
for line in data.split(b'\n'):
141173
ser.write(line)
142174
ser.write(b'\n')
143-
wait_prompt(ser)
175+
if not wait_prompt(ser, False):
176+
return False
177+
xprint('.')
178+
return True
179+
180+
def chunk_data(data, size):
181+
"""Split a data block into chunks"""
182+
return (data[0+i:size+i] for i in range(0, len(data), size))
183+
184+
def chunk_interactive_send(ser, data, size):
185+
"""Send the data chunked into blocks, waiting for an ack in between"""
186+
n=0
187+
for chunk in chunk_data(data, size):
188+
ser.write(chunk)
189+
if len(chunk) == size and not wait_prompt(ser, False):
190+
print(f"failed after sending {n} blocks")
191+
return False
192+
xprint('.')
193+
n += 1
194+
print(f" ok, sent {n} blocks")
195+
return True
144196

145197
def transmission(data):
146198
"""Perform STX/ETX/DLE framing and escaping of the data"""
@@ -159,30 +211,48 @@ def transmission(data):
159211
upload_name = args.name if args.name else args.file
160212

161213
file_data = load_file(args.file)
214+
print(f"Loaded {len(file_data)} bytes of file contents")
215+
216+
blocksize = bytes(str(args.blocksize).encode())
162217

163218
try:
164-
ser = serial.Serial(args.port, args.bitrate, timeout=1)
219+
ser = serial.Serial(port=args.port, baudrate=args.bitrate, timeout=1)
165220
except serial.SerialException as e:
166221
print(f"Error opening serial port {args.port}: {e}")
167222
sys.exit(1)
168223

169-
print("Synchronising serial...")
224+
print("Synchronising serial...", end='')
170225
if not sync(ser):
171-
print("NodeMCU not responding\n")
226+
print("\nNodeMCU not responding\n")
172227
sys.exit(1)
173228

174-
print(f'Uploading "{args.file}" as "{upload_name}"')
229+
print(f' ok\nUploading "{args.file}" as "{upload_name}"')
175230

176231
atexit.register(cleanup)
177232

178-
print("Sending loader...")
179-
line_interactive_send(
180-
ser, loader.replace(b"@FILENAME@", upload_name.encode()))
233+
xprint("Sending loader")
234+
ok = line_interactive_send(
235+
ser, loader.replace(
236+
b"@FILENAME@", upload_name.encode()).replace(
237+
b"@BLOCKSIZE@", blocksize))
238+
239+
if ok:
240+
xprint(" ok\nWaiting for go-ahead...")
241+
ok = wait_line_match(ser, b"ready", 5)
181242

182-
print("Sending file contents...")
183-
ser.write(transmission(file_data))
184-
wait_prompt(ser)
243+
if ok:
244+
xprint(f" ok\nSending file contents (using blocksize {args.blocksize})")
245+
ok = chunk_interactive_send(
246+
ser, transmission(file_data), int(blocksize))
247+
if ok:
248+
xprint("Waiting for final ack...")
249+
ok = wait_line_match(ser, b"done", 5)
250+
ser.write(b"\n")
251+
252+
if not ok or not wait_prompt(ser, False):
253+
print("transmission timed out")
254+
sys.exit(1)
185255

186256
ser.close()
187257
ser = None
188-
print("Done.")
258+
print(" ok\nUpload complete.")

0 commit comments

Comments
 (0)