upload.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import argparse, sys, os, time, random, serial
  2. from SCons.Script import DefaultEnvironment
  3. env = DefaultEnvironment()
  4. import MarlinBinaryProtocol
  5. #-----------------#
  6. # Upload Callback #
  7. #-----------------#
  8. def Upload(source, target, env):
  9. #-------#
  10. # Debug #
  11. #-------#
  12. Debug = False # Set to True to enable script debug
  13. def debugPrint(data):
  14. if Debug: print(f"[Debug]: {data}")
  15. #------------------#
  16. # Marlin functions #
  17. #------------------#
  18. def _GetMarlinEnv(marlinEnv, feature):
  19. if not marlinEnv: return None
  20. return marlinEnv[feature] if feature in marlinEnv else None
  21. #----------------#
  22. # Port functions #
  23. #----------------#
  24. def _GetUploadPort(env):
  25. debugPrint('Autodetecting upload port...')
  26. env.AutodetectUploadPort(env)
  27. portName = env.subst('$UPLOAD_PORT')
  28. if not portName:
  29. raise Exception('Error detecting the upload port.')
  30. debugPrint('OK')
  31. return portName
  32. #-------------------------#
  33. # Simple serial functions #
  34. #-------------------------#
  35. def _OpenPort():
  36. # Open serial port
  37. if port.is_open: return
  38. debugPrint('Opening upload port...')
  39. port.open()
  40. port.reset_input_buffer()
  41. debugPrint('OK')
  42. def _ClosePort():
  43. # Open serial port
  44. if port is None: return
  45. if not port.is_open: return
  46. debugPrint('Closing upload port...')
  47. port.close()
  48. debugPrint('OK')
  49. def _Send(data):
  50. debugPrint(f'>> {data}')
  51. strdata = bytearray(data, 'utf8') + b'\n'
  52. port.write(strdata)
  53. time.sleep(0.010)
  54. def _Recv():
  55. clean_responses = []
  56. responses = port.readlines()
  57. for Resp in responses:
  58. # Suppress invalid chars (coming from debug info)
  59. try:
  60. clean_response = Resp.decode('utf8').rstrip().lstrip()
  61. clean_responses.append(clean_response)
  62. debugPrint(f'<< {clean_response}')
  63. except:
  64. pass
  65. return clean_responses
  66. #------------------#
  67. # SDCard functions #
  68. #------------------#
  69. def _CheckSDCard():
  70. debugPrint('Checking SD card...')
  71. _Send('M21')
  72. Responses = _Recv()
  73. if len(Responses) < 1 or not any('SD card ok' in r for r in Responses):
  74. raise Exception('Error accessing SD card')
  75. debugPrint('SD Card OK')
  76. return True
  77. #----------------#
  78. # File functions #
  79. #----------------#
  80. def _GetFirmwareFiles(UseLongFilenames):
  81. debugPrint('Get firmware files...')
  82. _Send(f"M20 F{'L' if UseLongFilenames else ''}")
  83. Responses = _Recv()
  84. if len(Responses) < 3 or not any('file list' in r for r in Responses):
  85. raise Exception('Error getting firmware files')
  86. debugPrint('OK')
  87. return Responses
  88. def _FilterFirmwareFiles(FirmwareList, UseLongFilenames):
  89. Firmwares = []
  90. for FWFile in FirmwareList:
  91. # For long filenames take the 3rd column of the firmwares list
  92. if UseLongFilenames:
  93. Space = 0
  94. Space = FWFile.find(' ')
  95. if Space >= 0: Space = FWFile.find(' ', Space + 1)
  96. if Space >= 0: FWFile = FWFile[Space + 1:]
  97. if not '/' in FWFile and '.BIN' in FWFile.upper():
  98. Firmwares.append(FWFile[:FWFile.upper().index('.BIN') + 4])
  99. return Firmwares
  100. def _RemoveFirmwareFile(FirmwareFile):
  101. _Send(f'M30 /{FirmwareFile}')
  102. Responses = _Recv()
  103. Removed = len(Responses) >= 1 and any('File deleted' in r for r in Responses)
  104. if not Removed:
  105. raise Exception(f"Firmware file '{FirmwareFile}' not removed")
  106. return Removed
  107. def _RollbackUpload(FirmwareFile):
  108. if not rollback: return
  109. print(f"Rollback: trying to delete firmware '{FirmwareFile}'...")
  110. _OpenPort()
  111. # Wait for SD card release
  112. time.sleep(1)
  113. # Remount SD card
  114. _CheckSDCard()
  115. print(' OK' if _RemoveFirmwareFile(FirmwareFile) else ' Error!')
  116. _ClosePort()
  117. #---------------------#
  118. # Callback Entrypoint #
  119. #---------------------#
  120. port = None
  121. protocol = None
  122. filetransfer = None
  123. rollback = False
  124. # Get Marlin evironment vars
  125. MarlinEnv = env['MARLIN_FEATURES']
  126. marlin_pioenv = _GetMarlinEnv(MarlinEnv, 'PIOENV')
  127. marlin_motherboard = _GetMarlinEnv(MarlinEnv, 'MOTHERBOARD')
  128. marlin_board_info_name = _GetMarlinEnv(MarlinEnv, 'BOARD_INFO_NAME')
  129. marlin_board_custom_build_flags = _GetMarlinEnv(MarlinEnv, 'BOARD_CUSTOM_BUILD_FLAGS')
  130. marlin_firmware_bin = _GetMarlinEnv(MarlinEnv, 'FIRMWARE_BIN')
  131. marlin_long_filename_host_support = _GetMarlinEnv(MarlinEnv, 'LONG_FILENAME_HOST_SUPPORT') is not None
  132. marlin_longname_write = _GetMarlinEnv(MarlinEnv, 'LONG_FILENAME_WRITE_SUPPORT') is not None
  133. marlin_custom_firmware_upload = _GetMarlinEnv(MarlinEnv, 'CUSTOM_FIRMWARE_UPLOAD') is not None
  134. marlin_short_build_version = _GetMarlinEnv(MarlinEnv, 'SHORT_BUILD_VERSION')
  135. marlin_string_config_h_author = _GetMarlinEnv(MarlinEnv, 'STRING_CONFIG_H_AUTHOR')
  136. # Get firmware upload params
  137. upload_firmware_source_path = os.path.join(env["PROJECT_BUILD_DIR"], env["PIOENV"], f"{env['PROGNAME']}.bin") if 'PROGNAME' in env else str(source[0])
  138. # Source firmware filename
  139. upload_speed = env['UPLOAD_SPEED'] if 'UPLOAD_SPEED' in env else 115200
  140. # baud rate of serial connection
  141. upload_port = _GetUploadPort(env) # Serial port to use
  142. # Set local upload params
  143. upload_firmware_target_name = os.path.basename(upload_firmware_source_path)
  144. # Target firmware filename
  145. upload_timeout = 1000 # Communication timout, lossy/slow connections need higher values
  146. upload_blocksize = 512 # Transfer block size. 512 = Autodetect
  147. upload_compression = True # Enable compression
  148. upload_error_ratio = 0 # Simulated corruption ratio
  149. upload_test = False # Benchmark the serial link without storing the file
  150. upload_reset = True # Trigger a soft reset for firmware update after the upload
  151. # Set local upload params based on board type to change script behavior
  152. # "upload_delete_old_bins": delete all *.bin files in the root of SD Card
  153. upload_delete_old_bins = marlin_motherboard in ['BOARD_CREALITY_V4', 'BOARD_CREALITY_V4210', 'BOARD_CREALITY_V422', 'BOARD_CREALITY_V423',
  154. 'BOARD_CREALITY_V427', 'BOARD_CREALITY_V431', 'BOARD_CREALITY_V452', 'BOARD_CREALITY_V453',
  155. 'BOARD_CREALITY_V24S1']
  156. # "upload_random_name": generate a random 8.3 firmware filename to upload
  157. upload_random_filename = upload_delete_old_bins and not marlin_long_filename_host_support
  158. # Heatshrink module is needed (only) for compression
  159. if upload_compression:
  160. if sys.version_info[0] > 2:
  161. try:
  162. import heatshrink2
  163. except ImportError:
  164. print("Installing 'heatshrink2' python module...")
  165. env.Execute(env.subst("$PYTHONEXE -m pip install heatshrink2"))
  166. else:
  167. try:
  168. import heatshrink
  169. except ImportError:
  170. print("Installing 'heatshrink' python module...")
  171. env.Execute(env.subst("$PYTHONEXE -m pip install heatshrink"))
  172. try:
  173. # Start upload job
  174. print(f"Uploading firmware '{os.path.basename(upload_firmware_target_name)}' to '{marlin_motherboard}' via '{upload_port}'")
  175. # Dump some debug info
  176. if Debug:
  177. print('Upload using:')
  178. print('---- Marlin -----------------------------------')
  179. print(f' PIOENV : {marlin_pioenv}')
  180. print(f' SHORT_BUILD_VERSION : {marlin_short_build_version}')
  181. print(f' STRING_CONFIG_H_AUTHOR : {marlin_string_config_h_author}')
  182. print(f' MOTHERBOARD : {marlin_motherboard}')
  183. print(f' BOARD_INFO_NAME : {marlin_board_info_name}')
  184. print(f' CUSTOM_BUILD_FLAGS : {marlin_board_custom_build_flags}')
  185. print(f' FIRMWARE_BIN : {marlin_firmware_bin}')
  186. print(f' LONG_FILENAME_HOST_SUPPORT : {marlin_long_filename_host_support}')
  187. print(f' LONG_FILENAME_WRITE_SUPPORT : {marlin_longname_write}')
  188. print(f' CUSTOM_FIRMWARE_UPLOAD : {marlin_custom_firmware_upload}')
  189. print('---- Upload parameters ------------------------')
  190. print(f' Source : {upload_firmware_source_path}')
  191. print(f' Target : {upload_firmware_target_name}')
  192. print(f' Port : {upload_port} @ {upload_speed} baudrate')
  193. print(f' Timeout : {upload_timeout}')
  194. print(f' Block size : {upload_blocksize}')
  195. print(f' Compression : {upload_compression}')
  196. print(f' Error ratio : {upload_error_ratio}')
  197. print(f' Test : {upload_test}')
  198. print(f' Reset : {upload_reset}')
  199. print('-----------------------------------------------')
  200. # Custom implementations based on board parameters
  201. # Generate a new 8.3 random filename
  202. if upload_random_filename:
  203. upload_firmware_target_name = f"fw-{''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=5))}.BIN"
  204. print(f"Board {marlin_motherboard}: Overriding firmware filename to '{upload_firmware_target_name}'")
  205. # Delete all *.bin files on the root of SD Card (if flagged)
  206. if upload_delete_old_bins:
  207. # CUSTOM_FIRMWARE_UPLOAD is needed for this feature
  208. if not marlin_custom_firmware_upload:
  209. raise Exception(f"CUSTOM_FIRMWARE_UPLOAD must be enabled in 'Configuration_adv.h' for '{marlin_motherboard}'")
  210. # Init & Open serial port
  211. port = serial.Serial(upload_port, baudrate = upload_speed, write_timeout = 0, timeout = 0.1)
  212. _OpenPort()
  213. # Check SD card status
  214. _CheckSDCard()
  215. # Get firmware files
  216. FirmwareFiles = _GetFirmwareFiles(marlin_long_filename_host_support)
  217. if Debug:
  218. for FirmwareFile in FirmwareFiles:
  219. print(f'Found: {FirmwareFile}')
  220. # Get all 1st level firmware files (to remove)
  221. OldFirmwareFiles = _FilterFirmwareFiles(FirmwareFiles[1:len(FirmwareFiles)-2], marlin_long_filename_host_support) # Skip header and footers of list
  222. if len(OldFirmwareFiles) == 0:
  223. print('No old firmware files to delete')
  224. else:
  225. print(f"Remove {len(OldFirmwareFiles)} old firmware file{'s' if len(OldFirmwareFiles) != 1 else ''}:")
  226. for OldFirmwareFile in OldFirmwareFiles:
  227. print(f" -Removing- '{OldFirmwareFile}'...")
  228. print(' OK' if _RemoveFirmwareFile(OldFirmwareFile) else ' Error!')
  229. # Close serial
  230. _ClosePort()
  231. # Cleanup completed
  232. debugPrint('Cleanup completed')
  233. # WARNING! The serial port must be closed here because the serial transfer that follow needs it!
  234. # Upload firmware file
  235. debugPrint(f"Copy '{upload_firmware_source_path}' --> '{upload_firmware_target_name}'")
  236. protocol = MarlinBinaryProtocol.Protocol(upload_port, upload_speed, upload_blocksize, float(upload_error_ratio), int(upload_timeout))
  237. #echologger = MarlinBinaryProtocol.EchoProtocol(protocol)
  238. protocol.connect()
  239. # Mark the rollback (delete broken transfer) from this point on
  240. rollback = True
  241. filetransfer = MarlinBinaryProtocol.FileTransferProtocol(protocol)
  242. transferOK = filetransfer.copy(upload_firmware_source_path, upload_firmware_target_name, upload_compression, upload_test)
  243. protocol.disconnect()
  244. # Notify upload completed
  245. protocol.send_ascii('M117 Firmware uploaded' if transferOK else 'M117 Firmware upload failed')
  246. # Remount SD card
  247. print('Wait for SD card release...')
  248. time.sleep(1)
  249. print('Remount SD card')
  250. protocol.send_ascii('M21')
  251. # Transfer failed?
  252. if not transferOK:
  253. protocol.shutdown()
  254. _RollbackUpload(upload_firmware_target_name)
  255. else:
  256. # Trigger firmware update
  257. if upload_reset:
  258. print('Trigger firmware update...')
  259. protocol.send_ascii('M997', True)
  260. protocol.shutdown()
  261. print('Firmware update completed' if transferOK else 'Firmware update failed')
  262. return 0 if transferOK else -1
  263. except KeyboardInterrupt:
  264. print('Aborted by user')
  265. if filetransfer: filetransfer.abort()
  266. if protocol:
  267. protocol.disconnect()
  268. protocol.shutdown()
  269. _RollbackUpload(upload_firmware_target_name)
  270. _ClosePort()
  271. raise
  272. except serial.SerialException as se:
  273. # This exception is raised only for send_ascii data (not for binary transfer)
  274. print(f'Serial excepion: {se}, transfer aborted')
  275. if protocol:
  276. protocol.disconnect()
  277. protocol.shutdown()
  278. _RollbackUpload(upload_firmware_target_name)
  279. _ClosePort()
  280. raise Exception(se)
  281. except MarlinBinaryProtocol.FatalError:
  282. print('Too many retries, transfer aborted')
  283. if protocol:
  284. protocol.disconnect()
  285. protocol.shutdown()
  286. _RollbackUpload(upload_firmware_target_name)
  287. _ClosePort()
  288. raise
  289. except Exception as ex:
  290. print(f"\nException: {ex}, transfer aborted")
  291. if protocol:
  292. protocol.disconnect()
  293. protocol.shutdown()
  294. _RollbackUpload(upload_firmware_target_name)
  295. _ClosePort()
  296. print('Firmware not updated')
  297. raise
  298. # Attach custom upload callback
  299. env.Replace(UPLOADCMD=Upload)