nodemcu-uploader.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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 = 1
  54. def __init__(self, port = 0, baud = BAUD):
  55. self._port = serial.Serial(port, Uploader.BAUD, timeout=Uploader.TIMEOUT)
  56. # Keeps things working, if following conections are made:
  57. ## RTS = CH_PD (i.e reset)
  58. ## DTR = GPIO0
  59. self._port.setRTS(False)
  60. self._port.setDTR(False)
  61. time.sleep(0.5)
  62. self.dump()
  63. if baud != Uploader.BAUD:
  64. log.info('Changing communication to %s baud', baud)
  65. self._port.write('uart.setup(0,%s,8,0,1,1)\r\n' % baud)
  66. log.info(self.dump())
  67. self._port.close()
  68. self._port = serial.Serial(port, baud, timeout=Uploader.TIMEOUT)
  69. self.line_number = 0
  70. def close(self):
  71. self._port.write('uart.setup(0,%s,8,0,1,1)\r\n' % Uploader.BAUD)
  72. self._port.close()
  73. def dump(self, timeout=TIMEOUT):
  74. t = self._port.timeout
  75. if self._port.timeout != timeout:
  76. self._port.timeout = timeout
  77. n = self._port.read()
  78. data = ''
  79. while n != '':
  80. data += n
  81. n = self._port.read()
  82. self._port.timeout = t
  83. return data
  84. def prepare(self):
  85. log.info('Preparing esp for transfer.')
  86. self.write_lines(save_lua.replace('9600', '%d' % self._port.baudrate))
  87. self._port.write('\r\n')
  88. d = self.dump(0.1)
  89. if 'unexpected' in d or len(d) > len(save_lua)+10:
  90. log.error('error in save_lua "%s"' % d)
  91. return
  92. def download_file(self, filename):
  93. self.dump()
  94. self._port.write(r"file.open('" + filename + r"') print(file.seek('end', 0)) file.seek('set', 0) uart.write(0, file.read()) file.close()" + '\n')
  95. cmd, size, data = self.dump().split('\n', 2)
  96. data = data[0:int(size)]
  97. return data
  98. def read_file(self, filename, destination = ''):
  99. if not destination:
  100. destination = filename
  101. log.info('Transfering %s to %s' %(filename, destination))
  102. data = self.download_file(filename)
  103. with open(destination, 'w') as f:
  104. f.write(data)
  105. def write_file(self, path, destination = '', verify = False):
  106. filename = os.path.basename(path)
  107. if not destination:
  108. destination = filename
  109. log.info('Transfering %s as %s' %(filename, destination))
  110. self.dump()
  111. self._port.write(r"recv()" + '\n')
  112. count = 0
  113. while not 'C' in self.dump(0.2):
  114. time.sleep(1)
  115. count += 1
  116. if count > 5:
  117. log.error('Error waiting for esp "%s"' % self.dump())
  118. return
  119. self.dump(0.5)
  120. log.debug('sending destination filename "%s"', destination)
  121. self._port.write(destination + '\x00')
  122. if not self.got_ack():
  123. log.error('did not ack destination filename: "%s"' % self.dump())
  124. return
  125. f = open( path, 'rt' ); content = f.read(); f.close()
  126. log.debug('sending %d bytes in %s' % (len(content), filename))
  127. pos = 0
  128. chunk_size = 128
  129. error = False
  130. while pos < len(content):
  131. rest = len(content) - pos
  132. if rest > chunk_size:
  133. rest = chunk_size
  134. data = content[pos:pos+rest]
  135. if not self.write_chunk(data):
  136. error = True
  137. d = self.dump()
  138. log.error('Bad chunk response "%s" %s' % (d, ':'.join(x.encode('hex') for x in d)))
  139. break
  140. pos += chunk_size
  141. log.debug('sending zero block')
  142. if not error:
  143. #zero size block
  144. self.write_chunk('')
  145. if verify:
  146. log.info('Verifying...')
  147. data = self.download_file(destination)
  148. if content != data:
  149. log.error('Verification failed.')
  150. def got_ack(self):
  151. log.debug('waiting for ack')
  152. r = self._port.read(1)
  153. return r == '\x06' #ACK
  154. def write_lines(self, data):
  155. lines = data.replace('\r', '').split('\n')
  156. for line in lines:
  157. self._port.write(line + '\r\n')
  158. d = self.dump(0.1)
  159. log.debug(d)
  160. return
  161. def write_chunk(self, chunk):
  162. log.debug('writing %d bytes chunk' % len(chunk))
  163. data = '\x01' + chr(len(chunk)) + chunk
  164. if len(chunk) < 128:
  165. padding = 128 - len(chunk)
  166. log.debug('pad with %d characters' % padding)
  167. data = data + (' ' * padding)
  168. log.debug("packet size %d" % len(data))
  169. self._port.write(data)
  170. return self.got_ack()
  171. def file_list(self):
  172. log.info('Listing files')
  173. self._port.write('for key,value in pairs(file.list()) do print(key,value) end' + '\r\n')
  174. r = self.dump()
  175. log.info(r)
  176. return r
  177. def file_format(self):
  178. log.info('Format')
  179. self._port.write('file.format()' + '\r\n')
  180. r = self.dump()
  181. log.info(r)
  182. return r
  183. def node_heap(self):
  184. log.info('Heap')
  185. self._port.write('print(node.heap())\r\n')
  186. r = self.dump()
  187. log.info(r)
  188. return r
  189. def node_restart(self):
  190. log.info('Restart')
  191. self._port.write('node.restart()' +'\r\n')
  192. r = self.dump()
  193. log.info(r)
  194. return r
  195. def file_compile(self, path):
  196. log.info('Compile '+path)
  197. cmd = 'node.compile("%s")' % path
  198. self._port.write(cmd + '\r\n')
  199. r = self.dump()
  200. log.info(r)
  201. return r
  202. def file_remove(self, path):
  203. log.info('Remove '+path)
  204. cmd = 'file.remove("%s")' % path
  205. self._port.write(cmd + '\r\n')
  206. r = self.dump()
  207. log.info(r)
  208. return r
  209. def arg_auto_int(x):
  210. return int(x, 0)
  211. if __name__ == '__main__':
  212. parser = argparse.ArgumentParser(description = 'NodeMCU Lua file uploader', prog = 'nodemcu-uploader')
  213. parser.add_argument(
  214. '--verbose', '-v',
  215. help = 'verbose output',
  216. action = 'store_true',
  217. default = False)
  218. parser.add_argument(
  219. '--port', '-p',
  220. help = 'Serial port device',
  221. default = Uploader.PORT)
  222. parser.add_argument(
  223. '--baud', '-b',
  224. help = 'Serial port baudrate',
  225. type = arg_auto_int,
  226. default = Uploader.BAUD)
  227. subparsers = parser.add_subparsers(
  228. dest='operation',
  229. help = 'Run nodemcu-uploader {command} -h for additional help')
  230. upload_parser = subparsers.add_parser(
  231. 'upload',
  232. help = 'Path to one or more files to be uploaded. Destination name will be the same as the file name.')
  233. upload_parser.add_argument(
  234. '--filename', '-f',
  235. help = 'File to upload. You can specify this option multiple times.',
  236. action='append')
  237. upload_parser.add_argument(
  238. '--destination', '-d',
  239. help = 'Name to be used when saving in NodeMCU. You should specify one per file.',
  240. action='append')
  241. upload_parser.add_argument(
  242. '--compile', '-c',
  243. help = 'If file should be uploaded as compiled',
  244. action='store_true',
  245. default=False
  246. )
  247. upload_parser.add_argument(
  248. '--verify', '-v',
  249. help = 'To verify the uploaded data.',
  250. action='store_true',
  251. default=False
  252. )
  253. upload_parser.add_argument(
  254. '--restart', '-r',
  255. help = 'If esp should be restarted',
  256. action='store_true',
  257. default=False
  258. )
  259. download_parser = subparsers.add_parser(
  260. 'download',
  261. help = 'Path to one or more files to be downloaded. Destination name will be the same as the file name.')
  262. download_parser.add_argument(
  263. '--filename', '-f',
  264. help = 'File to download. You can specify this option multiple times.',
  265. action='append')
  266. download_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. file_parser = subparsers.add_parser(
  271. 'file',
  272. help = 'File functions')
  273. file_parser.add_argument('cmd', choices=('list', 'format'))
  274. args = parser.parse_args()
  275. formatter = logging.Formatter('%(message)s')
  276. logging.basicConfig(level=logging.INFO, format='%(message)s')
  277. uploader = Uploader(args.port, args.baud)
  278. if args.verbose:
  279. log.setLevel(logging.DEBUG)
  280. if args.operation == 'upload':
  281. if not args.destination:
  282. uploader.prepare()
  283. for f in args.filename:
  284. uploader.write_file(f, '', args.verify)
  285. elif len(args.destination) == len(args.filename):
  286. uploader.prepare()
  287. for f, d in zip(args.filename, args.destination):
  288. uploader.write_file(f, d, args.verify)
  289. if args.compile:
  290. uploader.file_compile(d)
  291. uploader.file_remove(d)
  292. if args.restart:
  293. uploader.node_restart()
  294. else:
  295. raise Exception('You must specify a destination filename for each file you want to upload.')
  296. print 'All done!'
  297. if args.operation == 'download':
  298. if not args.destination:
  299. for f in args.filename:
  300. uploader.read_file(f)
  301. elif len(args.destination) == len(args.filename):
  302. for f, d in zip(args.filename, args.destination):
  303. uploader.read_file(f, d)
  304. else:
  305. raise Exception('You must specify a destination filename for each file you want to download.')
  306. print 'All done!'
  307. elif args.operation == 'file':
  308. if args.cmd == 'list':
  309. uploader.file_list()
  310. elif args.cmd == 'format':
  311. uploader.file_format()
  312. uploader.close()