My Marlin configs for Fabrikator Mini and CTC i3 Pro B
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

auto_build.py 48KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300
  1. #!/usr/bin/env python
  2. #######################################
  3. #
  4. # Marlin 3D Printer Firmware
  5. # Copyright (c) 2019 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
  6. #
  7. # Based on Sprinter and grbl.
  8. # Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
  9. #
  10. # This program is free software: you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation, either version 3 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License
  21. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. #
  23. #######################################
  24. #######################################
  25. #
  26. # Revision: 2.0.1
  27. #
  28. # Description: script to automate PlatformIO builds
  29. # CLI: python auto_build.py build_option
  30. # build_option (required)
  31. # build executes -> platformio run -e target_env
  32. # clean executes -> platformio run --target clean -e target_env
  33. # upload executes -> platformio run --target upload -e target_env
  34. # traceback executes -> platformio run --target upload -e target_env
  35. # program executes -> platformio run --target program -e target_env
  36. # test executes -> platformio test upload -e target_env
  37. # remote executes -> platformio remote run --target upload -e target_env
  38. # debug executes -> platformio debug -e target_env
  39. #
  40. # 'traceback' just uses the debug variant of the target environment if one exists
  41. #
  42. #######################################
  43. #######################################
  44. #
  45. # General program flow
  46. #
  47. # 1. Scans Configuration.h for the motherboard name and Marlin version.
  48. # 2. Scans pins.h for the motherboard.
  49. # returns the CPU(s) and platformio environment(s) used by the motherboard
  50. # 3. If further info is needed then a popup gets it from the user.
  51. # 4. The OUTPUT_WINDOW class creates a window to display the output of the PlatformIO program.
  52. # 5. A thread is created by the OUTPUT_WINDOW class in order to execute the RUN_PIO function.
  53. # 6. The RUN_PIO function uses a subprocess to run the CLI version of PlatformIO.
  54. # 7. The "iter(pio_subprocess.stdout.readline, '')" function is used to stream the output of
  55. # PlatformIO back to the RUN_PIO function.
  56. # 8. Each line returned from PlatformIO is formatted to match the color coding seen in the
  57. # PlatformIO GUI.
  58. # 9. If there is a color change within a line then the line is broken at each color change
  59. # and sent separately.
  60. # 10. Each formatted segment (could be a full line or a split line) is put into the queue
  61. # IO_queue as it arrives from the platformio subprocess.
  62. # 11. The OUTPUT_WINDOW class periodically samples IO_queue. If data is available then it
  63. # is written to the window.
  64. # 12. The window stays open until the user closes it.
  65. # 13. The OUTPUT_WINDOW class continues to execute as long as the window is open. This allows
  66. # copying, saving, scrolling of the window. A right click popup is available.
  67. #
  68. #######################################
  69. from __future__ import print_function
  70. from __future__ import division
  71. import sys
  72. import os
  73. pwd = os.getcwd() # make sure we're executing from the correct directory level
  74. pwd = pwd.replace('\\', '/')
  75. if 0 <= pwd.find('buildroot/share/atom'):
  76. pwd = pwd[ : pwd.find('buildroot/share/atom')]
  77. os.chdir(pwd)
  78. print('pwd: ', pwd)
  79. num_args = len(sys.argv)
  80. if num_args > 1:
  81. build_type = str(sys.argv[1])
  82. else:
  83. print('Please specify build type')
  84. exit()
  85. print('build_type: ', build_type)
  86. print('\nWorking\n')
  87. python_ver = sys.version_info[0] # major version - 2 or 3
  88. print("python version " + str(sys.version_info[0]) + "." + str(sys.version_info[1]) + "." + str(sys.version_info[2]))
  89. import platform
  90. current_OS = platform.system()
  91. #globals
  92. target_env = ''
  93. board_name = ''
  94. from datetime import datetime, date, time
  95. #########
  96. # Python 2 error messages:
  97. # Can't find a usable init.tcl in the following directories ...
  98. # error "invalid command name "tcl_findLibrary""
  99. #
  100. # Fix for the above errors on my Win10 system:
  101. # search all init.tcl files for the line "package require -exact Tcl" that has the highest 8.5.x number
  102. # copy it into the first directory listed in the error messages
  103. # set the environmental variables TCLLIBPATH and TCL_LIBRARY to the directory where you found the init.tcl file
  104. # reboot
  105. #########
  106. ##########################################################################################
  107. #
  108. # popup to get input from user
  109. #
  110. ##########################################################################################
  111. def get_answer(board_name, cpu_label_txt, cpu_a_txt, cpu_b_txt):
  112. if python_ver == 2:
  113. import Tkinter as tk
  114. else:
  115. import tkinter as tk
  116. def CPU_exit_3(): # forward declare functions
  117. CPU_exit_3_()
  118. def CPU_exit_4():
  119. CPU_exit_4_()
  120. def kill_session():
  121. kill_session_()
  122. root_get_answer = tk.Tk()
  123. root_get_answer.attributes("-topmost", True)
  124. root_get_answer.chk_state_1 = 1 # declare variables used by TK and enable
  125. chk_state_1 = 0 # set initial state of check boxes
  126. global get_answer_val
  127. get_answer_val = 2 # return get_answer_val, set default to match chk_state_1 default
  128. l1 = tk.Label(text=board_name,
  129. fg = "light green",
  130. bg = "dark green",
  131. font = "Helvetica 12 bold").grid(row=1)
  132. l2 = tk.Label(text=cpu_label_txt,
  133. fg = "light green",
  134. bg = "dark green",
  135. font = "Helvetica 16 bold italic").grid(row=2)
  136. b4 = tk.Checkbutton(text=cpu_a_txt,
  137. fg = "black",
  138. font = "Times 20 bold ",
  139. variable=chk_state_1, onvalue=1, offvalue=0,
  140. command = CPU_exit_3).grid(row=3)
  141. b5 = tk.Checkbutton(text=cpu_b_txt,
  142. fg = "black",
  143. font = "Times 20 bold ",
  144. variable=chk_state_1, onvalue=0, offvalue=1,
  145. command = CPU_exit_4).grid(row=4) # use same variable but inverted so they will track
  146. b6 = tk.Button(text="CONFIRM",
  147. fg = "blue",
  148. font = "Times 20 bold ",
  149. command = root_get_answer.destroy).grid(row=5, pady=4)
  150. b7 = tk.Button(text="CANCEL",
  151. fg = "red",
  152. font = "Times 12 bold ",
  153. command = kill_session).grid(row=6, pady=4)
  154. def CPU_exit_3_():
  155. global get_answer_val
  156. get_answer_val = 1
  157. def CPU_exit_4_():
  158. global get_answer_val
  159. get_answer_val = 2
  160. def kill_session_():
  161. raise SystemExit(0) # kill everything
  162. root_get_answer.mainloop()
  163. # end - get answer
  164. #
  165. # move custom board definitions from project folder to PlatformIO
  166. #
  167. def resolve_path(path):
  168. import os
  169. # turn the selection into a partial path
  170. if 0 <= path.find('"'):
  171. path = path[ path.find('"') : ]
  172. if 0 <= path.find(', line '):
  173. path = path.replace(', line ', ':')
  174. path = path.replace('"', '')
  175. #get line and column numbers
  176. line_num = 1
  177. column_num = 1
  178. line_start = path.find(':', 2) # use 2 here so don't eat Windows full path
  179. column_start = path.find(':', line_start + 1)
  180. if column_start == -1:
  181. column_start = len(path)
  182. column_end = path.find(':', column_start + 1)
  183. if column_end == -1:
  184. column_end = len(path)
  185. if 0 <= line_start:
  186. line_num = path[ line_start + 1 : column_start]
  187. if line_num == '':
  188. line_num = 1
  189. if column_start != column_end:
  190. column_num = path[ column_start + 1 : column_end]
  191. if column_num == '':
  192. column_num = 0
  193. index_end = path.find(',')
  194. if 0 <= index_end:
  195. path = path[ : index_end] # delete comma and anything after
  196. index_end = path.find(':', 2)
  197. if 0 <= index_end:
  198. path = path[ : path.find(':', 2)] # delete the line number and anything after
  199. path = path.replace('\\','/')
  200. if 1 == path.find(':') and current_OS == 'Windows':
  201. return path, line_num, column_num # found a full path - no need for further processing
  202. elif 0 == path.find('/') and (current_OS == 'Linux' or current_OS == 'Darwin'):
  203. return path, line_num, column_num # found a full path - no need for further processing
  204. else:
  205. # resolve as many '../' as we can
  206. while 0 <= path.find('../'):
  207. end = path.find('../') - 1
  208. start = path.find('/')
  209. while 0 <= path.find('/',start) and end > path.find('/',start):
  210. start = path.find('/',start) + 1
  211. path = path[0:start] + path[end + 4: ]
  212. # this is an alternative to the above - it just deletes the '../' section
  213. # start_temp = path.find('../')
  214. # while 0 <= path.find('../',start_temp):
  215. # start = path.find('../',start_temp)
  216. # start_temp = start + 1
  217. # if 0 <= start:
  218. # path = path[start + 2 : ]
  219. start = path.find('/')
  220. if start != 0: # make sure path starts with '/'
  221. while 0 == path.find(' '): # eat any spaces at the beginning
  222. path = path[ 1 : ]
  223. path = '/' + path
  224. if current_OS == 'Windows':
  225. search_path = path.replace('/', '\\') # os.walk uses '\' in Windows
  226. else:
  227. search_path = path
  228. start_path = os.path.abspath('')
  229. # search project directory for the selection
  230. found = False
  231. full_path = ''
  232. for root, directories, filenames in os.walk(start_path):
  233. for filename in filenames:
  234. if 0 <= root.find('.git'): # don't bother looking in this directory
  235. break
  236. full_path = os.path.join(root,filename)
  237. if 0 <= full_path.find(search_path):
  238. found = True
  239. break
  240. if found:
  241. break
  242. return full_path, line_num, column_num
  243. # end - resolve_path
  244. #
  245. # Opens the file in the preferred editor at the line & column number
  246. # If the preferred editor isn't already running then it tries the next.
  247. # If none are open then the system default is used.
  248. #
  249. # Editor order:
  250. # 1. Notepad++ (Windows only)
  251. # 2. Sublime Text
  252. # 3. Atom
  253. # 4. System default (opens at line 1, column 1 only)
  254. #
  255. def open_file(path):
  256. import subprocess
  257. file_path, line_num, column_num = resolve_path(path)
  258. if file_path == '' :
  259. return
  260. if current_OS == 'Windows':
  261. editor_note = subprocess.check_output('wmic process where "name=' + "'notepad++.exe'" + '" get ExecutablePath')
  262. editor_sublime = subprocess.check_output('wmic process where "name=' + "'sublime_text.exe'" + '" get ExecutablePath')
  263. editor_atom = subprocess.check_output('wmic process where "name=' + "'atom.exe'" + '" get ExecutablePath')
  264. if 0 <= editor_note.find('notepad++.exe'):
  265. start = editor_note.find('\n') + 1
  266. end = editor_note.find('\n',start + 5) -4
  267. editor_note = editor_note[ start : end]
  268. command = file_path , ' -n' + str(line_num) , ' -c' + str(column_num)
  269. subprocess.Popen([editor_note, command])
  270. elif 0 <= editor_sublime.find('sublime_text.exe'):
  271. start = editor_sublime.find('\n') + 1
  272. end = editor_sublime.find('\n',start + 5) -4
  273. editor_sublime = editor_sublime[ start : end]
  274. command = file_path + ':' + line_num + ':' + column_num
  275. subprocess.Popen([editor_sublime, command])
  276. elif 0 <= editor_atom.find('atom.exe'):
  277. start = editor_atom.find('\n') + 1
  278. end = editor_atom.find('\n',start + 5) -4
  279. editor_atom = editor_atom[ start : end]
  280. command = file_path + ':' + str(line_num) + ':' + str(column_num)
  281. subprocess.Popen([editor_atom, command])
  282. else:
  283. os.startfile(resolve_path(path)) # open file with default app
  284. elif current_OS == 'Linux':
  285. command = file_path + ':' + str(line_num) + ':' + str(column_num)
  286. index_end = command.find(',')
  287. if 0 <= index_end:
  288. command = command[ : index_end] # sometimes a comma magically appears, don't want it
  289. running_apps = subprocess.Popen('ps ax -o cmd', stdout=subprocess.PIPE, shell=True)
  290. (output, err) = running_apps.communicate()
  291. temp = output.split('\n')
  292. def find_editor_linux(name, search_obj):
  293. for line in search_obj:
  294. if 0 <= line.find(name):
  295. path = line
  296. return True, path
  297. return False , ''
  298. (success_sublime, editor_path_sublime) = find_editor_linux('sublime_text',temp)
  299. (success_atom, editor_path_atom) = find_editor_linux('atom',temp)
  300. if success_sublime:
  301. subprocess.Popen([editor_path_sublime, command])
  302. elif success_atom:
  303. subprocess.Popen([editor_path_atom, command])
  304. else:
  305. os.system('xdg-open ' + file_path )
  306. elif current_OS == 'Darwin': # MAC
  307. command = file_path + ':' + str(line_num) + ':' + str(column_num)
  308. index_end = command.find(',')
  309. if 0 <= index_end:
  310. command = command[ : index_end] # sometimes a comma magically appears, don't want it
  311. running_apps = subprocess.Popen('ps axwww -o command', stdout=subprocess.PIPE, shell=True)
  312. (output, err) = running_apps.communicate()
  313. temp = output.split('\n')
  314. def find_editor_mac(name, search_obj):
  315. for line in search_obj:
  316. if 0 <= line.find(name):
  317. path = line
  318. if 0 <= path.find('-psn'):
  319. path = path[ : path.find('-psn') - 1 ]
  320. return True, path
  321. return False , ''
  322. (success_sublime, editor_path_sublime) = find_editor_mac('Sublime',temp)
  323. (success_atom, editor_path_atom) = find_editor_mac('Atom',temp)
  324. if success_sublime:
  325. subprocess.Popen([editor_path_sublime, command])
  326. elif success_atom:
  327. subprocess.Popen([editor_path_atom, command])
  328. else:
  329. os.system('open ' + file_path )
  330. # end - open_file
  331. # gets the last build environment
  332. def get_build_last():
  333. env_last = ''
  334. DIR_PWD = os.listdir('.')
  335. if '.pio' in DIR_PWD:
  336. date_last = 0.0
  337. DIR__pioenvs = os.listdir('.pio')
  338. for name in DIR__pioenvs:
  339. if 0 <= name.find('.') or 0 <= name.find('-'): # skip files in listing
  340. continue
  341. DIR_temp = os.listdir('.pio/build/' + name)
  342. for names_temp in DIR_temp:
  343. if 0 == names_temp.find('firmware.'):
  344. date_temp = os.path.getmtime('.pio/build/' + name + '/' + names_temp)
  345. if date_temp > date_last:
  346. date_last = date_temp
  347. env_last = name
  348. return env_last
  349. # gets the board being built from the Configuration.h file
  350. # returns: board name, major version of Marlin being used (1 or 2)
  351. def get_board_name():
  352. board_name = ''
  353. # get board name
  354. with open('Marlin/Configuration.h', 'r') as myfile:
  355. Configuration_h = myfile.read()
  356. Configuration_h = Configuration_h.split('\n')
  357. Marlin_ver = 0 # set version to invalid number
  358. for lines in Configuration_h:
  359. if 0 == lines.find('#define CONFIGURATION_H_VERSION 01'):
  360. Marlin_ver = 1
  361. if 0 == lines.find('#define CONFIGURATION_H_VERSION 02'):
  362. Marlin_ver = 2
  363. board = lines.find(' BOARD_') + 1
  364. motherboard = lines.find(' MOTHERBOARD ') + 1
  365. define = lines.find('#define ')
  366. comment = lines.find('//')
  367. if (comment == -1 or comment > board) and \
  368. board > motherboard and \
  369. motherboard > define and \
  370. define >= 0 :
  371. spaces = lines.find(' ', board) # find the end of the board substring
  372. if spaces == -1:
  373. board_name = lines[board : ]
  374. else:
  375. board_name = lines[board : spaces]
  376. break
  377. return board_name, Marlin_ver
  378. # extract first environment name it finds after the start position
  379. # returns: environment name and position to start the next search from
  380. def get_env_from_line(line, start_position):
  381. env = ''
  382. next_position = -1
  383. env_position = line.find('env:', start_position)
  384. if 0 < env_position:
  385. next_position = line.find(' ', env_position + 4)
  386. if 0 < next_position:
  387. env = line[env_position + 4 : next_position]
  388. else:
  389. env = line[env_position + 4 : ] # at the end of the line
  390. return env, next_position
  391. #scans pins.h for board name and returns the environment(s) it finds
  392. def get_starting_env(board_name_full, version):
  393. # get environment starting point
  394. if version == 1:
  395. path = 'Marlin/pins.h'
  396. if version == 2:
  397. path = 'Marlin/src/pins/pins.h'
  398. with open(path, 'r') as myfile:
  399. pins_h = myfile.read()
  400. env_A = ''
  401. env_B = ''
  402. env_C = ''
  403. board_name = board_name_full[ 6 : ] # only use the part after "BOARD_" since we're searching the pins.h file
  404. pins_h = pins_h.split('\n')
  405. environment = ''
  406. board_line = ''
  407. cpu_A = ''
  408. cpu_B = ''
  409. i = 0
  410. list_start_found = False
  411. for lines in pins_h:
  412. i = i + 1 # i is always one ahead of the index into pins_h
  413. if 0 < lines.find("Unknown MOTHERBOARD value set in Configuration.h"):
  414. break # no more
  415. if 0 < lines.find('1280'):
  416. list_start_found = True
  417. if list_start_found == False: # skip lines until find start of CPU list
  418. continue
  419. board = lines.find(board_name)
  420. comment_start = lines.find('// ')
  421. cpu_A_loc = comment_start
  422. cpu_B_loc = 0
  423. if board > 0: # need to look at the next line for environment info
  424. cpu_line = pins_h[i]
  425. comment_start = cpu_line.find('// ')
  426. env_A, next_position = get_env_from_line(cpu_line, comment_start) # get name of environment & start of search for next
  427. env_B, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it exists
  428. env_C, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it exists
  429. break
  430. return env_A, env_B, env_C
  431. # scans input string for CPUs that the users may need to select from
  432. # returns: CPU name
  433. def get_CPU_name(environment):
  434. CPU_list = ('1280', '2560','644', '1284', 'LPC1768', 'DUE')
  435. CPU_name = ''
  436. for CPU in CPU_list:
  437. if 0 < environment.find(CPU):
  438. return CPU
  439. # get environment to be used for the build
  440. # returns: environment
  441. def get_env(board_name, ver_Marlin):
  442. def no_environment():
  443. print('ERROR - no environment for this board')
  444. print(board_name)
  445. raise SystemExit(0) # no environment so quit
  446. def invalid_board():
  447. print('ERROR - invalid board')
  448. print(board_name)
  449. raise SystemExit(0) # quit if unable to find board
  450. CPU_question = ( ('1280', '2560', " 1280 or 2560 CPU? "), ('644', '1284', " 644 or 1284 CPU? ") )
  451. if 0 < board_name.find('MELZI') :
  452. get_answer(' ' + board_name + ' ', " Which flavor of Melzi? ", "Melzi (Optiboot bootloader)", "Melzi ")
  453. if 1 == get_answer_val:
  454. target_env = 'melzi_optiboot'
  455. else:
  456. target_env = 'melzi'
  457. else:
  458. env_A, env_B, env_C = get_starting_env(board_name, ver_Marlin)
  459. if env_A == '':
  460. no_environment()
  461. if env_B == '':
  462. return env_A # only one environment so finished
  463. CPU_A = get_CPU_name(env_A)
  464. CPU_B = get_CPU_name(env_B)
  465. for item in CPU_question:
  466. if CPU_A == item[0]:
  467. get_answer(' ' + board_name + ' ', item[2], item[0], item[1])
  468. if 2 == get_answer_val:
  469. target_env = env_B
  470. else:
  471. target_env = env_A
  472. return target_env
  473. if env_A == 'LPC1768':
  474. if build_type == 'traceback' or (build_type == 'clean' and get_build_last() == 'LPC1768_debug_and_upload'):
  475. target_env = 'LPC1768_debug_and_upload'
  476. else:
  477. target_env = 'LPC1768'
  478. elif env_A == 'DUE':
  479. target_env = 'DUE'
  480. if build_type == 'traceback' or (build_type == 'clean' and get_build_last() == 'DUE_debug'):
  481. target_env = 'DUE_debug'
  482. elif env_B == 'DUE_USB':
  483. get_answer(' ' + board_name + ' ', " DUE: need download port ", "USB (native USB) port", "Programming port ")
  484. if 1 == get_answer_val:
  485. target_env = 'DUE_USB'
  486. else:
  487. target_env = 'DUE'
  488. else:
  489. invalid_board()
  490. if build_type == 'traceback' and target_env != 'LPC1768_debug_and_upload' and target_env != 'DUE_debug' and Marlin_ver == 2:
  491. print("ERROR - this board isn't setup for traceback")
  492. print('board_name: ', board_name)
  493. print('target_env: ', target_env)
  494. raise SystemExit(0)
  495. return target_env
  496. # end - get_env
  497. # puts screen text into queue so that the parent thread can fetch the data from this thread
  498. if python_ver == 2:
  499. import Queue as queue
  500. else:
  501. import queue as queue
  502. IO_queue = queue.Queue()
  503. #PIO_queue = queue.Queue() not used!
  504. def write_to_screen_queue(text, format_tag = 'normal'):
  505. double_in = [text, format_tag]
  506. IO_queue.put(double_in, block = False)
  507. #
  508. # send one line to the terminal screen with syntax highlighting
  509. #
  510. # input: unformatted text, flags from previous run
  511. # returns: formatted text ready to go to the terminal, flags from this run
  512. #
  513. # This routine remembers the status from call to call because previous
  514. # lines can affect how the current line is highlighted
  515. #
  516. # 'static' variables - init here and then keep updating them from within print_line
  517. warning = False
  518. warning_FROM = False
  519. error = False
  520. standard = True
  521. prev_line_COM = False
  522. next_line_warning = False
  523. warning_continue = False
  524. line_counter = 0
  525. def line_print(line_input):
  526. global warning
  527. global warning_FROM
  528. global error
  529. global standard
  530. global prev_line_COM
  531. global next_line_warning
  532. global warning_continue
  533. global line_counter
  534. # all '0' elements must precede all '1' elements or they'll be skipped
  535. platformio_highlights = [
  536. ['Environment', 0, 'highlight_blue'],
  537. ['[SKIP]', 1, 'warning'],
  538. ['[IGNORED]', 1, 'warning'],
  539. ['[ERROR]', 1, 'error'],
  540. ['[FAILED]', 1, 'error'],
  541. ['[SUCCESS]', 1, 'highlight_green']
  542. ]
  543. def write_to_screen_with_replace(text, highlights): # search for highlights & split line accordingly
  544. did_something = False
  545. for highlight in highlights:
  546. found = text.find(highlight[0])
  547. if did_something == True:
  548. break
  549. if found >= 0 :
  550. did_something = True
  551. if 0 == highlight[1]:
  552. found_1 = text.find(' ')
  553. found_tab = text.find('\t')
  554. if found_1 < 0 or found_1 > found_tab:
  555. found_1 = found_tab
  556. write_to_screen_queue(text[ : found_1 + 1 ])
  557. for highlight_2 in highlights:
  558. if highlight[0] == highlight_2[0] :
  559. continue
  560. found = text.find(highlight_2[0])
  561. if found >= 0 :
  562. found_space = text.find(' ', found_1 + 1)
  563. found_tab = text.find('\t', found_1 + 1)
  564. if found_space < 0 or found_space > found_tab:
  565. found_space = found_tab
  566. found_right = text.find(']', found + 1)
  567. write_to_screen_queue(text[found_1 + 1 : found_space + 1 ], highlight[2])
  568. write_to_screen_queue(text[found_space + 1 : found + 1 ])
  569. write_to_screen_queue(text[found + 1 : found_right], highlight_2[2])
  570. write_to_screen_queue(text[found_right : ] + '\n')
  571. break
  572. break
  573. if 1 == highlight[1]:
  574. found_right = text.find(']', found + 1)
  575. write_to_screen_queue(text[ : found + 1 ])
  576. write_to_screen_queue(text[found + 1 : found_right ], highlight[2])
  577. write_to_screen_queue(text[found_right : ] + '\n' + '\n')
  578. break
  579. if did_something == False:
  580. r_loc = text.find('\r') + 1
  581. if r_loc > 0 and r_loc < len(text): # need to split this line
  582. text = text.split('\r')
  583. for line in text:
  584. if line != '':
  585. write_to_screen_queue(line + '\n')
  586. else:
  587. write_to_screen_queue(text + '\n')
  588. # end - write_to_screen_with_replace
  589. # scan the line
  590. line_counter = line_counter + 1
  591. max_search = len(line_input)
  592. if max_search > 3 :
  593. max_search = 3
  594. beginning = line_input[:max_search]
  595. # set flags
  596. if 0 < line_input.find(': warning: '): # start of warning block
  597. warning = True
  598. warning_FROM = False
  599. error = False
  600. standard = False
  601. prev_line_COM = False
  602. prev_line_COM = False
  603. warning_continue = True
  604. if 0 < line_input.find('Thank you') or 0 < line_input.find('SUMMARY') :
  605. warning = False #standard line found
  606. warning_FROM = False
  607. error = False
  608. standard = True
  609. prev_line_COM = False
  610. warning_continue = False
  611. elif beginning == 'War' or \
  612. beginning == '#er' or \
  613. beginning == 'In ' or \
  614. (beginning != 'Com' and prev_line_COM == True and not(beginning == 'Arc' or beginning == 'Lin' or beginning == 'Ind') or \
  615. next_line_warning == True):
  616. warning = True #warning found
  617. warning_FROM = False
  618. error = False
  619. standard = False
  620. prev_line_COM = False
  621. elif beginning == 'Com' or \
  622. beginning == 'Ver' or \
  623. beginning == ' [E' or \
  624. beginning == 'Rem' or \
  625. beginning == 'Bui' or \
  626. beginning == 'Ind' or \
  627. beginning == 'PLA':
  628. warning = False #standard line found
  629. warning_FROM = False
  630. error = False
  631. standard = True
  632. prev_line_COM = False
  633. warning_continue = False
  634. elif beginning == '***':
  635. warning = False # error found
  636. warning_FROM = False
  637. error = True
  638. standard = False
  639. prev_line_COM = False
  640. elif 0 < line_input.find(': error:') or \
  641. 0 < line_input.find(': fatal error:'): # start of warning /error block
  642. warning = False # error found
  643. warning_FROM = False
  644. error = True
  645. standard = False
  646. prev_line_COM = False
  647. warning_continue = True
  648. elif beginning == 'fro' and warning == True or \
  649. beginning == '.pi' : # start of warning /error block
  650. warning_FROM = True
  651. prev_line_COM = False
  652. warning_continue = True
  653. elif warning_continue == True:
  654. warning = True
  655. warning_FROM = False # keep the warning status going until find a standard line or an error
  656. error = False
  657. standard = False
  658. prev_line_COM = False
  659. warning_continue = True
  660. else:
  661. warning = False # unknown so assume standard line
  662. warning_FROM = False
  663. error = False
  664. standard = True
  665. prev_line_COM = False
  666. warning_continue = False
  667. if beginning == 'Com':
  668. prev_line_COM = True
  669. # print based on flags
  670. if standard == True:
  671. write_to_screen_with_replace(line_input, platformio_highlights) #print white on black with substitutions
  672. if warning == True:
  673. write_to_screen_queue(line_input + '\n', 'warning')
  674. if error == True:
  675. write_to_screen_queue(line_input + '\n', 'error')
  676. # end - line_print
  677. ##########################################################################
  678. # #
  679. # run Platformio #
  680. # #
  681. ##########################################################################
  682. # build platformio run -e target_env
  683. # clean platformio run --target clean -e target_env
  684. # upload platformio run --target upload -e target_env
  685. # traceback platformio run --target upload -e target_env
  686. # program platformio run --target program -e target_env
  687. # test platformio test upload -e target_env
  688. # remote platformio remote run --target upload -e target_env
  689. # debug platformio debug -e target_env
  690. def sys_PIO():
  691. ##########################################################################
  692. # #
  693. # run Platformio inside the same shell as this Python script #
  694. # #
  695. ##########################################################################
  696. global build_type
  697. global target_env
  698. import os
  699. print('build_type: ', build_type)
  700. print('starting platformio')
  701. if build_type == 'build':
  702. # pio_result = os.system("echo -en '\033c'")
  703. pio_result = os.system('platformio run -e ' + target_env)
  704. elif build_type == 'clean':
  705. pio_result = os.system('platformio run --target clean -e ' + target_env)
  706. elif build_type == 'upload':
  707. pio_result = os.system('platformio run --target upload -e ' + target_env)
  708. elif build_type == 'traceback':
  709. pio_result = os.system('platformio run --target upload -e ' + target_env)
  710. elif build_type == 'program':
  711. pio_result = os.system('platformio run --target program -e ' + target_env)
  712. elif build_type == 'test':
  713. pio_result = os.system('platformio test upload -e ' + target_env)
  714. elif build_type == 'remote':
  715. pio_result = os.system('platformio remote run --target program -e ' + target_env)
  716. elif build_type == 'debug':
  717. pio_result = os.system('platformio debug -e ' + target_env)
  718. else:
  719. print('ERROR - unknown build type: ', build_type)
  720. raise SystemExit(0) # kill everything
  721. # stream output from subprocess and split it into lines
  722. #for line in iter(pio_subprocess.stdout.readline, ''):
  723. # line_print(line.replace('\n', ''))
  724. # append info used to run PlatformIO
  725. # write_to_screen_queue('\nBoard name: ' + board_name + '\n') # put build info at the bottom of the screen
  726. # write_to_screen_queue('Build type: ' + build_type + '\n')
  727. # write_to_screen_queue('Environment used: ' + target_env + '\n')
  728. # write_to_screen_queue(str(datetime.now()) + '\n')
  729. # end - sys_PIO
  730. def run_PIO(dummy):
  731. global build_type
  732. global target_env
  733. global board_name
  734. print('build_type: ', build_type)
  735. import subprocess
  736. import sys
  737. print('starting platformio')
  738. if build_type == 'build':
  739. # platformio run -e target_env
  740. # combine stdout & stderr so all compile messages are included
  741. pio_subprocess = subprocess.Popen(['platformio', 'run', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  742. elif build_type == 'clean':
  743. # platformio run --target clean -e target_env
  744. # combine stdout & stderr so all compile messages are included
  745. pio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'clean', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  746. elif build_type == 'upload':
  747. # platformio run --target upload -e target_env
  748. # combine stdout & stderr so all compile messages are included
  749. pio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  750. elif build_type == 'traceback':
  751. # platformio run --target upload -e target_env - select the debug environment if there is one
  752. # combine stdout & stderr so all compile messages are included
  753. pio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  754. elif build_type == 'program':
  755. # platformio run --target program -e target_env
  756. # combine stdout & stderr so all compile messages are included
  757. pio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'program', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  758. elif build_type == 'test':
  759. #platformio test upload -e target_env
  760. # combine stdout & stderr so all compile messages are included
  761. pio_subprocess = subprocess.Popen(['platformio', 'test', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  762. elif build_type == 'remote':
  763. # platformio remote run --target upload -e target_env
  764. # combine stdout & stderr so all compile messages are included
  765. pio_subprocess = subprocess.Popen(['platformio', 'remote', 'run', '--target', 'program', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  766. elif build_type == 'debug':
  767. # platformio debug -e target_env
  768. # combine stdout & stderr so all compile messages are included
  769. pio_subprocess = subprocess.Popen(['platformio', 'debug', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  770. else:
  771. print('ERROR - unknown build type: ', build_type)
  772. raise SystemExit(0) # kill everything
  773. # stream output from subprocess and split it into lines
  774. if python_ver == 2:
  775. for line in iter(pio_subprocess.stdout.readline, ''):
  776. line_print(line.replace('\n', ''))
  777. else:
  778. for line in iter(pio_subprocess.stdout.readline, b''):
  779. line = line.decode('utf-8')
  780. line_print(line.replace('\n', ''))
  781. # append info used to run PlatformIO
  782. write_to_screen_queue('\nBoard name: ' + board_name + '\n') # put build info at the bottom of the screen
  783. write_to_screen_queue('Build type: ' + build_type + '\n')
  784. write_to_screen_queue('Environment used: ' + target_env + '\n')
  785. write_to_screen_queue(str(datetime.now()) + '\n')
  786. # end - run_PIO
  787. ########################################################################
  788. import time
  789. import threading
  790. if python_ver == 2:
  791. import Tkinter as tk
  792. import Queue as queue
  793. import ttk
  794. from Tkinter import Tk, Frame, Text, Scrollbar, Menu
  795. #from tkMessageBox import askokcancel this is not used: removed
  796. import tkFileDialog as fileDialog
  797. else:
  798. import tkinter as tk
  799. import queue as queue
  800. from tkinter import ttk, Tk, Frame, Text, Scrollbar, Menu
  801. from tkinter import filedialog
  802. import subprocess
  803. import sys
  804. que = queue.Queue()
  805. #IO_queue = queue.Queue()
  806. class output_window(Text):
  807. # based on Super Text
  808. global continue_updates
  809. continue_updates = True
  810. global search_position
  811. search_position = '' # start with invalid search position
  812. global error_found
  813. error_found = False # are there any errors?
  814. def __init__(self):
  815. self.root = tk.Tk()
  816. self.root.attributes("-topmost", True)
  817. self.frame = tk.Frame(self.root)
  818. self.frame.pack(fill='both', expand=True)
  819. # text widget
  820. #self.text = tk.Text(self.frame, borderwidth=3, relief="sunken")
  821. Text.__init__(self, self.frame, borderwidth=3, relief="sunken")
  822. self.config(tabs=(400,)) # configure Text widget tab stops
  823. self.config(background = 'black', foreground = 'white', font= ("consolas", 12), wrap = 'word', undo = 'True')
  824. #self.config(background = 'black', foreground = 'white', font= ("consolas", 12), wrap = 'none', undo = 'True')
  825. self.config(height = 24, width = 100)
  826. self.config(insertbackground = 'pale green') # keyboard insertion point
  827. self.pack(side='left', fill='both', expand=True)
  828. self.tag_config('normal', foreground = 'white')
  829. self.tag_config('warning', foreground = 'yellow' )
  830. self.tag_config('error', foreground = 'red')
  831. self.tag_config('highlight_green', foreground = 'green')
  832. self.tag_config('highlight_blue', foreground = 'cyan')
  833. self.tag_config('error_highlight_inactive', background = 'dim gray')
  834. self.tag_config('error_highlight_active', background = 'light grey')
  835. self.bind_class("Text","<Control-a>", self.select_all) # required in windows, works in others
  836. self.bind_all("<Control-Shift-E>", self.scroll_errors)
  837. self.bind_class("<Control-Shift-R>", self.rebuild)
  838. # scrollbar
  839. scrb = tk.Scrollbar(self.frame, orient='vertical', command=self.yview)
  840. self.config(yscrollcommand=scrb.set)
  841. scrb.pack(side='right', fill='y')
  842. #self.scrb_Y = tk.Scrollbar(self.frame, orient='vertical', command=self.yview)
  843. #self.scrb_Y.config(yscrollcommand=self.scrb_Y.set)
  844. #self.scrb_Y.pack(side='right', fill='y')
  845. #self.scrb_X = tk.Scrollbar(self.frame, orient='horizontal', command=self.xview)
  846. #self.scrb_X.config(xscrollcommand=self.scrb_X.set)
  847. #self.scrb_X.pack(side='bottom', fill='x')
  848. #scrb_X = tk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.xview) # tk.HORIZONTAL now have a horizsontal scroll bar BUT... shrinks it to a postage stamp and hides far right behind the vertical scroll bar
  849. #self.config(xscrollcommand=scrb_X.set)
  850. #scrb_X.pack(side='bottom', fill='x')
  851. #scrb= tk.Scrollbar(self, orient='vertical', command=self.yview)
  852. #self.config(yscrollcommand=scrb.set)
  853. #scrb.pack(side='right', fill='y')
  854. #self.config(height = 240, width = 1000) # didn't get the size baCK TO NORMAL
  855. #self.pack(side='left', fill='both', expand=True) # didn't get the size baCK TO NORMAL
  856. # pop-up menu
  857. self.popup = tk.Menu(self, tearoff=0)
  858. self.popup.add_command(label='Copy', command=self._copy)
  859. self.popup.add_command(label='Paste', command=self._paste)
  860. self.popup.add_separator()
  861. self.popup.add_command(label='Cut', command=self._cut)
  862. self.popup.add_separator()
  863. self.popup.add_command(label='Select All', command=self._select_all)
  864. self.popup.add_command(label='Clear All', command=self._clear_all)
  865. self.popup.add_separator()
  866. self.popup.add_command(label='Save As', command=self._file_save_as)
  867. self.popup.add_separator()
  868. #self.popup.add_command(label='Repeat Build(CTL-shift-r)', command=self._rebuild)
  869. self.popup.add_command(label='Repeat Build', command=self._rebuild)
  870. self.popup.add_separator()
  871. self.popup.add_command(label='Scroll Errors (CTL-shift-e)', command=self._scroll_errors)
  872. self.popup.add_separator()
  873. self.popup.add_command(label='Open File at Cursor', command=self._open_selected_file)
  874. if current_OS == 'Darwin': # MAC
  875. self.bind('<Button-2>', self._show_popup) # macOS only
  876. else:
  877. self.bind('<Button-3>', self._show_popup) # Windows & Linux
  878. # threading & subprocess section
  879. def start_thread(self, ):
  880. global continue_updates
  881. # create then start a secondary thread to run an arbitrary function
  882. # must have at least one argument
  883. self.secondary_thread = threading.Thread(target = lambda q, arg1: q.put(run_PIO(arg1)), args=(que, ''))
  884. self.secondary_thread.start()
  885. continue_updates = True
  886. # check the Queue in 50ms
  887. self.root.after(50, self.check_thread)
  888. self.root.after(50, self.update)
  889. def check_thread(self): # wait for user to kill the window
  890. global continue_updates
  891. if continue_updates == True:
  892. self.root.after(10, self.check_thread)
  893. def update(self):
  894. global continue_updates
  895. if continue_updates == True:
  896. self.root.after(10, self.update)#method is called every 50ms
  897. temp_text = ['0','0']
  898. if IO_queue.empty():
  899. if not(self.secondary_thread.is_alive()):
  900. continue_updates = False # queue is exhausted and thread is dead so no need for further updates
  901. else:
  902. try:
  903. temp_text = IO_queue.get(block = False)
  904. except Queue.Empty:
  905. continue_updates = False # queue is exhausted so no need for further updates
  906. else:
  907. self.insert('end', temp_text[0], temp_text[1])
  908. self.see("end") # make the last line visible (scroll text off the top)
  909. # text editing section
  910. def _scroll_errors(self):
  911. global search_position
  912. global error_found
  913. if search_position == '': # first time so highlight all errors
  914. countVar = tk.IntVar()
  915. search_position = '1.0'
  916. search_count = 0
  917. while search_position != '' and search_count < 100:
  918. search_position = self.search("error", search_position, stopindex="end", count=countVar, nocase=1)
  919. search_count = search_count + 1
  920. if search_position != '':
  921. error_found = True
  922. end_pos = '{}+{}c'.format(search_position, 5)
  923. self.tag_add("error_highlight_inactive", search_position, end_pos)
  924. search_position = '{}+{}c'.format(search_position, 1) # point to the next character for new search
  925. else:
  926. break
  927. if error_found:
  928. if search_position == '':
  929. search_position = self.search("error", '1.0', stopindex="end", nocase=1) # new search
  930. else: # remove active highlight
  931. end_pos = '{}+{}c'.format(search_position, 5)
  932. start_pos = '{}+{}c'.format(search_position, -1)
  933. self.tag_remove("error_highlight_active", start_pos, end_pos)
  934. search_position = self.search("error", search_position, stopindex="end", nocase=1) # finds first occurrence AGAIN on the first time through
  935. if search_position == "": # wrap around
  936. search_position = self.search("error", '1.0', stopindex="end", nocase=1)
  937. end_pos = '{}+{}c'.format(search_position, 5)
  938. self.tag_add("error_highlight_active", search_position, end_pos) # add active highlight
  939. self.see(search_position)
  940. search_position = '{}+{}c'.format(search_position, 1) # point to the next character for new search
  941. def scroll_errors(self, event):
  942. self._scroll_errors()
  943. def _rebuild(self):
  944. #global board_name
  945. #global Marlin_ver
  946. #global target_env
  947. #board_name, Marlin_ver = get_board_name()
  948. #target_env = get_env(board_name, Marlin_ver)
  949. self.start_thread()
  950. def rebuild(self, event):
  951. print("event happened")
  952. self._rebuild()
  953. def _open_selected_file(self):
  954. current_line = self.index('insert')
  955. line_start = current_line[ : current_line.find('.')] + '.0'
  956. line_end = current_line[ : current_line.find('.')] + '.200'
  957. self.mark_set("path_start", line_start)
  958. self.mark_set("path_end", line_end)
  959. path = self.get("path_start", "path_end")
  960. from_loc = path.find('from ')
  961. colon_loc = path.find(': ')
  962. if 0 <= from_loc and ((colon_loc == -1) or (from_loc < colon_loc)) :
  963. path = path [ from_loc + 5 : ]
  964. if 0 <= colon_loc:
  965. path = path [ : colon_loc ]
  966. if 0 <= path.find('\\') or 0 <= path.find('/'): # make sure it really contains a path
  967. open_file(path)
  968. def _file_save_as(self):
  969. self.filename = fileDialog.asksaveasfilename(defaultextension = '.txt')
  970. f = open(self.filename, 'w')
  971. f.write(self.get('1.0', 'end'))
  972. f.close()
  973. def copy(self, event):
  974. try:
  975. selection = self.get(*self.tag_ranges('sel'))
  976. self.clipboard_clear()
  977. self.clipboard_append(selection)
  978. except TypeError:
  979. pass
  980. def cut(self, event):
  981. try:
  982. selection = self.get(*self.tag_ranges('sel'))
  983. self.clipboard_clear()
  984. self.clipboard_append(selection)
  985. self.delete(*self.tag_ranges('sel'))
  986. except TypeError:
  987. pass
  988. def _show_popup(self, event):
  989. '''right-click popup menu'''
  990. if self.root.focus_get() != self:
  991. self.root.focus_set()
  992. try:
  993. self.popup.tk_popup(event.x_root, event.y_root, 0)
  994. finally:
  995. self.popup.grab_release()
  996. def _cut(self):
  997. try:
  998. selection = self.get(*self.tag_ranges('sel'))
  999. self.clipboard_clear()
  1000. self.clipboard_append(selection)
  1001. self.delete(*self.tag_ranges('sel'))
  1002. except TypeError:
  1003. pass
  1004. def cut(self, event):
  1005. self._cut()
  1006. def _copy(self):
  1007. try:
  1008. selection = self.get(*self.tag_ranges('sel'))
  1009. self.clipboard_clear()
  1010. self.clipboard_append(selection)
  1011. except TypeError:
  1012. pass
  1013. def copy(self, event):
  1014. self._copy()
  1015. def _paste(self):
  1016. self.insert('insert', self.selection_get(selection='CLIPBOARD'))
  1017. def _select_all(self):
  1018. self.tag_add('sel', '1.0', 'end')
  1019. def select_all(self, event):
  1020. self.tag_add('sel', '1.0', 'end')
  1021. def _clear_all(self):
  1022. #'''erases all text'''
  1023. #
  1024. #isok = askokcancel('Clear All', 'Erase all text?', frame=self,
  1025. # default='ok')
  1026. #if isok:
  1027. # self.delete('1.0', 'end')
  1028. self.delete('1.0', 'end')
  1029. # end - output_window
  1030. def main():
  1031. ##########################################################################
  1032. # #
  1033. # main program #
  1034. # #
  1035. ##########################################################################
  1036. global build_type
  1037. global target_env
  1038. global board_name
  1039. board_name, Marlin_ver = get_board_name()
  1040. target_env = get_env(board_name, Marlin_ver)
  1041. # Re-use the VSCode terminal, if possible
  1042. if os.environ.get('PLATFORMIO_CALLER', '') == 'vscode':
  1043. sys_PIO()
  1044. else:
  1045. auto_build = output_window()
  1046. auto_build.start_thread() # executes the "run_PIO" function
  1047. auto_build.root.mainloop()
  1048. if __name__ == '__main__':
  1049. main()