espota.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. #!/usr/bin/env python
  2. #
  3. # Original espota.py by Ivan Grokhotkov:
  4. # https://gist.github.com/igrr/d35ab8446922179dc58c
  5. #
  6. # Modified since 2015-09-18 from Pascal Gollor (https://github.com/pgollor)
  7. # Modified since 2015-11-09 from Hristo Gochkov (https://github.com/me-no-dev)
  8. # Modified since 2016-01-03 from Matthew O'Gorman (https://githumb.com/mogorman)
  9. #
  10. # This script will push an OTA update to the ESP
  11. # use it like: python espota.py -i <ESP_IP_address> -I <Host_IP_address> -p <ESP_port> -P <Host_port> [-a password] -f <sketch.bin>
  12. # Or to upload SPIFFS image:
  13. # python espota.py -i <ESP_IP_address> -I <Host_IP_address> -p <ESP_port> -P <HOST_port> [-a password] -s -f <spiffs.bin>
  14. #
  15. # Changes
  16. # 2015-09-18:
  17. # - Add option parser.
  18. # - Add logging.
  19. # - Send command to controller to differ between flashing and transmitting SPIFFS image.
  20. #
  21. # Changes
  22. # 2015-11-09:
  23. # - Added digest authentication
  24. # - Enhanced error tracking and reporting
  25. #
  26. # Changes
  27. # 2016-01-03:
  28. # - Added more options to parser.
  29. #
  30. from __future__ import print_function
  31. import socket
  32. import sys
  33. import os
  34. import optparse
  35. import logging
  36. import hashlib
  37. import random
  38. # Commands
  39. FLASH = 0
  40. SPIFFS = 100
  41. AUTH = 200
  42. PROGRESS = False
  43. # update_progress() : Displays or updates a console progress bar
  44. ## Accepts a float between 0 and 1. Any int will be converted to a float.
  45. ## A value under 0 represents a 'halt'.
  46. ## A value at 1 or bigger represents 100%
  47. def update_progress(progress):
  48. if (PROGRESS):
  49. barLength = 60 # Modify this to change the length of the progress bar
  50. status = ""
  51. if isinstance(progress, int):
  52. progress = float(progress)
  53. if not isinstance(progress, float):
  54. progress = 0
  55. status = "error: progress var must be float\r\n"
  56. if progress < 0:
  57. progress = 0
  58. status = "Halt...\r\n"
  59. if progress >= 1:
  60. progress = 1
  61. status = "Done...\r\n"
  62. block = int(round(barLength*progress))
  63. text = "\rUploading: [{0}] {1}% {2}".format( "="*block + " "*(barLength-block), int(progress*100), status)
  64. sys.stderr.write(text)
  65. sys.stderr.flush()
  66. else:
  67. sys.stderr.write('.')
  68. sys.stderr.flush()
  69. def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, command = FLASH):
  70. # Create a TCP/IP socket
  71. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  72. server_address = (localAddr, localPort)
  73. logging.info('Starting on %s:%s', str(server_address[0]), str(server_address[1]))
  74. try:
  75. sock.bind(server_address)
  76. sock.listen(1)
  77. except:
  78. logging.error("Listen Failed")
  79. return 1
  80. content_size = os.path.getsize(filename)
  81. f = open(filename,'rb')
  82. file_md5 = hashlib.md5(f.read()).hexdigest()
  83. f.close()
  84. logging.info('Upload size: %d', content_size)
  85. message = '%d %d %d %s\n' % (command, localPort, content_size, file_md5)
  86. # Wait for a connection
  87. inv_trys = 0
  88. data = ''
  89. msg = 'Sending invitation to %s ' % (remoteAddr)
  90. sys.stderr.write(msg)
  91. sys.stderr.flush()
  92. while (inv_trys < 10):
  93. inv_trys += 1
  94. sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  95. remote_address = (remoteAddr, int(remotePort))
  96. try:
  97. sent = sock2.sendto(message.encode(), remote_address)
  98. except:
  99. sys.stderr.write('failed\n')
  100. sys.stderr.flush()
  101. sock2.close()
  102. logging.error('Host %s Not Found', remoteAddr)
  103. return 1
  104. sock2.settimeout(TIMEOUT)
  105. try:
  106. data = sock2.recv(37).decode()
  107. break;
  108. except:
  109. sys.stderr.write('.')
  110. sys.stderr.flush()
  111. sock2.close()
  112. sys.stderr.write('\n')
  113. sys.stderr.flush()
  114. if (inv_trys == 10):
  115. logging.error('No response from the ESP')
  116. return 1
  117. if (data != "OK"):
  118. if(data.startswith('AUTH')):
  119. nonce = data.split()[1]
  120. cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remoteAddr)
  121. cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
  122. passmd5 = hashlib.md5(password.encode()).hexdigest()
  123. result_text = '%s:%s:%s' % (passmd5 ,nonce, cnonce)
  124. result = hashlib.md5(result_text.encode()).hexdigest()
  125. sys.stderr.write('Authenticating...')
  126. sys.stderr.flush()
  127. message = '%d %s %s\n' % (AUTH, cnonce, result)
  128. sock2.sendto(message.encode(), remote_address)
  129. sock2.settimeout(10)
  130. try:
  131. data = sock2.recv(32).decode()
  132. except:
  133. sys.stderr.write('FAIL\n')
  134. logging.error('No Answer to our Authentication')
  135. sock2.close()
  136. return 1
  137. if (data != "OK"):
  138. sys.stderr.write('FAIL\n')
  139. logging.error('%s', data)
  140. sock2.close()
  141. sys.exit(1);
  142. return 1
  143. sys.stderr.write('OK\n')
  144. else:
  145. logging.error('Bad Answer: %s', data)
  146. sock2.close()
  147. return 1
  148. sock2.close()
  149. logging.info('Waiting for device...')
  150. try:
  151. sock.settimeout(10)
  152. connection, client_address = sock.accept()
  153. sock.settimeout(None)
  154. connection.settimeout(None)
  155. except:
  156. logging.error('No response from device')
  157. sock.close()
  158. return 1
  159. try:
  160. f = open(filename, "rb")
  161. if (PROGRESS):
  162. update_progress(0)
  163. else:
  164. sys.stderr.write('Uploading')
  165. sys.stderr.flush()
  166. offset = 0
  167. while True:
  168. chunk = f.read(1024)
  169. if not chunk: break
  170. offset += len(chunk)
  171. update_progress(offset/float(content_size))
  172. connection.settimeout(10)
  173. try:
  174. connection.sendall(chunk)
  175. res = connection.recv(10)
  176. lastResponseContainedOK = 'OK' in res.decode()
  177. except:
  178. sys.stderr.write('\n')
  179. logging.error('Error Uploading')
  180. connection.close()
  181. f.close()
  182. sock.close()
  183. return 1
  184. if lastResponseContainedOK:
  185. logging.info('Success')
  186. connection.close()
  187. f.close()
  188. sock.close()
  189. return 0
  190. sys.stderr.write('\n')
  191. logging.info('Waiting for result...')
  192. try:
  193. count = 0
  194. while True:
  195. count=count+1
  196. connection.settimeout(60)
  197. data = connection.recv(32).decode()
  198. logging.info('Result: %s' ,data)
  199. if "OK" in data:
  200. logging.info('Success')
  201. connection.close()
  202. f.close()
  203. sock.close()
  204. return 0;
  205. if count == 5:
  206. logging.error('Error response from device')
  207. connection.close()
  208. f.close()
  209. sock.close()
  210. return 1
  211. except e:
  212. logging.error('No Result!')
  213. connection.close()
  214. f.close()
  215. sock.close()
  216. return 1
  217. finally:
  218. connection.close()
  219. f.close()
  220. sock.close()
  221. return 1
  222. # end serve
  223. def parser(unparsed_args):
  224. parser = optparse.OptionParser(
  225. usage = "%prog [options]",
  226. description = "Transmit image over the air to the esp32 module with OTA support."
  227. )
  228. # destination ip and port
  229. group = optparse.OptionGroup(parser, "Destination")
  230. group.add_option("-i", "--ip",
  231. dest = "esp_ip",
  232. action = "store",
  233. help = "ESP32 IP Address.",
  234. default = False
  235. )
  236. group.add_option("-I", "--host_ip",
  237. dest = "host_ip",
  238. action = "store",
  239. help = "Host IP Address.",
  240. default = "0.0.0.0"
  241. )
  242. group.add_option("-p", "--port",
  243. dest = "esp_port",
  244. type = "int",
  245. help = "ESP32 ota Port. Default 3232",
  246. default = 3232
  247. )
  248. group.add_option("-P", "--host_port",
  249. dest = "host_port",
  250. type = "int",
  251. help = "Host server ota Port. Default random 10000-60000",
  252. default = random.randint(10000,60000)
  253. )
  254. parser.add_option_group(group)
  255. # auth
  256. group = optparse.OptionGroup(parser, "Authentication")
  257. group.add_option("-a", "--auth",
  258. dest = "auth",
  259. help = "Set authentication password.",
  260. action = "store",
  261. default = ""
  262. )
  263. parser.add_option_group(group)
  264. # image
  265. group = optparse.OptionGroup(parser, "Image")
  266. group.add_option("-f", "--file",
  267. dest = "image",
  268. help = "Image file.",
  269. metavar="FILE",
  270. default = None
  271. )
  272. group.add_option("-s", "--spiffs",
  273. dest = "spiffs",
  274. action = "store_true",
  275. help = "Use this option to transmit a SPIFFS image and do not flash the module.",
  276. default = False
  277. )
  278. parser.add_option_group(group)
  279. # output group
  280. group = optparse.OptionGroup(parser, "Output")
  281. group.add_option("-d", "--debug",
  282. dest = "debug",
  283. help = "Show debug output. And override loglevel with debug.",
  284. action = "store_true",
  285. default = False
  286. )
  287. group.add_option("-r", "--progress",
  288. dest = "progress",
  289. help = "Show progress output. Does not work for ArduinoIDE",
  290. action = "store_true",
  291. default = False
  292. )
  293. group.add_option("-t", "--timeout",
  294. dest = "timeout",
  295. type = "int",
  296. help = "Timeout to wait for the ESP32 to accept invitation",
  297. default = 10
  298. )
  299. parser.add_option_group(group)
  300. (options, args) = parser.parse_args(unparsed_args)
  301. return options
  302. # end parser
  303. def main(args):
  304. options = parser(args)
  305. loglevel = logging.WARNING
  306. if (options.debug):
  307. loglevel = logging.DEBUG
  308. logging.basicConfig(level = loglevel, format = '%(asctime)-8s [%(levelname)s]: %(message)s', datefmt = '%H:%M:%S')
  309. logging.debug("Options: %s", str(options))
  310. # check options
  311. global PROGRESS
  312. PROGRESS = options.progress
  313. global TIMEOUT
  314. TIMEOUT = options.timeout
  315. if (not options.esp_ip or not options.image):
  316. logging.critical("Not enough arguments.")
  317. return 1
  318. command = FLASH
  319. if (options.spiffs):
  320. command = SPIFFS
  321. return serve(options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command)
  322. # end main
  323. if __name__ == '__main__':
  324. sys.exit(main(sys.argv))