uploader.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright (C) 2015-2016 Peter Magnusson <peter@birchroad.net>
  4. import time
  5. import logging
  6. import hashlib
  7. import os
  8. from platform import system
  9. import serial
  10. from .luacode import SAVE_LUA
  11. log = logging.getLogger(__name__)
  12. __all__ = ['Uploader', 'default_port']
  13. CHUNK_END = '\v'
  14. CHUNK_REPLY = '\v'
  15. def default_port():
  16. return {
  17. 'Windows': 'COM1',
  18. 'Darwin': '/dev/tty.SLAB_USBtoUART'
  19. }.get(system(), '/dev/ttyUSB0')
  20. class Uploader(object):
  21. BAUD = 9600
  22. TIMEOUT = 5
  23. PORT = default_port()
  24. def __init__(self, port=PORT, baud=BAUD):
  25. self._port = serial.Serial(port, Uploader.BAUD, timeout=Uploader.TIMEOUT)
  26. # Keeps things working, if following conections are made:
  27. ## RTS = CH_PD (i.e reset)
  28. ## DTR = GPIO0
  29. self._port.setRTS(False)
  30. self._port.setDTR(False)
  31. # Get in sync with LUA (this assumes that NodeMCU gets reset by the previous two lines)
  32. self.exchange(';') # Get a defined state
  33. self.writeln('print("%sync%");')
  34. self.expect('%sync%\r\n> ')
  35. if baud != Uploader.BAUD:
  36. log.info('Changing communication to %s baud', baud)
  37. self.writeln('uart.setup(0,%s,8,0,1,1)' % baud)
  38. # Wait for the string to be sent before switching baud
  39. time.sleep(0.1)
  40. self._port.setBaudrate(baud)
  41. # Get in sync again
  42. self.exchange('')
  43. self.exchange('')
  44. self.line_number = 0
  45. def expect(self, exp='> ', timeout=TIMEOUT):
  46. timer = self._port.timeout
  47. # Checking for new data every 100us is fast enough
  48. lt = 0.0001
  49. if self._port.timeout != lt:
  50. self._port.timeout = lt
  51. end = time.time() + timeout
  52. # Finish as soon as either exp matches or we run out of time (work like dump, but faster on success)
  53. data = ''
  54. while not data.endswith(exp) and time.time() <= end:
  55. data += self._port.read()
  56. self._port.timeout = timer
  57. log.debug('expect return: %s', data)
  58. return data
  59. def write(self, output, binary=False):
  60. if not binary:
  61. log.debug('write: %s', output)
  62. else:
  63. log.debug('write binary: %s', ':'.join(x.encode('hex') for x in output))
  64. self._port.write(output)
  65. self._port.flush()
  66. def writeln(self, output):
  67. self.write(output + '\n')
  68. def exchange(self, output):
  69. self.writeln(output)
  70. return self.expect()
  71. def close(self):
  72. self.writeln('uart.setup(0,%s,8,0,1,1)' % Uploader.BAUD)
  73. self._port.close()
  74. def prepare(self):
  75. log.info('Preparing esp for transfer.')
  76. data = SAVE_LUA.replace('9600', '%d' % self._port.baudrate)
  77. lines = data.replace('\r', '').split('\n')
  78. for line in lines:
  79. line = line.strip().replace(', ', ',').replace(' = ', '=')
  80. if len(line) == 0:
  81. continue
  82. d = self.exchange(line)
  83. #do some basic test of the result
  84. if 'unexpected' in d or len(d) > len(SAVE_LUA)+10:
  85. log.error('error in save_lua "%s"', d)
  86. return
  87. def download_file(self, filename):
  88. chunk_size = 256
  89. bytes_read = 0
  90. data = ""
  91. while True:
  92. d = self.exchange("file.open('" + filename + r"') print(file.seek('end', 0)) file.seek('set', %d) uart.write(0, file.read(%d))file.close()" % (bytes_read, chunk_size))
  93. cmd, size, tmp_data = d.split('\n', 2)
  94. data = data + tmp_data[0:chunk_size]
  95. bytes_read = bytes_read + chunk_size
  96. if bytes_read > int(size):
  97. break
  98. data = data[0:int(size)]
  99. return data
  100. def read_file(self, filename, destination=''):
  101. if not destination:
  102. destination = filename
  103. log.info('Transfering %s to %s', filename, destination)
  104. data = self.download_file(filename)
  105. with open(destination, 'w') as f:
  106. f.write(data)
  107. def write_file(self, path, destination='', verify='none'):
  108. filename = os.path.basename(path)
  109. if not destination:
  110. destination = filename
  111. log.info('Transfering %s as %s', path, destination)
  112. self.writeln("recv()")
  113. res = self.expect('C> ')
  114. if not res.endswith('C> '):
  115. log.error('Error waiting for esp "%s"', res)
  116. return
  117. log.debug('sending destination filename "%s"', destination)
  118. self.write(destination + '\x00', True)
  119. if not self.got_ack():
  120. log.error('did not ack destination filename')
  121. return
  122. f = open(path, 'rb')
  123. content = f.read()
  124. f.close()
  125. log.debug('sending %d bytes in %s', len(content), filename)
  126. pos = 0
  127. chunk_size = 128
  128. while pos < len(content):
  129. rest = len(content) - pos
  130. if rest > chunk_size:
  131. rest = chunk_size
  132. data = content[pos:pos+rest]
  133. if not self.write_chunk(data):
  134. d = self.expect()
  135. log.error('Bad chunk response "%s" %s', d, ':'.join(x.encode('hex') for x in d))
  136. return
  137. pos += chunk_size
  138. log.debug('sending zero block')
  139. #zero size block
  140. self.write_chunk('')
  141. if verify == 'standard':
  142. log.info('Verifying...')
  143. data = self.download_file(destination)
  144. if content != data:
  145. log.error('Verification failed.')
  146. elif verify == 'sha1':
  147. #Calculate SHA1 on remote file. Extract just hash from result
  148. data = self.exchange('shafile("'+destination+'")').splitlines()[1]
  149. log.info('Remote SHA1: %s', data)
  150. #Calculate hash of local data
  151. filehashhex = hashlib.sha1(content.encode()).hexdigest()
  152. log.info('Local SHA1: %s', filehashhex)
  153. if data != filehashhex:
  154. log.error('Verification failed.')
  155. def exec_file(self, path):
  156. filename = os.path.basename(path)
  157. log.info('Execute %s', filename)
  158. f = open(path, 'rt')
  159. res = '> '
  160. for line in f:
  161. line = line.rstrip('\r\n')
  162. retlines = (res + self.exchange(line)).splitlines()
  163. # Log all but the last line
  164. res = retlines.pop()
  165. for lin in retlines:
  166. log.info(lin)
  167. # last line
  168. log.info(res)
  169. f.close()
  170. def got_ack(self):
  171. log.debug('waiting for ack')
  172. res = self._port.read(1)
  173. log.debug('ack read %s', res.encode('hex'))
  174. return res == '\x06' #ACK
  175. def write_lines(self, data):
  176. lines = data.replace('\r', '').split('\n')
  177. for line in lines:
  178. self.exchange(line)
  179. return
  180. def write_chunk(self, chunk):
  181. log.debug('writing %d bytes chunk', len(chunk))
  182. data = '\x01' + chr(len(chunk)) + chunk
  183. if len(chunk) < 128:
  184. padding = 128 - len(chunk)
  185. log.debug('pad with %d characters', padding)
  186. data = data + (' ' * padding)
  187. log.debug("packet size %d", len(data))
  188. self.write(data)
  189. return self.got_ack()
  190. def file_list(self):
  191. log.info('Listing files')
  192. res = self.exchange('for key,value in pairs(file.list()) do print(key,value) end')
  193. log.info(res)
  194. return res
  195. def file_do(self, f):
  196. log.info('Executing '+f)
  197. res = self.exchange('dofile("'+f+'")')
  198. log.info(res)
  199. return res
  200. def file_format(self):
  201. log.info('Formating...')
  202. res = self.exchange('file.format()')
  203. if 'format done' not in res:
  204. log.error(res)
  205. else:
  206. log.info(res)
  207. return res
  208. def node_heap(self):
  209. log.info('Heap')
  210. res = self.exchange('print(node.heap())')
  211. log.info(res)
  212. return res
  213. def node_restart(self):
  214. log.info('Restart')
  215. res = self.exchange('node.restart()')
  216. log.info(res)
  217. return res
  218. def file_compile(self, path):
  219. log.info('Compile '+path)
  220. cmd = 'node.compile("%s")' % path
  221. res = self.exchange(cmd)
  222. log.info(res)
  223. return res
  224. def file_remove(self, path):
  225. log.info('Remove '+path)
  226. cmd = 'file.remove("%s")' % path
  227. res = self.exchange(cmd)
  228. log.info(res)
  229. return res