aboutsummaryrefslogtreecommitdiff
path: root/buildroot/share/vscode/auto_build.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildroot/share/vscode/auto_build.py')
-rw-r--r--buildroot/share/vscode/auto_build.py1303
1 files changed, 1303 insertions, 0 deletions
diff --git a/buildroot/share/vscode/auto_build.py b/buildroot/share/vscode/auto_build.py
new file mode 100644
index 0000000..5dd2d0d
--- /dev/null
+++ b/buildroot/share/vscode/auto_build.py
@@ -0,0 +1,1303 @@
+#!/usr/bin/env python
+#######################################
+#
+# Marlin 3D Printer Firmware
+# Copyright (c) 2020 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 <https://www.gnu.org/licenses/>.
+#
+#######################################
+
+#######################################
+#
+# Revision: 2.1.0
+#
+# 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_function
+from __future__ import division
+
+import sys
+import os
+
+pwd = os.getcwd() # make sure we're executing from the correct directory level
+pwd = 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 3
+
+print("python version " + str(sys.version_info[0]) + "." + str(sys.version_info[1]) + "." + str(sys.version_info[2]))
+
+import platform
+current_OS = platform.system()
+
+#globals
+target_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 tk
+ else:
+ import tkinter as tk
+
+ def CPU_exit_3(): # forward declare functions
+ CPU_exit_3_()
+
+ def got_answer():
+ got_answer_()
+
+ def kill_session():
+ kill_session_()
+
+ root_get_answer = tk.Tk()
+ root_get_answer.title('')
+ #root_get_answer.withdraw()
+ #root_get_answer.deiconify()
+ root_get_answer.attributes("-topmost", True)
+
+ def disable_event():
+ pass
+
+ root_get_answer.protocol("WM_DELETE_WINDOW", disable_event)
+ root_get_answer.resizable(False, False)
+
+ root_get_answer.radio_state = 1 # declare variables used by TK and enable
+
+ global get_answer_val
+ get_answer_val = 2 # return get_answer_val, set default to match radio_state default
+
+ radio_state = tk.IntVar()
+ radio_state.set(get_answer_val)
+
+ l1 = tk.Label(text=board_name, fg="light green", bg="dark green",
+ font="default 14 bold").grid(row=0, columnspan=2, sticky='EW', ipadx=2, ipady=2)
+
+ l2 = tk.Label(text=cpu_label_txt).grid(row=1, pady=4, columnspan=2, sticky='EW')
+
+ b4 = tk.Radiobutton(
+ text=cpu_a_txt,
+ fg="black",
+ bg="lightgray",
+ relief=tk.SUNKEN,
+ selectcolor="green",
+ variable=radio_state,
+ value=1,
+ indicatoron=0,
+ command=CPU_exit_3
+ ).grid(row=2, pady=1, ipady=2, ipadx=10, columnspan=2)
+
+ b5 = tk.Radiobutton(
+ text=cpu_b_txt,
+ fg="black",
+ bg="lightgray",
+ relief=tk.SUNKEN,
+ selectcolor="green",
+ variable=radio_state,
+ value=2,
+ indicatoron=0,
+ command=CPU_exit_3
+ ).grid(row=3, pady=1, ipady=2, ipadx=10, columnspan=2) # use same variable but inverted so they will track
+
+ b6 = tk.Button(text="Cancel", fg="red", command=kill_session).grid(row=4, column=0, padx=4, pady=4, ipadx=2, ipady=2)
+
+ b7 = tk.Button(text="Continue", fg="green", command=got_answer).grid(row=4, column=1, padx=4, pady=4, ipadx=2, ipady=2)
+
+ def got_answer_():
+ root_get_answer.destroy()
+
+ def CPU_exit_3_():
+ global get_answer_val
+ get_answer_val = radio_state.get()
+
+ def kill_session_():
+ raise SystemExit(0) # kill everything
+
+ root_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 path
+
+ if 0 <= path.find('"'):
+ path = path[path.find('"'):]
+ if 0 <= path.find(', line '):
+ path = path.replace(', line ', ':')
+ path = path.replace('"', '')
+
+ # get line and column numbers
+ line_num = 1
+ column_num = 1
+ line_start = path.find(':', 2) # use 2 here so don't eat Windows full path
+ column_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 = 1
+ if column_start != column_end:
+ column_num = path[column_start + 1:column_end]
+ if column_num == '':
+ column_num = 0
+
+ index_end = path.find(',')
+ if 0 <= index_end:
+ path = path[:index_end] # delete comma and anything after
+ index_end = path.find(':', 2)
+ if 0 <= index_end:
+ path = path[:path.find(':', 2)] # delete the line number and anything after
+
+ path = path.replace('\\', '/')
+
+ if 1 == path.find(':') and current_OS == 'Windows':
+ return path, line_num, column_num # found a full path - no need for further processing
+ elif 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 processing
+
+ else:
+
+ # resolve as many '../' as we can
+ while 0 <= path.find('../'):
+ end = path.find('../') - 1
+ start = path.find('/')
+ while 0 <= path.find('/', start) and end > path.find('/', start):
+ start = path.find('/', start) + 1
+ path = 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 start != 0: # make sure path starts with '/'
+ while 0 == path.find(' '): # eat any spaces at the beginning
+ path = path[1:]
+ path = '/' + path
+
+ if current_OS == 'Windows':
+ search_path = path.replace('/', '\\') # os.walk uses '\' in Windows
+ else:
+ search_path = path
+
+ start_path = os.path.abspath('')
+
+ # search project directory for the selection
+ found = False
+ full_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 directory
+ break
+ full_path = os.path.join(root, filename)
+ if 0 <= full_path.find(search_path):
+ found = True
+ break
+ if found:
+ break
+
+ return full_path, line_num, column_num
+
+# end - resolve_path
+
+
+#
+# Open 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 subprocess
+ file_path, line_num, column_num = resolve_path(path)
+
+ if file_path == '':
+ return
+
+ if 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') + 1
+ end = editor_note.find('\n', start + 5) - 4
+ editor_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') + 1
+ end = editor_sublime.find('\n', start + 5) - 4
+ editor_sublime = editor_sublime[start:end]
+ command = file_path + ':' + line_num + ':' + column_num
+ subprocess.Popen([editor_sublime, command])
+
+ elif 0 <= editor_atom.find('atom.exe'):
+ start = editor_atom.find('\n') + 1
+ end = editor_atom.find('\n', start + 5) - 4
+ editor_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 app
+
+ elif 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 it
+ running_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 = line
+ return True, path
+ return 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': # MAC
+
+ 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 it
+ running_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 = line
+ if 0 <= path.find('-psn'):
+ path = path[:path.find('-psn') - 1]
+ return True, path
+ return 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
+
+
+# Get the last build environment
+def get_build_last():
+ env_last = ''
+ DIR_PWD = os.listdir('.')
+ if '.pio' in DIR_PWD:
+ date_last = 0.0
+ DIR__pioenvs = os.listdir('.pio')
+ for name in DIR__pioenvs:
+ if 0 <= name.find('.') or 0 <= name.find('-'): # skip files in listing
+ continue
+ DIR_temp = os.listdir('.pio/build/' + name)
+ for names_temp in DIR_temp:
+
+ if 0 == names_temp.find('firmware.'):
+ date_temp = os.path.getmtime('.pio/build/' + name + '/' + names_temp)
+ if date_temp > date_last:
+ date_last = date_temp
+ env_last = name
+ return env_last
+
+
+# Get the board being built from the Configuration.h file
+# return: board name, major version of Marlin being used (1 or 2)
+def get_board_name():
+ board_name = ''
+ # get board name
+
+ with open('Marlin/Configuration.h', 'r') as myfile:
+ Configuration_h = myfile.read()
+
+ Configuration_h = Configuration_h.split('\n')
+ Marlin_ver = 0 # set version to invalid number
+ for lines in Configuration_h:
+ if 0 == lines.find('#define CONFIGURATION_H_VERSION 01'):
+ Marlin_ver = 1
+ if 0 == lines.find('#define CONFIGURATION_H_VERSION 02'):
+ Marlin_ver = 2
+ board = lines.find(' BOARD_') + 1
+ motherboard = lines.find(' MOTHERBOARD ') + 1
+ define = 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 substring
+ if spaces == -1:
+ board_name = lines[board:]
+ else:
+ board_name = lines[board:spaces]
+ break
+
+ return board_name, Marlin_ver
+
+
+# extract first environment name found after the start position
+# return: environment name and position to start the next search from
+def get_env_from_line(line, start_position):
+ env = ''
+ next_position = -1
+ env_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 line
+ return env, next_position
+
+
+# scan pins.h for board name and return the environment(s) found
+def get_starting_env(board_name_full, version):
+ # get environment starting point
+
+ if 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 file
+ pins_h = pins_h.split('\n')
+ environment = ''
+ board_line = ''
+ cpu_A = ''
+ cpu_B = ''
+ i = 0
+ list_start_found = False
+ for lines in pins_h:
+ i = i + 1 # i is always one ahead of the index into pins_h
+ if 0 < lines.find("Unknown MOTHERBOARD value set in Configuration.h"):
+ break # no more
+ if 0 < lines.find('1280'):
+ list_start_found = True
+ if list_start_found == False: # skip lines until find start of CPU list
+ continue
+ board = lines.find(board_name)
+ comment_start = lines.find('// ')
+ cpu_A_loc = comment_start
+ cpu_B_loc = 0
+ if board > 0: # need to look at the next line for environment info
+ cpu_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 next
+ env_B, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it exists
+ env_C, next_position = get_env_from_line(cpu_line, next_position) # get next environment, if it exists
+ break
+ return env_A, env_B, env_C
+
+
+# Scan input string for CPUs that users may need to select from
+# return: CPU name
+def 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
+# return: environment
+def 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 quit
+
+ def invalid_board():
+ print('ERROR - invalid board')
+ print(board_name)
+ raise SystemExit(0) # quit if unable to find board
+
+ CPU_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 finished
+
+ CPU_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_B
+ else:
+ target_env = env_A
+ return target_env
+
+ if 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 Download Port?', '(Native) USB port', 'Programming port')
+ if 1 == get_answer_val:
+ target_env = 'DUE_USB'
+ else:
+ target_env = 'DUE'
+ elif env_A == 'STM32F103RC_btt' or env_A == 'STM32F103RE_btt':
+ if env_A == 'STM32F103RE_btt':
+ get_answer(board_name, 'MCU Type?', 'STM32F103RC', 'STM32F103RE')
+ if 1 == get_answer_val:
+ env_A = 'STM32F103RC_btt'
+ target_env = env_A
+ if env_A == 'STM32F103RC_btt':
+ get_answer(board_name, 'RCT6 Flash Size?', '512K', '256K')
+ if 1 == get_answer_val:
+ target_env += '_512K'
+ get_answer(board_name, 'USB Support?', 'USB', 'No USB')
+ if 1 == get_answer_val:
+ target_env += '_USB'
+ else:
+ invalid_board()
+
+ if build_type == 'traceback' and target_env != 'LPC1768_debug_and_upload' and 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 thread
+if python_ver == 2:
+ import Queue as queue
+else:
+ import queue as queue
+IO_queue = queue.Queue()
+
+
+#PIO_queue = queue.Queue() not used!
+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
+# return: 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_line
+warning = False
+warning_FROM = False
+error = False
+standard = True
+prev_line_COM = False
+next_line_warning = False
+warning_continue = False
+line_counter = 0
+
+
+def line_print(line_input):
+
+ global warning
+ global warning_FROM
+ global error
+ global standard
+ global prev_line_COM
+ global next_line_warning
+ global warning_continue
+ global line_counter
+
+ # all '0' elements must precede all '1' elements or they'll be skipped
+ platformio_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 accordingly
+ did_something = False
+ for highlight in highlights:
+ found = text.find(highlight[0])
+ if did_something == True:
+ break
+ if found >= 0:
+ did_something = True
+ if 0 == highlight[1]:
+ found_1 = text.find(' ')
+ found_tab = text.find('\t')
+ if found_1 < 0 or found_1 > found_tab:
+ found_1 = found_tab
+ write_to_screen_queue(text[:found_1 + 1])
+ for highlight_2 in highlights:
+ if highlight[0] == highlight_2[0]:
+ continue
+ found = 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_tab
+ found_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')
+ break
+ break
+ if 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')
+ break
+ if did_something == False:
+ r_loc = text.find('\r') + 1
+ if r_loc > 0 and r_loc < len(text): # need to split this line
+ text = text.split('\r')
+ for line in text:
+ if line != '':
+ write_to_screen_queue(line + '\n')
+ else:
+ write_to_screen_queue(text + '\n')
+
+ # end - write_to_screen_with_replace
+
+# scan the line
+ line_counter = line_counter + 1
+ max_search = len(line_input)
+ if max_search > 3:
+ max_search = 3
+ beginning = line_input[:max_search]
+
+ # set flags
+ if 0 < line_input.find(': warning: '): # start of warning block
+ warning = True
+ warning_FROM = False
+ error = False
+ standard = False
+ prev_line_COM = False
+ prev_line_COM = False
+ warning_continue = True
+ if 0 < line_input.find('Thank you') or 0 < line_input.find('SUMMARY'):
+ warning = False #standard line found
+ warning_FROM = False
+ error = False
+ standard = True
+ prev_line_COM = False
+ warning_continue = False
+ elif 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 found
+ warning_FROM = False
+ error = False
+ standard = False
+ prev_line_COM = False
+ elif beginning == 'Com' or \
+ beginning == 'Ver' or \
+ beginning == ' [E' or \
+ beginning == 'Rem' or \
+ beginning == 'Bui' or \
+ beginning == 'Ind' or \
+ beginning == 'PLA':
+ warning = False #standard line found
+ warning_FROM = False
+ error = False
+ standard = True
+ prev_line_COM = False
+ warning_continue = False
+ elif beginning == '***':
+ warning = False # error found
+ warning_FROM = False
+ error = True
+ standard = False
+ prev_line_COM = False
+ elif 0 < line_input.find(': error:') or \
+ 0 < line_input.find(': fatal error:'): # start of warning /error block
+ warning = False # error found
+ warning_FROM = False
+ error = True
+ standard = False
+ prev_line_COM = False
+ warning_continue = True
+ elif beginning == 'fro' and warning == True or \
+ beginning == '.pi' : # start of warning /error block
+ warning_FROM = True
+ prev_line_COM = False
+ warning_continue = True
+ elif warning_continue == True:
+ warning = True
+ warning_FROM = False # keep the warning status going until find a standard line or an error
+ error = False
+ standard = False
+ prev_line_COM = False
+ warning_continue = True
+
+ else:
+ warning = False # unknown so assume standard line
+ warning_FROM = False
+ error = False
+ standard = True
+ prev_line_COM = False
+ warning_continue = False
+
+ if beginning == 'Com':
+ prev_line_COM = True
+
+# print based on flags
+ if standard == True:
+ write_to_screen_with_replace(line_input, platformio_highlights) #print white on black with substitutions
+ if warning == True:
+ write_to_screen_queue(line_input + '\n', 'warning')
+ if error == True:
+ write_to_screen_queue(line_input + '\n', 'error')
+
+# end - line_print
+
+
+##########################################################################
+# #
+# 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_env
+
+
+def sys_PIO():
+
+ ##########################################################################
+ # #
+ # run Platformio inside the same shell as this Python script #
+ # #
+ ##########################################################################
+
+ global build_type
+ global target_env
+
+ import os
+
+ print('build_type: ', build_type)
+ print('starting platformio')
+
+ if build_type == 'build':
+ # pio_result = os.system("echo -en '\033c'")
+ pio_result = os.system('platformio run -e ' + target_env)
+ elif build_type == 'clean':
+ pio_result = os.system('platformio run --target clean -e ' + target_env)
+ elif build_type == 'upload':
+ pio_result = os.system('platformio run --target upload -e ' + target_env)
+ elif build_type == 'traceback':
+ pio_result = os.system('platformio run --target upload -e ' + target_env)
+ elif build_type == 'program':
+ pio_result = os.system('platformio run --target program -e ' + target_env)
+ elif build_type == 'test':
+ pio_result = os.system('platformio test upload -e ' + target_env)
+ elif build_type == 'remote':
+ pio_result = os.system('platformio remote run --target program -e ' + target_env)
+ elif build_type == 'debug':
+ pio_result = os.system('platformio debug -e ' + target_env)
+ else:
+ print('ERROR - unknown build type: ', build_type)
+ raise SystemExit(0) # kill everything
+
+ # stream output from subprocess and split it into lines
+ #for line in iter(pio_subprocess.stdout.readline, ''):
+ # line_print(line.replace('\n', ''))
+
+ # append info used to run PlatformIO
+ # write_to_screen_queue('\nBoard name: ' + board_name + '\n') # put build info at the bottom of the screen
+ # write_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 - sys_PIO
+
+
+def run_PIO(dummy):
+
+ global build_type
+ global target_env
+ global board_name
+ print('build_type: ', build_type)
+
+ import subprocess
+ import sys
+
+ print('starting platformio')
+
+ if build_type == 'build':
+ # platformio run -e target_env
+ # combine stdout & stderr so all compile messages are included
+ pio_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 included
+ pio_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 included
+ pio_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 included
+ pio_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 included
+ pio_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 included
+ pio_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 included
+ pio_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 included
+ pio_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 lines
+ if python_ver == 2:
+ for line in iter(pio_subprocess.stdout.readline, ''):
+ line_print(line.replace('\n', ''))
+ else:
+ for line in iter(pio_subprocess.stdout.readline, b''):
+ line = line.decode('utf-8')
+ line_print(line.replace('\n', ''))
+
+# append info used to run PlatformIO
+ write_to_screen_queue('\nBoard name: ' + board_name + '\n') # put build info at the bottom of the screen
+ write_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 time
+import threading
+if python_ver == 2:
+ import Tkinter as tk
+ import Queue as queue
+ import ttk
+ from Tkinter import Tk, Frame, Text, Scrollbar, Menu
+ #from tkMessageBox import askokcancel this is not used: removed
+ import tkFileDialog as fileDialog
+else:
+ import tkinter as tk
+ import queue as queue
+ from tkinter import ttk, Tk, Frame, Text, Menu
+import subprocess
+import sys
+que = queue.Queue()
+#IO_queue = queue.Queue()
+
+
+class output_window(Text):
+ # based on Super Text
+ global continue_updates
+ continue_updates = True
+
+ global search_position
+ search_position = '' # start with invalid search position
+
+ global error_found
+ error_found = False # are there any errors?
+
+ def __init__(self):
+
+ self.root = tk.Tk()
+ self.root.attributes("-topmost", True)
+ 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 stops
+ self.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 point
+ self.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 others
+ self.bind_all("<Control-Shift-E>", self.scroll_errors)
+ self.bind_class("<Control-Shift-R>", self.rebuild)
+
+ # scrollbar
+
+ scrb = 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 menu
+ self.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': # MAC
+ self.bind('<Button-2>', self._show_popup) # macOS only
+ else:
+ self.bind('<Button-3>', self._show_popup) # Windows & Linux
+
+# threading & subprocess section
+
+ def start_thread(self, ):
+ global continue_updates
+ # create then start a secondary thread to run an arbitrary function
+ # must have at least one argument
+ self.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 50ms
+ self.root.after(50, self.check_thread)
+ self.root.after(50, self.update)
+
+ def check_thread(self): # wait for user to kill the window
+ global continue_updates
+ if continue_updates == True:
+ self.root.after(10, self.check_thread)
+
+ def update(self):
+ global continue_updates
+ if continue_updates == True:
+ self.root.after(10, self.update) #method is called every 50ms
+ temp_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 updates
+ else:
+ try:
+ temp_text = IO_queue.get(block=False)
+ except Queue.Empty:
+ continue_updates = False # queue is exhausted so no need for further updates
+ else:
+ self.insert('end', temp_text[0], temp_text[1])
+ self.see("end") # make the last line visible (scroll text off the top)
+
+# text editing section
+
+ def _scroll_errors(self):
+ global search_position
+ global error_found
+ if search_position == '': # first time so highlight all errors
+ countVar = tk.IntVar()
+ search_position = '1.0'
+ search_count = 0
+ while search_position != '' and search_count < 100:
+ search_position = self.search("error", search_position, stopindex="end", count=countVar, nocase=1)
+ search_count = search_count + 1
+ if search_position != '':
+ error_found = True
+ end_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 search
+ else:
+ break
+
+ if error_found:
+ if search_position == '':
+ search_position = self.search("error", '1.0', stopindex="end", nocase=1) # new search
+ else: # remove active highlight
+ end_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 through
+ if search_position == "": # wrap around
+ search_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 highlight
+ self.see(search_position)
+ search_position = '{}+{}c'.format(search_position, 1) # point to the next character for new search
+
+ def 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 path
+ open_file(path)
+
+ def _file_save_as(self):
+ self.filename = fileDialog.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:
+ pass
+
+ def 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:
+ pass
+
+ def _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:
+ pass
+
+ def 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:
+ pass
+
+ def 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_window
+
+
+def main():
+
+ ##########################################################################
+ # #
+ # main program #
+ # #
+ ##########################################################################
+
+ global build_type
+ global target_env
+ global board_name
+
+ board_name, Marlin_ver = get_board_name()
+
+ target_env = get_env(board_name, Marlin_ver)
+
+ # Re-use the VSCode terminal, if possible
+ if os.environ.get('PLATFORMIO_CALLER', '') == 'vscode':
+ sys_PIO()
+ else:
+ auto_build = output_window()
+ auto_build.start_thread() # executes the "run_PIO" function
+
+ auto_build.root.mainloop()
+
+
+if __name__ == '__main__':
+
+ main()