Blame | Last modification | View Log | RSS feed
#!/usr/bin/env python######################################### Marlin 3D Printer Firmware# Copyright (c) 2019 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]## Based on Sprinter and grbl.# Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm## This program is free software: you can redistribute it and/or modify# it under the terms of the GNU General Public License as published by# the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with this program. If not, see <http://www.gnu.org/licenses/>.################################################################################# Revision: 2.0.1## Description: script to automate PlatformIO builds# CLI: python auto_build.py build_option# build_option (required)# build executes -> platformio run -e target_env# clean executes -> platformio run --target clean -e target_env# upload executes -> platformio run --target upload -e target_env# traceback executes -> platformio run --target upload -e target_env# program executes -> platformio run --target program -e target_env# test executes -> platformio test upload -e target_env# remote executes -> platformio remote run --target upload -e target_env# debug executes -> platformio debug -e target_env## 'traceback' just uses the debug variant of the target environment if one exists################################################################################# General program flow## 1. Scans Configuration.h for the motherboard name and Marlin version.# 2. Scans pins.h for the motherboard.# returns the CPU(s) and platformio environment(s) used by the motherboard# 3. If further info is needed then a popup gets it from the user.# 4. The OUTPUT_WINDOW class creates a window to display the output of the PlatformIO program.# 5. A thread is created by the OUTPUT_WINDOW class in order to execute the RUN_PIO function.# 6. The RUN_PIO function uses a subprocess to run the CLI version of PlatformIO.# 7. The "iter(pio_subprocess.stdout.readline, '')" function is used to stream the output of# PlatformIO back to the RUN_PIO function.# 8. Each line returned from PlatformIO is formatted to match the color coding seen in the# PlatformIO GUI.# 9. If there is a color change within a line then the line is broken at each color change# and sent separately.# 10. Each formatted segment (could be a full line or a split line) is put into the queue# IO_queue as it arrives from the platformio subprocess.# 11. The OUTPUT_WINDOW class periodically samples IO_queue. If data is available then it# is written to the window.# 12. The window stays open until the user closes it.# 13. The OUTPUT_WINDOW class continues to execute as long as the window is open. This allows# copying, saving, scrolling of the window. A right click popup is available.########################################from __future__ import print_functionfrom __future__ import divisionimport sysimport ospwd = os.getcwd() # make sure we're executing from the correct directory levelpwd = pwd.replace('\\', '/')if 0 <= pwd.find('buildroot/share/atom'):pwd = pwd[ : pwd.find('buildroot/share/atom')]os.chdir(pwd)print('pwd: ', pwd)num_args = len(sys.argv)if num_args > 1:build_type = str(sys.argv[1])else:print('Please specify build type')exit()print('build_type: ', build_type)print('\nWorking\n')python_ver = sys.version_info[0] # major version - 2 or 3if python_ver == 2:print("python version " + str(sys.version_info[0]) + "." + str(sys.version_info[1]) + "." + str(sys.version_info[2]))else:print("python version " + str(sys.version_info[0]))print("This script only runs under python 2")exit()import platformcurrent_OS = platform.system()#globalstarget_env = ''board_name = ''from datetime import datetime, date, time########## Python 2 error messages:# Can't find a usable init.tcl in the following directories ...# error "invalid command name "tcl_findLibrary""## Fix for the above errors on my Win10 system:# search all init.tcl files for the line "package require -exact Tcl" that has the highest 8.5.x number# copy it into the first directory listed in the error messages# set the environmental variables TCLLIBPATH and TCL_LIBRARY to the directory where you found the init.tcl file# reboot##################################################################################################### popup to get input from user###########################################################################################def get_answer(board_name, cpu_label_txt, cpu_a_txt, cpu_b_txt):if python_ver == 2:import Tkinter as tkelse:import tkinter as tkdef CPU_exit_3(): # forward declare functionsCPU_exit_3_()def CPU_exit_4():CPU_exit_4_()def kill_session():kill_session_()root_get_answer = tk.Tk()root_get_answer.chk_state_1 = 1 # declare variables used by TK and enablechk_state_1 = 0 # set initial state of check boxesglobal get_answer_valget_answer_val = 2 # return get_answer_val, set default to match chk_state_1 defaultl1 = tk.Label(text=board_name,fg = "light green",bg = "dark green",font = "Helvetica 12 bold").grid(row=1)l2 = tk.Label(text=cpu_label_txt,fg = "light green",bg = "dark green",font = "Helvetica 16 bold italic").grid(row=2)b4 = tk.Checkbutton(text=cpu_a_txt,fg = "black",font = "Times 20 bold ",variable=chk_state_1, onvalue=1, offvalue=0,command = CPU_exit_3).grid(row=3)b5 = tk.Checkbutton(text=cpu_b_txt,fg = "black",font = "Times 20 bold ",variable=chk_state_1, onvalue=0, offvalue=1,command = CPU_exit_4).grid(row=4) # use same variable but inverted so they will trackb6 = tk.Button(text="CONFIRM",fg = "blue",font = "Times 20 bold ",command = root_get_answer.destroy).grid(row=5, pady=4)b7 = tk.Button(text="CANCEL",fg = "red",font = "Times 12 bold ",command = kill_session).grid(row=6, pady=4)def CPU_exit_3_():global get_answer_valget_answer_val = 1def CPU_exit_4_():global get_answer_valget_answer_val = 2def kill_session_():raise SystemExit(0) # kill everythingroot_get_answer.mainloop()# end - get answer## move custom board definitions from project folder to PlatformIO#def resolve_path(path):import os# turn the selection into a partial pathif 0 <= path.find('"'):path = path[ path.find('"') : ]if 0 <= path.find(', line '):path = path.replace(', line ', ':')path = path.replace('"', '')#get line and column numbersline_num = 1column_num = 1line_start = path.find(':', 2) # use 2 here so don't eat Windows full pathcolumn_start = path.find(':', line_start + 1)if column_start == -1:column_start = len(path)column_end = path.find(':', column_start + 1)if column_end == -1:column_end = len(path)if 0 <= line_start:line_num = path[ line_start + 1 : column_start]if line_num == '':line_num = 1if not(column_start == column_end):column_num = path[ column_start + 1 : column_end]if column_num == '':column_num = 0index_end = path.find(',')if 0 <= index_end:path = path[ : index_end] # delete comma and anything afterindex_end = path.find(':', 2)if 0 <= index_end:path = path[ : path.find(':', 2)] # delete the line number and anything afterpath = path.replace('\\','/')if 1 == path.find(':') and current_OS == 'Windows':return path, line_num, column_num # found a full path - no need for further processingelif 0 == path.find('/') and (current_OS == 'Linux' or current_OS == 'Darwin'):return path, line_num, column_num # found a full path - no need for further processingelse:# resolve as many '../' as we canwhile 0 <= path.find('../'):end = path.find('../') - 1start = path.find('/')while 0 <= path.find('/',start) and end > path.find('/',start):start = path.find('/',start) + 1path = path[0:start] + path[end + 4: ]# this is an alternative to the above - it just deletes the '../' section# start_temp = path.find('../')# while 0 <= path.find('../',start_temp):# start = path.find('../',start_temp)# start_temp = start + 1# if 0 <= start:# path = path[start + 2 : ]start = path.find('/')if not(0 == start): # make sure path starts with '/'while 0 == path.find(' '): # eat any spaces at the beginningpath = path[ 1 : ]path = '/' + pathif current_OS == 'Windows':search_path = path.replace('/', '\\') # os.walk uses '\' in Windowselse:search_path = pathstart_path = os.path.abspath('')# search project directory for the selectionfound = Falsefull_path = ''for root, directories, filenames in os.walk(start_path):for filename in filenames:if 0 <= root.find('.git'): # don't bother looking in this directorybreakfull_path = os.path.join(root,filename)if 0 <= full_path.find(search_path):found = Truebreakif found:breakreturn full_path, line_num, column_num# end - resolve_path## Opens the file in the preferred editor at the line & column number# If the preferred editor isn't already running then it tries the next.# If none are open then the system default is used.## Editor order:# 1. Notepad++ (Windows only)# 2. Sublime Text# 3. Atom# 4. System default (opens at line 1, column 1 only)#def open_file(path):import subprocessfile_path, line_num, column_num = resolve_path(path)if file_path == '' :returnif current_OS == 'Windows':editor_note = subprocess.check_output('wmic process where "name=' + "'notepad++.exe'" + '" get ExecutablePath')editor_sublime = subprocess.check_output('wmic process where "name=' + "'sublime_text.exe'" + '" get ExecutablePath')editor_atom = subprocess.check_output('wmic process where "name=' + "'atom.exe'" + '" get ExecutablePath')if 0 <= editor_note.find('notepad++.exe'):start = editor_note.find('\n') + 1end = editor_note.find('\n',start + 5) -4editor_note = editor_note[ start : end]command = file_path , ' -n' + str(line_num) , ' -c' + str(column_num)subprocess.Popen([editor_note, command])elif 0 <= editor_sublime.find('sublime_text.exe'):start = editor_sublime.find('\n') + 1end = editor_sublime.find('\n',start + 5) -4editor_sublime = editor_sublime[ start : end]command = file_path + ':' + line_num + ':' + column_numsubprocess.Popen([editor_sublime, command])elif 0 <= editor_atom.find('atom.exe'):start = editor_atom.find('\n') + 1end = editor_atom.find('\n',start + 5) -4editor_atom = editor_atom[ start : end]command = file_path + ':' + str(line_num) + ':' + str(column_num)subprocess.Popen([editor_atom, command])else:os.startfile(resolve_path(path)) # open file with default appelif current_OS == 'Linux':command = file_path + ':' + str(line_num) + ':' + str(column_num)index_end = command.find(',')if 0 <= index_end:command = command[ : index_end] # sometimes a comma magically appears, don't want itrunning_apps = subprocess.Popen('ps ax -o cmd', stdout=subprocess.PIPE, shell=True)(output, err) = running_apps.communicate()temp = output.split('\n')def find_editor_linux(name, search_obj):for line in search_obj:if 0 <= line.find(name):path = linereturn True, pathreturn False , ''(success_sublime, editor_path_sublime) = find_editor_linux('sublime_text',temp)(success_atom, editor_path_atom) = find_editor_linux('atom',temp)if success_sublime:subprocess.Popen([editor_path_sublime, command])elif success_atom:subprocess.Popen([editor_path_atom, command])else:os.system('xdg-open ' + file_path )elif current_OS == 'Darwin': # MACcommand = file_path + ':' + str(line_num) + ':' + str(column_num)index_end = command.find(',')if 0 <= index_end:command = command[ : index_end] # sometimes a comma magically appears, don't want itrunning_apps = subprocess.Popen('ps axwww -o command', stdout=subprocess.PIPE, shell=True)(output, err) = running_apps.communicate()temp = output.split('\n')def find_editor_mac(name, search_obj):for line in search_obj:if 0 <= line.find(name):path = lineif 0 <= path.find('-psn'):path = path[ : path.find('-psn') - 1 ]return True, pathreturn False , ''(success_sublime, editor_path_sublime) = find_editor_mac('Sublime',temp)(success_atom, editor_path_atom) = find_editor_mac('Atom',temp)if success_sublime:subprocess.Popen([editor_path_sublime, command])elif success_atom:subprocess.Popen([editor_path_atom, command])else:os.system('open ' + file_path )# end - open_file# gets the last build environmentdef get_build_last():env_last = ''DIR_PWD = os.listdir('.')if '.pioenvs' in DIR_PWD:date_last = 0.0DIR__pioenvs = os.listdir('.pioenvs')for name in DIR__pioenvs:if 0 <= name.find('.') or 0 <= name.find('-'): # skip files in listingcontinueDIR_temp = os.listdir('.pioenvs/' + name)for names_temp in DIR_temp:if 0 == names_temp.find('firmware.'):date_temp = os.path.getmtime('.pioenvs/' + name + '/' + names_temp)if date_temp > date_last:date_last = date_tempenv_last = namereturn env_last# gets the board being built from the Configuration.h file# returns: board name, major version of Marlin being used (1 or 2)def get_board_name():board_name = ''# get board namewith open('Marlin/Configuration.h', 'r') as myfile:Configuration_h = myfile.read()Configuration_h = Configuration_h.split('\n')Marlin_ver = 0 # set version to invalid numberfor lines in Configuration_h:if 0 == lines.find('#define CONFIGURATION_H_VERSION 01'):Marlin_ver = 1if 0 == lines.find('#define CONFIGURATION_H_VERSION 02'):Marlin_ver = 2board = lines.find(' BOARD_') + 1motherboard = lines.find(' MOTHERBOARD ') + 1define = lines.find('#define ')comment = lines.find('//')if (comment == -1 or comment > board) and \board > motherboard and \motherboard > define and \define >= 0 :spaces = lines.find(' ', board) # find the end of the board substringif spaces == -1:board_name = lines[board : ]else:board_name = lines[board : spaces]breakreturn board_name, Marlin_ver# extract first environment name it finds after the start position# returns: environment name and position to start the next search fromdef get_env_from_line(line, start_position):env = ''next_position = -1env_position = line.find('env:', start_position)if 0 < env_position:next_position = line.find(' ', env_position + 4)if 0 < next_position:env = line[env_position + 4 : next_position]else:env = line[env_position + 4 : ] # at the end of the linereturn env, next_position#scans pins.h for board name and returns the environment(s) it findsdef get_starting_env(board_name_full, version):# get environment starting pointif version == 1:path = 'Marlin/pins.h'if version == 2:path = 'Marlin/src/pins/pins.h'with open(path, 'r') as myfile:pins_h = myfile.read()env_A = ''env_B = ''env_C = ''board_name = board_name_full[ 6 : ] # only use the part after "BOARD_" since we're searching the pins.h filepins_h = pins_h.split('\n')environment = ''board_line = ''cpu_A = ''cpu_B = ''i = 0list_start_found = Falsefor lines in pins_h:i = i + 1 # i is always one ahead of the index into pins_hif 0 < lines.find("Unknown MOTHERBOARD value set in Configuration.h"):break # no moreif 0 < lines.find('1280'):list_start_found = Trueif list_start_found == False: # skip lines until find start of CPU listcontinueboard = lines.find(board_name)comment_start = lines.find('// ')cpu_A_loc = comment_startcpu_B_loc = 0if board > 0: # need to look at the next line for environment infocpu_line = pins_h[i]comment_start = cpu_line.find('// ')env_A, next_position = get_env_from_line(cpu_line, comment_start) # get name of environment & start of search for nextenv_B, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it existsenv_C, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it existsbreakreturn env_A, env_B, env_C# scans input string for CPUs that the users may need to select from# returns: CPU namedef get_CPU_name(environment):CPU_list = ('1280', '2560','644', '1284', 'LPC1768', 'DUE')CPU_name = ''for CPU in CPU_list:if 0 < environment.find(CPU):return CPU# get environment to be used for the build# returns: environmentdef get_env(board_name, ver_Marlin):def no_environment():print('ERROR - no environment for this board')print(board_name)raise SystemExit(0) # no environment so quitdef invalid_board():print('ERROR - invalid board')print(board_name)raise SystemExit(0) # quit if unable to find boardCPU_question = ( ('1280', '2560', " 1280 or 2560 CPU? "), ('644', '1284', " 644 or 1284 CPU? ") )if 0 < board_name.find('MELZI') :get_answer(' ' + board_name + ' ', " Which flavor of Melzi? ", "Melzi (Optiboot bootloader)", "Melzi ")if 1 == get_answer_val:target_env = 'melzi_optiboot'else:target_env = 'melzi'else:env_A, env_B, env_C = get_starting_env(board_name, ver_Marlin)if env_A == '':no_environment()if env_B == '':return env_A # only one environment so finishedCPU_A = get_CPU_name(env_A)CPU_B = get_CPU_name(env_B)for item in CPU_question:if CPU_A == item[0]:get_answer(' ' + board_name + ' ', item[2], item[0], item[1])if 2 == get_answer_val:target_env = env_Belse:target_env = env_Areturn target_envif env_A == 'LPC1768':if build_type == 'traceback' or (build_type == 'clean' and get_build_last() == 'LPC1768_debug_and_upload'):target_env = 'LPC1768_debug_and_upload'else:target_env = 'LPC1768'elif env_A == 'DUE':target_env = 'DUE'if build_type == 'traceback' or (build_type == 'clean' and get_build_last() == 'DUE_debug'):target_env = 'DUE_debug'elif env_B == 'DUE_USB':get_answer(' ' + board_name + ' ', " DUE: need download port ", "USB (native USB) port", "Programming port ")if 1 == get_answer_val:target_env = 'DUE_USB'else:target_env = 'DUE'else:invalid_board()if build_type == 'traceback' and not(target_env == 'LPC1768_debug_and_upload' or target_env == 'DUE_debug') and Marlin_ver == 2:print("ERROR - this board isn't setup for traceback")print('board_name: ', board_name)print('target_env: ', target_env)raise SystemExit(0)return target_env# end - get_env# puts screen text into queue so that the parent thread can fetch the data from this threadimport QueueIO_queue = Queue.Queue()PIO_queue = Queue.Queue()def write_to_screen_queue(text, format_tag = 'normal'):double_in = [text, format_tag]IO_queue.put(double_in, block = False)## send one line to the terminal screen with syntax highlighting## input: unformatted text, flags from previous run# returns: formatted text ready to go to the terminal, flags from this run## This routine remembers the status from call to call because previous# lines can affect how the current line is highlighted## 'static' variables - init here and then keep updating them from within print_linewarning = Falsewarning_FROM = Falseerror = Falsestandard = Trueprev_line_COM = Falsenext_line_warning = Falsewarning_continue = Falseline_counter = 0def line_print(line_input):global warningglobal warning_FROMglobal errorglobal standardglobal prev_line_COMglobal next_line_warningglobal warning_continueglobal line_counter# all '0' elements must precede all '1' elements or they'll be skippedplatformio_highlights = [['Environment', 0, 'highlight_blue'],['[SKIP]', 1, 'warning'],['[IGNORED]', 1, 'warning'],['[ERROR]', 1, 'error'],['[FAILED]', 1, 'error'],['[SUCCESS]', 1, 'highlight_green']]def write_to_screen_with_replace(text, highlights): # search for highlights & split line accordinglydid_something = Falsefor highlight in highlights:found = text.find(highlight[0])if did_something == True:breakif found >= 0 :did_something = Trueif 0 == highlight[1]:found_1 = text.find(' ')found_tab = text.find('\t')if found_1 < 0 or found_1 > found_tab:found_1 = found_tabwrite_to_screen_queue(text[ : found_1 + 1 ])for highlight_2 in highlights:if highlight[0] == highlight_2[0] :continuefound = text.find(highlight_2[0])if found >= 0 :found_space = text.find(' ', found_1 + 1)found_tab = text.find('\t', found_1 + 1)if found_space < 0 or found_space > found_tab:found_space = found_tabfound_right = text.find(']', found + 1)write_to_screen_queue(text[found_1 + 1 : found_space + 1 ], highlight[2])write_to_screen_queue(text[found_space + 1 : found + 1 ])write_to_screen_queue(text[found + 1 : found_right], highlight_2[2])write_to_screen_queue(text[found_right : ] + '\n')breakbreakif 1 == highlight[1]:found_right = text.find(']', found + 1)write_to_screen_queue(text[ : found + 1 ])write_to_screen_queue(text[found + 1 : found_right ], highlight[2])write_to_screen_queue(text[found_right : ] + '\n' + '\n')breakif did_something == False:r_loc = text.find('\r') + 1if r_loc > 0 and r_loc < len(text): # need to split this linetext = text.split('\r')for line in text:if not(line == ""):write_to_screen_queue(line + '\n')else:write_to_screen_queue(text + '\n')# end - write_to_screen_with_replace# scan the lineline_counter = line_counter + 1max_search = len(line_input)if max_search > 3 :max_search = 3beginning = line_input[:max_search]# set flagsif 0 < line_input.find(': warning: '): # start of warning blockwarning = Truewarning_FROM = Falseerror = Falsestandard = Falseprev_line_COM = Falseprev_line_COM = Falsewarning_continue = Trueif 0 < line_input.find('Thank you') or 0 < line_input.find('SUMMARY') :warning = False #standard line foundwarning_FROM = Falseerror = Falsestandard = Trueprev_line_COM = Falsewarning_continue = Falseelif beginning == 'War' or \beginning == '#er' or \beginning == 'In ' or \(beginning != 'Com' and prev_line_COM == True and not(beginning == 'Arc' or beginning == 'Lin' or beginning == 'Ind') or \next_line_warning == True):warning = True #warning foundwarning_FROM = Falseerror = Falsestandard = Falseprev_line_COM = Falseelif beginning == 'Com' or \beginning == 'Ver' or \beginning == ' [E' or \beginning == 'Rem' or \beginning == 'Bui' or \beginning == 'Ind' or \beginning == 'PLA':warning = False #standard line foundwarning_FROM = Falseerror = Falsestandard = Trueprev_line_COM = Falsewarning_continue = Falseelif beginning == '***':warning = False # error foundwarning_FROM = Falseerror = Truestandard = Falseprev_line_COM = Falseelif 0 < line_input.find(': error:') or \0 < line_input.find(': fatal error:'): # start of warning /error blockwarning = False # error foundwarning_FROM = Falseerror = Truestandard = Falseprev_line_COM = Falsewarning_continue = Trueelif beginning == 'fro' and warning == True or \beginning == '.pi' : # start of warning /error blockwarning_FROM = Trueprev_line_COM = Falsewarning_continue = Trueelif warning_continue == True:warning = Truewarning_FROM = False # keep the warning status going until find a standard line or an errorerror = Falsestandard = Falseprev_line_COM = Falsewarning_continue = Trueelse:warning = False # unknown so assume standard linewarning_FROM = Falseerror = Falsestandard = Trueprev_line_COM = Falsewarning_continue = Falseif beginning == 'Com':prev_line_COM = True# print based on flagsif standard == True:write_to_screen_with_replace(line_input, platformio_highlights) #print white on black with substitutionsif warning == True:write_to_screen_queue(line_input + '\n', 'warning')if error == True:write_to_screen_queue(line_input + '\n', 'error')# end - line_printdef run_PIO(dummy):########################################################################### ## run Platformio ## ############################################################################ build platformio run -e target_env# clean platformio run --target clean -e target_env# upload platformio run --target upload -e target_env# traceback platformio run --target upload -e target_env# program platformio run --target program -e target_env# test platformio test upload -e target_env# remote platformio remote run --target upload -e target_env# debug platformio debug -e target_envglobal build_typeglobal target_envglobal board_nameprint('build_type: ', build_type)import subprocessimport sysprint('starting platformio')if build_type == 'build':# platformio run -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'run', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'clean':# platformio run --target clean -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'clean', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'upload':# platformio run --target upload -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'traceback':# platformio run --target upload -e target_env - select the debug environment if there is one# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'program':# platformio run --target program -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'run', '--target', 'program', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'test':#platformio test upload -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'test', 'upload', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'remote':# platformio remote run --target upload -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'remote', 'run', '--target', 'program', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)elif build_type == 'debug':# platformio debug -e target_env# combine stdout & stderr so all compile messages are includedpio_subprocess = subprocess.Popen(['platformio', 'debug', '-e', target_env], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)else:print('ERROR - unknown build type: ', build_type)raise SystemExit(0) # kill everything# stream output from subprocess and split it into linesfor line in iter(pio_subprocess.stdout.readline, ''):line_print(line.replace('\n', ''))# append info used to run PlatformIOwrite_to_screen_queue('\nBoard name: ' + board_name + '\n') # put build info at the bottom of the screenwrite_to_screen_queue('Build type: ' + build_type + '\n')write_to_screen_queue('Environment used: ' + target_env + '\n')write_to_screen_queue(str(datetime.now()) + '\n')# end - run_PIO########################################################################import timeimport threadingimport Tkinter as tkimport ttkimport Queueimport subprocessimport sysque = Queue.Queue()#IO_queue = Queue.Queue()from Tkinter import Tk, Frame, Text, Scrollbar, Menufrom tkMessageBox import askokcancelimport tkFileDialogfrom tkMessageBox import askokcancelimport tkFileDialogclass output_window(Text):# based on Super Textglobal continue_updatescontinue_updates = Trueglobal search_positionsearch_position = '' # start with invalid search positionglobal error_founderror_found = False # are there any errors?def __init__(self):self.root = tk.Tk()self.frame = tk.Frame(self.root)self.frame.pack(fill='both', expand=True)# text widget#self.text = tk.Text(self.frame, borderwidth=3, relief="sunken")Text.__init__(self, self.frame, borderwidth=3, relief="sunken")self.config(tabs=(400,)) # configure Text widget tab stopsself.config(background = 'black', foreground = 'white', font= ("consolas", 12), wrap = 'word', undo = 'True')#self.config(background = 'black', foreground = 'white', font= ("consolas", 12), wrap = 'none', undo = 'True')self.config(height = 24, width = 100)self.config(insertbackground = 'pale green') # keyboard insertion pointself.pack(side='left', fill='both', expand=True)self.tag_config('normal', foreground = 'white')self.tag_config('warning', foreground = 'yellow' )self.tag_config('error', foreground = 'red')self.tag_config('highlight_green', foreground = 'green')self.tag_config('highlight_blue', foreground = 'cyan')self.tag_config('error_highlight_inactive', background = 'dim gray')self.tag_config('error_highlight_active', background = 'light grey')self.bind_class("Text","<Control-a>", self.select_all) # required in windows, works in othersself.bind_all("<Control-Shift-E>", self.scroll_errors)self.bind_class("<Control-Shift-R>", self.rebuild)# scrollbarscrb = tk.Scrollbar(self.frame, orient='vertical', command=self.yview)self.config(yscrollcommand=scrb.set)scrb.pack(side='right', fill='y')#self.scrb_Y = tk.Scrollbar(self.frame, orient='vertical', command=self.yview)#self.scrb_Y.config(yscrollcommand=self.scrb_Y.set)#self.scrb_Y.pack(side='right', fill='y')#self.scrb_X = tk.Scrollbar(self.frame, orient='horizontal', command=self.xview)#self.scrb_X.config(xscrollcommand=self.scrb_X.set)#self.scrb_X.pack(side='bottom', fill='x')#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#self.config(xscrollcommand=scrb_X.set)#scrb_X.pack(side='bottom', fill='x')#scrb= tk.Scrollbar(self, orient='vertical', command=self.yview)#self.config(yscrollcommand=scrb.set)#scrb.pack(side='right', fill='y')#self.config(height = 240, width = 1000) # didn't get the size baCK TO NORMAL#self.pack(side='left', fill='both', expand=True) # didn't get the size baCK TO NORMAL# pop-up menuself.popup = tk.Menu(self, tearoff=0)self.popup.add_command(label='Copy', command=self._copy)self.popup.add_command(label='Paste', command=self._paste)self.popup.add_separator()self.popup.add_command(label='Cut', command=self._cut)self.popup.add_separator()self.popup.add_command(label='Select All', command=self._select_all)self.popup.add_command(label='Clear All', command=self._clear_all)self.popup.add_separator()self.popup.add_command(label='Save As', command=self._file_save_as)self.popup.add_separator()#self.popup.add_command(label='Repeat Build(CTL-shift-r)', command=self._rebuild)self.popup.add_command(label='Repeat Build', command=self._rebuild)self.popup.add_separator()self.popup.add_command(label='Scroll Errors (CTL-shift-e)', command=self._scroll_errors)self.popup.add_separator()self.popup.add_command(label='Open File at Cursor', command=self._open_selected_file)if current_OS == 'Darwin': # MACself.bind('<Button-2>', self._show_popup) # macOS onlyelse:self.bind('<Button-3>', self._show_popup) # Windows & Linux# threading & subprocess sectiondef start_thread(self, ):global continue_updates# create then start a secondary thread to run an arbitrary function# must have at least one argumentself.secondary_thread = threading.Thread(target = lambda q, arg1: q.put(run_PIO(arg1)), args=(que, ''))self.secondary_thread.start()continue_updates = True# check the Queue in 50msself.root.after(50, self.check_thread)self.root.after(50, self.update)def check_thread(self): # wait for user to kill the windowglobal continue_updatesif continue_updates == True:self.root.after(10, self.check_thread)def update(self):global continue_updatesif continue_updates == True:self.root.after(10, self.update)#method is called every 50mstemp_text = ['0','0']if IO_queue.empty():if not(self.secondary_thread.is_alive()):continue_updates = False # queue is exhausted and thread is dead so no need for further updateselse:try:temp_text = IO_queue.get(block = False)except Queue.Empty:continue_updates = False # queue is exhausted so no need for further updateselse:self.insert('end', temp_text[0], temp_text[1])self.see("end") # make the last line visible (scroll text off the top)# text editing sectiondef _scroll_errors(self):global search_positionglobal error_foundif search_position == '': # first time so highlight all errorscountVar = tk.IntVar()search_position = '1.0'search_count = 0while not(search_position == '') and search_count < 100:search_position = self.search("error", search_position, stopindex="end", count=countVar, nocase=1)search_count = search_count + 1if not(search_position == ''):error_found = Trueend_pos = '{}+{}c'.format(search_position, 5)self.tag_add("error_highlight_inactive", search_position, end_pos)search_position = '{}+{}c'.format(search_position, 1) # point to the next character for new searchelse:breakif error_found:if search_position == '':search_position = self.search("error", '1.0', stopindex="end", nocase=1) # new searchelse: # remove active highlightend_pos = '{}+{}c'.format(search_position, 5)start_pos = '{}+{}c'.format(search_position, -1)self.tag_remove("error_highlight_active", start_pos, end_pos)search_position = self.search("error", search_position, stopindex="end", nocase=1) # finds first occurrence AGAIN on the first time throughif search_position == "": # wrap aroundsearch_position = self.search("error", '1.0', stopindex="end", nocase=1)end_pos = '{}+{}c'.format(search_position, 5)self.tag_add("error_highlight_active", search_position, end_pos) # add active highlightself.see(search_position)search_position = '{}+{}c'.format(search_position, 1) # point to the next character for new searchdef scroll_errors(self, event):self._scroll_errors()def _rebuild(self):#global board_name#global Marlin_ver#global target_env#board_name, Marlin_ver = get_board_name()#target_env = get_env(board_name, Marlin_ver)self.start_thread()def rebuild(self, event):print("event happened")self._rebuild()def _open_selected_file(self):current_line = self.index('insert')line_start = current_line[ : current_line.find('.')] + '.0'line_end = current_line[ : current_line.find('.')] + '.200'self.mark_set("path_start", line_start)self.mark_set("path_end", line_end)path = self.get("path_start", "path_end")from_loc = path.find('from ')colon_loc = path.find(': ')if 0 <= from_loc and ((colon_loc == -1) or (from_loc < colon_loc)) :path = path [ from_loc + 5 : ]if 0 <= colon_loc:path = path [ : colon_loc ]if 0 <= path.find('\\') or 0 <= path.find('/'): # make sure it really contains a pathopen_file(path)def _file_save_as(self):self.filename = tkFileDialog.asksaveasfilename(defaultextension = '.txt')f = open(self.filename, 'w')f.write(self.get('1.0', 'end'))f.close()def copy(self, event):try:selection = self.get(*self.tag_ranges('sel'))self.clipboard_clear()self.clipboard_append(selection)except TypeError:passdef cut(self, event):try:selection = self.get(*self.tag_ranges('sel'))self.clipboard_clear()self.clipboard_append(selection)self.delete(*self.tag_ranges('sel'))except TypeError:passdef _show_popup(self, event):'''right-click popup menu'''if self.root.focus_get() != self:self.root.focus_set()try:self.popup.tk_popup(event.x_root, event.y_root, 0)finally:self.popup.grab_release()def _cut(self):try:selection = self.get(*self.tag_ranges('sel'))self.clipboard_clear()self.clipboard_append(selection)self.delete(*self.tag_ranges('sel'))except TypeError:passdef cut(self, event):self._cut()def _copy(self):try:selection = self.get(*self.tag_ranges('sel'))self.clipboard_clear()self.clipboard_append(selection)except TypeError:passdef copy(self, event):self._copy()def _paste(self):self.insert('insert', self.selection_get(selection='CLIPBOARD'))def _select_all(self):self.tag_add('sel', '1.0', 'end')def select_all(self, event):self.tag_add('sel', '1.0', 'end')def _clear_all(self):#'''erases all text'''##isok = askokcancel('Clear All', 'Erase all text?', frame=self,# default='ok')#if isok:# self.delete('1.0', 'end')self.delete('1.0', 'end')# end - output_windowdef main():########################################################################### ## main program ## ###########################################################################global build_typeglobal target_envglobal board_nameboard_name, Marlin_ver = get_board_name()target_env = get_env(board_name, Marlin_ver)os.environ["BUILD_TYPE"] = build_type # let sub processes know what is happeningos.environ["TARGET_ENV"] = target_envos.environ["BOARD_NAME"] = board_nameauto_build = output_window()auto_build.start_thread() # executes the "run_PIO" functionauto_build.root.mainloop()if __name__ == '__main__':main()