nodemcu-uploader.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. #!/usr/bin/env python
  2. # Copyright (C) 2015 Peter Magnusson
  3. # For NodeMCU version 0.9.4 build 2014-12-30 and newer.
  4. import os
  5. import serial
  6. import sys
  7. import argparse
  8. import time
  9. import logging
  10. log = logging.getLogger(__name__)
  11. def minify(script):
  12. return ' '.join([line.strip() for line in script.split('\n')])
  13. save_lua = \
  14. r"""
  15. function recv_block(d)
  16. if string.byte(d, 1) == 1 then
  17. size = string.byte(d, 2)
  18. if size > 0 then
  19. file.write(string.sub(d, 3, 3+size-1))
  20. uart.write(0,'\006')
  21. else
  22. uart.write(0,'\006')
  23. file.close()
  24. uart.on('data')
  25. uart.setup(0,9600,8,0,1,1)
  26. end
  27. else
  28. uart.write(0, '\021' .. d)
  29. uart.setup(0,9600,8,0,1,1)
  30. uart.on('data')
  31. end
  32. end
  33. function recv_name(d)
  34. d = string.gsub(d, '\000', '')
  35. file.remove(d)
  36. file.open(d, 'w')
  37. uart.on('data', 130, recv_block, 0)
  38. uart.write(0, '\006')
  39. end
  40. function recv()
  41. uart.setup(0,9600,8,0,1,0)
  42. uart.on('data', '\000', recv_name, 0)
  43. uart.write(0, 'C')
  44. end
  45. """
  46. #save_lua = minify(save_lua)
  47. #save_lua = ' '.join([line.strip().replace(', ', ',') for line in save_lua.split('\n')])
  48. CHUNK_END = '\v'
  49. CHUNK_REPLY = '\v'
  50. class Uploader:
  51. BAUD = 9600
  52. PORT = '/dev/ttyUSB0'
  53. TIMEOUT = 5
  54. def expect(self, exp='> ', timeout=TIMEOUT):
  55. t = self._port.timeout
  56. # Checking for new data every 100us is fast enough
  57. lt = 0.0001
  58. if self._port.timeout != lt:
  59. self._port.timeout = lt
  60. end = time.time() + timeout
  61. # Finish as soon as either exp matches or we run out of time (work like dump, but faster on success)
  62. data = ''
  63. while not data.endswith(exp) and time.time() <= end:
  64. data += self._port.read()
  65. self._port.timeout = t
  66. log.debug('expect return: %s', data)
  67. return data
  68. def write(self, output, binary=False):
  69. if not binary:
  70. log.debug('write: %s', output)
  71. else:
  72. log.debug('write binary: %s' % ':'.join(x.encode('hex') for x in output))
  73. self._port.write(output)
  74. self._port.flush()
  75. def writeln(self, output):
  76. self.write(output + '\n')
  77. def exchange(self, output):
  78. self.writeln(output)
  79. return self.expect()
  80. def __init__(self, port = 0, baud = BAUD):
  81. self._port = serial.Serial(port, Uploader.BAUD, timeout=Uploader.TIMEOUT)
  82. # Keeps things working, if following conections are made:
  83. ## RTS = CH_PD (i.e reset)
  84. ## DTR = GPIO0
  85. self._port.setRTS(False)
  86. self._port.setDTR(False)
  87. # Get in sync with LUA (this assumes that NodeMCU gets reset by the previous two lines)
  88. self.exchange(';'); # Get a defined state
  89. self.writeln('print("%sync%");');
  90. self.expect('%sync%\r\n> ');
  91. if baud != Uploader.BAUD:
  92. log.info('Changing communication to %s baud', baud)
  93. self.writeln('uart.setup(0,%s,8,0,1,1)' % baud)
  94. # Wait for the string to be sent before switching baud
  95. time.sleep(0.1)
  96. self._port.setBaudrate(baud)
  97. # Get in sync again
  98. self.exchange('')
  99. self.exchange('')
  100. self.line_number = 0
  101. def close(self):
  102. self.writeln('uart.setup(0,%s,8,0,1,1)' % Uploader.BAUD)
  103. self._port.close()
  104. def prepare(self):
  105. log.info('Preparing esp for transfer.')
  106. data = save_lua.replace('9600', '%d' % self._port.baudrate)
  107. lines = data.replace('\r', '').split('\n')
  108. for line in lines:
  109. d = self.exchange(line)
  110. if 'unexpected' in d or len(d) > len(save_lua)+10:
  111. log.error('error in save_lua "%s"' % d)
  112. return
  113. def download_file(self, filename):
  114. d = self.exchange(r"file.open('" + filename + r"') print(file.seek('end', 0)) file.seek('set', 0) uart.write(0, file.read()) file.close()")
  115. cmd, size, data = d.split('\n', 2)
  116. data = data[0:int(size)]
  117. return data
  118. def read_file(self, filename, destination = ''):
  119. if not destination:
  120. destination = filename
  121. log.info('Transfering %s to %s' %(filename, destination))
  122. data = self.download_file(filename)
  123. with open(destination, 'w') as f:
  124. f.write(data)
  125. def write_file(self, path, destination = '', verify = False):
  126. filename = os.path.basename(path)
  127. if not destination:
  128. destination = filename
  129. log.info('Transfering %s as %s' %(filename, destination))
  130. self.writeln("recv()")
  131. r = self.expect('C> ')
  132. if not r.endswith('C> '):
  133. log.error('Error waiting for esp "%s"' % r)
  134. return
  135. log.debug('sending destination filename "%s"', destination)
  136. self.write(destination + '\x00', True)
  137. if not self.got_ack():
  138. log.error('did not ack destination filename')
  139. return
  140. f = open( path, 'rt' ); content = f.read(); f.close()
  141. log.debug('sending %d bytes in %s' % (len(content), filename))
  142. pos = 0
  143. chunk_size = 128
  144. error = False
  145. while pos < len(content):
  146. rest = len(content) - pos
  147. if rest > chunk_size:
  148. rest = chunk_size
  149. data = content[pos:pos+rest]
  150. if not self.write_chunk(data):
  151. d = self.expect()
  152. log.error('Bad chunk response "%s" %s' % (d, ':'.join(x.encode('hex') for x in d)))
  153. return
  154. pos += chunk_size
  155. log.debug('sending zero block')
  156. #zero size block
  157. self.write_chunk('')
  158. if verify:
  159. log.info('Verifying...')
  160. data = self.download_file(destination)
  161. if content != data:
  162. log.error('Verification failed.')
  163. def exec_file(self, path):
  164. filename = os.path.basename(path)
  165. log.info('Execute %s' %(filename,))
  166. f = open( path, 'rt' );
  167. res = '> '
  168. for line in f:
  169. line = line.rstrip('\r\n')
  170. retlines = (res + self.exchange(line)).splitlines()
  171. # Log all but the last line
  172. res = retlines.pop()
  173. for l in retlines:
  174. log.info(l)
  175. # last line
  176. log.info(res)
  177. f.close()
  178. def got_ack(self):
  179. log.debug('waiting for ack')
  180. r = self._port.read(1)
  181. log.debug('ack read %s', r.encode('hex'))
  182. return r == '\x06' #ACK
  183. def write_lines(self, data):
  184. lines = data.replace('\r', '').split('\n')
  185. for line in lines:
  186. self.exchange(line)
  187. return
  188. def write_chunk(self, chunk):
  189. log.debug('writing %d bytes chunk' % len(chunk))
  190. data = '\x01' + chr(len(chunk)) + chunk
  191. if len(chunk) < 128:
  192. padding = 128 - len(chunk)
  193. log.debug('pad with %d characters' % padding)
  194. data = data + (' ' * padding)
  195. log.debug("packet size %d" % len(data))
  196. self.write(data)
  197. return self.got_ack()
  198. def file_list(self):
  199. log.info('Listing files')
  200. r = self.exchange('for key,value in pairs(file.list()) do print(key,value) end')
  201. log.info(r)
  202. return r
  203. def file_do(self, f):
  204. log.info('Executing '+f)
  205. r = self.exchange('dofile("'+f+'")')
  206. log.info(r)
  207. return r
  208. def file_format(self):
  209. log.info('Formating...')
  210. r = self.exchange('file.format()')
  211. if 'format done' not in r:
  212. log.error(r)
  213. else:
  214. log.info(r)
  215. return r
  216. def node_heap(self):
  217. log.info('Heap')
  218. r = self.exchange('print(node.heap())')
  219. log.info(r)
  220. return r
  221. def node_restart(self):
  222. log.info('Restart')
  223. r = self.exchange('node.restart()')
  224. log.info(r)
  225. return r
  226. def file_compile(self, path):
  227. log.info('Compile '+path)
  228. cmd = 'node.compile("%s")' % path
  229. r = self.exchange(cmd)
  230. log.info(r)
  231. return r
  232. def file_remove(self, path):
  233. log.info('Remove '+path)
  234. cmd = 'file.remove("%s")' % path
  235. r = self.exchange(cmd)
  236. log.info(r)
  237. return r
  238. def arg_auto_int(x):
  239. return int(x, 0)
  240. if __name__ == '__main__':
  241. parser = argparse.ArgumentParser(description = 'NodeMCU Lua file uploader', prog = 'nodemcu-uploader')
  242. parser.add_argument(
  243. '--verbose',
  244. help = 'verbose output',
  245. action = 'store_true',
  246. default = False)
  247. parser.add_argument(
  248. '--port', '-p',
  249. help = 'Serial port device',
  250. default = Uploader.PORT)
  251. parser.add_argument(
  252. '--baud', '-b',
  253. help = 'Serial port baudrate',
  254. type = arg_auto_int,
  255. default = Uploader.BAUD)
  256. subparsers = parser.add_subparsers(
  257. dest='operation',
  258. help = 'Run nodemcu-uploader {command} -h for additional help')
  259. upload_parser = subparsers.add_parser(
  260. 'upload',
  261. help = 'Path to one or more files to be uploaded. Destination name will be the same as the file name.')
  262. # upload_parser.add_argument(
  263. # '--filename', '-f',
  264. # help = 'File to upload. You can specify this option multiple times.',
  265. # action='append')
  266. # upload_parser.add_argument(
  267. # '--destination', '-d',
  268. # help = 'Name to be used when saving in NodeMCU. You should specify one per file.',
  269. # action='append')
  270. upload_parser.add_argument('filename', nargs='+', help = 'Lua file to upload. Use colon to give alternate destination.')
  271. upload_parser.add_argument(
  272. '--compile', '-c',
  273. help = 'If file should be uploaded as compiled',
  274. action='store_true',
  275. default=False
  276. )
  277. upload_parser.add_argument(
  278. '--verify', '-v',
  279. help = 'To verify the uploaded data.',
  280. action='store_true',
  281. default=False
  282. )
  283. upload_parser.add_argument(
  284. '--restart', '-r',
  285. help = 'If esp should be restarted',
  286. action='store_true',
  287. default=False
  288. )
  289. exec_parser = subparsers.add_parser(
  290. 'exec',
  291. help = 'Path to one or more files to be executed line by line.')
  292. exec_parser.add_argument('filename', nargs='+', help = 'Lua file to execute.')
  293. download_parser = subparsers.add_parser(
  294. 'download',
  295. help = 'Path to one or more files to be downloaded. Destination name will be the same as the file name.')
  296. # download_parser.add_argument(
  297. # '--filename', '-f',
  298. # help = 'File to download. You can specify this option multiple times.',
  299. # action='append')
  300. # download_parser.add_argument(
  301. # '--destination', '-d',
  302. # help = 'Name to be used when saving in NodeMCU. You should specify one per file.',
  303. # action='append')
  304. download_parser.add_argument('filename', nargs='+', help = 'Lua file to download. Use colon to give alternate destination.')
  305. file_parser = subparsers.add_parser(
  306. 'file',
  307. help = 'File functions')
  308. file_parser.add_argument('cmd', choices=('list', 'do', 'format'))
  309. file_parser.add_argument('filename', nargs='*', help = 'Lua file to run.')
  310. node_parse = subparsers.add_parser(
  311. 'node',
  312. help = 'Node functions')
  313. node_parse.add_argument('ncmd', choices=('heap', 'restart'))
  314. args = parser.parse_args()
  315. formatter = logging.Formatter('%(message)s')
  316. logging.basicConfig(level=logging.INFO, format='%(message)s')
  317. if args.verbose:
  318. log.setLevel(logging.DEBUG)
  319. uploader = Uploader(args.port, args.baud)
  320. if args.operation == 'upload' or args.operation == 'download':
  321. sources = args.filename
  322. destinations = []
  323. for i in range(0, len(sources)):
  324. sd = sources[i].split(':')
  325. if len(sd) == 2:
  326. destinations.append(sd[1])
  327. sources[i]=sd[0]
  328. else:
  329. destinations.append(sd[0])
  330. if args.operation == 'upload':
  331. if len(destinations) == len(sources):
  332. uploader.prepare()
  333. for f, d in zip(sources, destinations):
  334. uploader.write_file(f, d, args.verify)
  335. if args.compile:
  336. uploader.file_compile(d)
  337. uploader.file_remove(d)
  338. else:
  339. raise Exception('You must specify a destination filename for each file you want to upload.')
  340. if args.restart:
  341. uploader.node_restart()
  342. log.info('All done!')
  343. if args.operation == 'download':
  344. if len(destinations) == len(sources):
  345. for f, d in zip(sources, destinations):
  346. uploader.read_file(f, d)
  347. else:
  348. raise Exception('You must specify a destination filename for each file you want to download.')
  349. log.info('All done!')
  350. elif args.operation == 'exec':
  351. sources = args.filename
  352. for f in sources:
  353. uploader.exec_file(f)
  354. elif args.operation == 'file':
  355. if args.cmd == 'list':
  356. uploader.file_list()
  357. if args.cmd == 'do':
  358. for f in args.filename:
  359. uploader.file_do(f)
  360. elif args.cmd == 'format':
  361. uploader.file_format()
  362. elif args.operation == 'node':
  363. if args.ncmd == 'heap':
  364. uploader.node_heap()
  365. elif args.ncmd == 'restart':
  366. uploader.node_restart()
  367. uploader.close()