Subversion Repositories MK-Marlin

Rev

Details | Last modification | View Log | RSS feed

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