15
15
16
16
# The loader we send to NodeMCU so that we may upload a (binary) file safely.
17
17
# 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.
18
20
loader = b'''
19
21
(function()
20
22
local function transmission_receiver(chunk_cb)
21
23
local inframe = false
22
24
local escaped = false
23
25
local done = false
26
+ local len = 0
24
27
local STX = 2
25
28
local ETX = 3
26
29
local DLE = 16
30
33
end
31
34
return function(data)
32
35
if done then return end
36
+ len = len + #data
37
+ while len >= @BLOCKSIZE@ do
38
+ len = len - @BLOCKSIZE@
39
+ console.write("> ")
40
+ end
33
41
local from
34
42
local to
35
43
for i = 1, #data
38
46
if inframe
39
47
then
40
48
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
45
50
if b == DLE
46
51
then
47
52
escaped = true
57
62
else -- look for an (unescaped) STX to sync frame start
58
63
if b == DLE then escaped = true
59
64
elseif b == STX and not escaped then inframe = true
60
- else escaped = false
61
- end
65
+ else escaped = false end
62
66
end
63
67
-- else ignore byte outside of framing
64
68
end
70
74
local function file_saver(name)
71
75
local f = io.open(name, "w")
72
76
return function(chunk)
73
- if chunk then f:write(chunk)
74
- else
77
+ if chunk then f:write(chunk) else
75
78
f:close()
76
79
console.on("data", 0, nil)
77
80
console.mode(console.INTERACTIVE)
81
+ console.write("done")
78
82
end
79
83
end
80
84
end
81
85
82
- console.on("data", 0, transmission_receiver(file_saver("@FILENAME@")))
86
+ console.on("data", 0, transmission_receiver(file_saver(
87
+ "@FILENAME@")))
83
88
console.mode(console.NONINTERACTIVE)
89
+ console.write("ready")
84
90
end)()
85
91
'''
86
92
@@ -91,6 +97,7 @@ def parse_args():
91
97
parser .add_argument ("name" , nargs = "?" , help = "Name to upload file as." )
92
98
parser .add_argument ("-p" , "--port" , default = "/dev/ttyUSB0" , help = "Serial port (default: /dev/ttyUSB0)." )
93
99
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)" )
94
101
return parser .parse_args ()
95
102
96
103
def load_file (filename ):
@@ -103,26 +110,51 @@ def load_file(filename):
103
110
print (f"Error reading file { filename } : { e } " )
104
111
sys .exit (1 )
105
112
106
- def wait_prompt (ser ):
113
+ def xprint (msg ):
114
+ print (msg , end = '' , flush = True )
115
+
116
+ def wait_prompt (ser , ignore ):
107
117
"""Wait until we see the '> ' prompt, or the serial times out"""
108
118
buf = bytearray ()
109
119
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 )
112
134
if buf .find (b'> ' ) != - 1 :
113
135
return True
114
136
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 ('!' )
115
149
return False
116
150
117
151
def sync (ser ):
118
152
"""Get ourselves to a clean prompt so we can understand the output"""
119
153
ser .write (b'\x03 \x03 \n ' )
120
- wait_prompt (ser )
154
+ if not wait_prompt (ser , True ):
155
+ return False
121
156
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 )
126
158
127
159
def cleanup ():
128
160
"""Cleanup function to send final data and close the serial port."""
@@ -140,7 +172,27 @@ def line_interactive_send(ser, data):
140
172
for line in data .split (b'\n ' ):
141
173
ser .write (line )
142
174
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
144
196
145
197
def transmission (data ):
146
198
"""Perform STX/ETX/DLE framing and escaping of the data"""
@@ -159,30 +211,48 @@ def transmission(data):
159
211
upload_name = args .name if args .name else args .file
160
212
161
213
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 ())
162
217
163
218
try :
164
- ser = serial .Serial (args .port , args .bitrate , timeout = 1 )
219
+ ser = serial .Serial (port = args .port , baudrate = args .bitrate , timeout = 1 )
165
220
except serial .SerialException as e :
166
221
print (f"Error opening serial port { args .port } : { e } " )
167
222
sys .exit (1 )
168
223
169
- print ("Synchronising serial..." )
224
+ print ("Synchronising serial..." , end = '' )
170
225
if not sync (ser ):
171
- print ("NodeMCU not responding\n " )
226
+ print ("\n NodeMCU not responding\n " )
172
227
sys .exit (1 )
173
228
174
- print (f'Uploading "{ args .file } " as "{ upload_name } "' )
229
+ print (f' ok \n Uploading "{ args .file } " as "{ upload_name } "' )
175
230
176
231
atexit .register (cleanup )
177
232
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\n Waiting for go-ahead..." )
241
+ ok = wait_line_match (ser , b"ready" , 5 )
181
242
182
- print ("Sending file contents..." )
183
- ser .write (transmission (file_data ))
184
- wait_prompt (ser )
243
+ if ok :
244
+ xprint (f" ok\n Sending 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 )
185
255
186
256
ser .close ()
187
257
ser = None
188
- print ("Done ." )
258
+ print (" ok \n Upload complete ." )
0 commit comments