• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

INTI-CMNB / KiAuto / 6981974383

24 Nov 2023 02:31PM UTC coverage: 88.592%. First build
6981974383

push

github

set-soft
Forced test

3021 of 3410 relevant lines covered (88.59%)

3.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.66
/src/pcbnew_do
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
# Copyright (c) 2020-2023 Salvador E. Tropea
4
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
5
# Copyright (c) 2019 Jesse Vincent (@obra)
6
# Copyright (c) 2018-2019 Seppe Stas (@seppestas) (Productize SPRL)
7
# Based on ideas by: Scott Bezek (@scottbez1)
8
# License: Apache 2.0
9
# Project: KiAuto (formerly kicad-automation-scripts)
10
# Adapted from: https://github.com/obra/kicad-automation-scripts
11
"""
6✔
12
Various pcbnew operations
13

14
This program runs pcbnew and can:
15
1) Print PCB layers
16
2) Run the DRC
17
The process is graphical and very delicated.
18
"""
19

20
import argparse
6✔
21
import atexit
6✔
22
import gettext
6✔
23
import json
6✔
24
import os
6✔
25
import re
6✔
26
import shutil
6✔
27
import subprocess
6✔
28
import sys
6✔
29
from tempfile import TemporaryDirectory
6✔
30
from time import (asctime, localtime, sleep)
6✔
31
import time
6✔
32

33
# Look for the 'kiauto' module from where the script is running
34
script_dir = os.path.dirname(os.path.abspath(__file__))
6✔
35
sys.path.insert(0, os.path.dirname(script_dir))
6✔
36
# Utils import
37
# Log functionality first
38
from kiauto import log
6✔
39
log.set_domain(os.path.splitext(os.path.basename(__file__))[0])
6✔
40
logger = None
6✔
41

42
from kiauto.file_util import (load_filters, wait_for_file_created_by_process, apply_filters, list_errors, list_warnings,
6✔
43
                              check_kicad_config_dir, restore_config, backup_config, check_lib_table, create_user_hotkeys,
44
                              check_input_file, memorize_project, restore_project, get_log_files, create_kicad_config,
45
                              set_time_out_scale as set_time_out_scale_f)
46
from kiauto.misc import (REC_W, REC_H, __version__, NO_PCB, PCBNEW_CFG_PRESENT, WAIT_START, WRONG_LAYER_NAME,
6✔
47
                         WRONG_PCB_NAME, PCBNEW_ERROR, WRONG_ARGUMENTS, Config, USER_HOTKEYS_PRESENT, RULES_KEY,
48
                         CORRUPTED_PCB, __copyright__, __license__, TIME_OUT_MULT, get_en_locale, KICAD_CFG_PRESENT,
49
                         MISSING_TOOL, KICAD_VERSION_6_99)
50
from kiauto.interposer import (check_interposer, dump_interposer_dialog, start_queue, setup_interposer_filename,
6✔
51
                               create_interposer_print_options_file, wait_queue, wait_start_by_msg, wait_and_show_progress,
52
                               set_kicad_process, open_dialog_i, wait_kicad_ready_i, paste_bogus_filename, paste_text_i,
53
                               paste_output_file_i, exit_kicad_i, send_keys, wait_create_i, save_interposer_print_data)
54
from kiauto.ui_automation import (PopenContext, xdotool, wait_not_focused, wait_for_window, recorded_xvfb,
6✔
55
                                  wait_point, text_replace, set_time_out_scale, open_dialog_with_retry, ShowInfoAction)
56

57
TITLE_CONFIRMATION = '^Confirmation$'
6✔
58
TITLE_ERROR = '^Error$'
6✔
59
TITLE_WARNING = '^Warning$'
6✔
60
TITLE_FILE_OPEN_ERROR = '^File Open (Error|Warning)$'
6✔
61
# This is very Debian specific, you need to install `gdb` and the kicad-nightly-dbg package
62
DEBUG_KICAD_NG = False
6✔
63
DEBUG_KICAD = False
6✔
64
VRML_UNITS = ['millimeters', 'meters', 'deciinches', 'inches']
6✔
65

66

67
def parse_drc_ki5(lines):
6✔
68
    drc_errors = None
2✔
69
    unconnected_pads = None
2✔
70
    in_errs = False
2✔
71
    in_wrns = False
2✔
72
    err_regex = re.compile(r'^ErrType\((\d+)\): (.*)')
2✔
73
    for line in lines:
2✔
74
        m = re.search(r'^\*\* Found ([0-9]+) DRC (errors|violations) \*\*$', line)
2✔
75
        if m:
2✔
76
            drc_errors = m.group(1)
2✔
77
            in_errs = True
2✔
78
            continue
2✔
79
        m = re.search(r'^\*\* Found ([0-9]+) unconnected pads \*\*$', line)
2✔
80
        if m:
2✔
81
            unconnected_pads = m.group(1)
2✔
82
            in_errs = False
2✔
83
            in_wrns = True
2✔
84
            continue
2✔
85
        m = re.search(r'^\*\* End of Report \*\*$', line)
2✔
86
        if m:
2✔
87
            break
2✔
88
        # TODO: Add support for this category
89
        m = re.search(r'^\*\* Found ([0-9]+) Footprint', line)
2✔
90
        if m:
2✔
91
            break
×
92
        if in_errs:
2✔
93
            m = err_regex.search(line)
2✔
94
            if m:
2✔
95
                cfg.errs.append('({}) {}'.format(m.group(1), m.group(2)))
2✔
96
                continue
2✔
97
            if len(line) > 4 and len(cfg.errs) > 0:
2✔
98
                cfg.errs.append(cfg.errs.pop()+'\n'+line)
2✔
99
                continue
2✔
100
        if in_wrns:
2✔
101
            m = err_regex.search(line)
2✔
102
            if m:
2✔
103
                cfg.wrns.append('({}) {}'.format(m.group(1), m.group(2)))
2✔
104
                continue
2✔
105
            if len(line) > 4 and len(cfg.wrns) > 0:
2✔
106
                cfg.wrns.append(cfg.wrns.pop()+'\n'+line)
2✔
107
                continue
2✔
108

109
    return int(drc_errors), int(unconnected_pads)
2✔
110

111

112
def parse_drc_ki6(lines):
6✔
113
    # A violation entry looks something like:
114
    #
115
    # [courtyards_overlap]: Courtyards overlap
116
    #     Local override; Severity: error (excluded)
117
    #     @(107.9891 mm, 114.3683 mm): Footprint SW104
118
    #     @(100.0000 mm, 119.3700 mm): Footprint J201
119
    #
120
    # Some violations may have fewer lines (eg missing courtyard will have only one location)
121
    # We are only interested in the first two lines of each violation.
122
    #
123
    # Unconnected traces are presented similarly:
124
    #
125
    # [unconnected_items]: Missing connection between items
126
    #     Local override; Severity: error
127
    #     @(99.4000 mm, 112.8900 mm): Pad 1 [Net-(J201-PadB5)] of R201 on B.Cu
128
    #     @(99.4000 mm, 114.5700 mm): Pad B5 [Net-(J201-PadB5)] of J201 on B.Cu
129
    err_regex = re.compile(r'^\[(\S+)\]: (.*)')
4✔
130
    sev_regex = re.compile(r'^    (.*); Severity: (.*)')
4✔
131
    # Collect all violations
132
    violations = []
4✔
133
    for offset in range(len(lines)-2):
4✔
134
        m = err_regex.search(lines[offset])
4✔
135
        s = sev_regex.search(lines[offset+1])
4✔
136
        if m and s:
4✔
137
            # Found a violation in this pair of lines
138
            violation = {'type': m.group(1), 'message': m.group(2), 'rule': s.group(1), 'severity': s.group(2)}
4✔
139
            assert violation['severity'] in ["error (excluded)", "error", "warning (excluded)", "warning"]
4✔
140
            violations.append(violation)
4✔
141
            context = []
4✔
142
            while offset+len(context) < len(lines):
4✔
143
                ln = lines[offset+2+len(context)]
4✔
144
                if not ln.startswith("    "):
4✔
145
                    break
4✔
146
                context.append(ln.rstrip())  # Strip out ending \n
4✔
147
            violation['context'] = "\n".join(context)
4✔
148
    # Classify the violations
149
    unconnected = [v for v in violations if v['type'] == "unconnected_items"]
4✔
150
    drc_errors = [v for v in violations if v['severity'] == "error" and v not in unconnected]
4✔
151
    drc_warns = [v for v in violations if v['severity'] == "warning" or v['type'] == "unconnected_items"]
4✔
152
    # Add the errors and warnings
153
    cfg.errs = ['({}) {}; Severity: error\n{}'.format(e['type'], e['message'], e['context']) for e in drc_errors]
4✔
154
    cfg.wrns = ['({}) {}; Severity: warning\n{}'.format(w['type'], w['message'], w['context']) for w in drc_warns]
4✔
155

156
    return len(drc_errors), len(drc_warns)
4✔
157

158

159
def parse_drc(cfg):
6✔
160
    with open(cfg.output_file, 'rt') as f:
6✔
161
        lines = f.read().splitlines()
6✔
162
    if cfg.ki5:
6✔
163
        return parse_drc_ki5(lines)
2✔
164
    else:
165
        return parse_drc_ki6(lines)
4✔
166

167

168
def dismiss_already_running():
6✔
169
    # The "Confirmation" modal pops up if pcbnew is already running
170
    nf_title = TITLE_CONFIRMATION
1✔
171
    wait_for_window(nf_title, nf_title, 1)
1✔
172

173
    logger.info('Dismiss pcbnew already running')
1✔
174
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
1✔
175
    logger.debug('Found, sending Return')
1✔
176
    xdotool(['key', 'Return'])
1✔
177
    logger.debug('Wait a little, this dialog is slow')
1✔
178
    sleep(5)
1✔
179

180

181
def dismiss_warning():  # pragma: no cover
182
    nf_title = TITLE_WARNING
183
    wait_for_window(nf_title, nf_title, 1)
184

185
    logger.error('Dismiss pcbnew warning, will fail')
186
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
187
    xdotool(['key', 'Return'])
188

189

190
def dismiss_file_open_error():
6✔
191
    nf_title = TITLE_FILE_OPEN_ERROR
2✔
192
    wait_for_window(nf_title, nf_title, 1)
2✔
193

194
    logger.warning('This file is already opened ({})'.format(cfg.input_file))
2✔
195
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
2✔
196
    xdotool(['key', 'Left', 'Return'])
2✔
197

198

199
def dismiss_error():
6✔
200
    nf_title = TITLE_ERROR
×
201
    wait_for_window(nf_title, nf_title, 1)
×
202

203
    logger.debug('Dismiss pcbnew error')
×
204
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
×
205
    logger.debug('Found, sending Return')
×
206
    xdotool(['key', 'Return'])
×
207

208

209
def wait_pcbnew(time=10, others=None):
6✔
210
    return wait_for_window('Main pcbnew window', cfg.pn_window_title, time, others=others, popen_obj=cfg.popen_obj)
6✔
211

212

213
def wait_pcbew_start(cfg):
6✔
214
    failed_focuse = False
3✔
215
    other = None
3✔
216
    try:
3✔
217
        id = wait_pcbnew(cfg.wait_start, [TITLE_CONFIRMATION, TITLE_WARNING, TITLE_ERROR, TITLE_FILE_OPEN_ERROR])
3✔
218
    except RuntimeError:  # pragma: no cover
219
        logger.debug('Time-out waiting for pcbnew, will retry')
220
        failed_focuse = True
221
    except ValueError as err:
3✔
222
        other = str(err)
3✔
223
        logger.debug('Found "'+other+'" window instead of pcbnew')
3✔
224
        failed_focuse = True
3✔
225
    except subprocess.CalledProcessError:
1✔
226
        logger.debug('Pcbnew is no longer running (returned {})'.format(cfg.popen_obj.poll()))
1✔
227
        id = [0]
1✔
228
    if failed_focuse:
3✔
229
        wait_point(cfg)
3✔
230
        if other == TITLE_ERROR:
3✔
231
            dismiss_error()
×
232
            logger.error('pcbnew reported an error')
×
233
            exit(PCBNEW_ERROR)
×
234
        elif other == TITLE_CONFIRMATION:
3✔
235
            dismiss_already_running()
1✔
236
        elif other == TITLE_WARNING:  # pragma: no cover
237
            dismiss_warning()
238
        elif other == TITLE_FILE_OPEN_ERROR:
2✔
239
            dismiss_file_open_error()
2✔
240
        try:
3✔
241
            id = wait_pcbnew(5)
3✔
242
        except RuntimeError:  # pragma: no cover
243
            logger.error('Time-out waiting for pcbnew, giving up')
244
            exit(PCBNEW_ERROR)
245
    if len(id) > 1:
3✔
246
        logger.error('More than one PCBNew windows detected, one could be a misnamed error dialog')
×
247
        exit(PCBNEW_ERROR)
×
248
    return id[0]
3✔
249

250

251
def exit_pcbnew(cfg):
6✔
252
    # Wait until the dialog is closed, useful when more than one file are created
253
    id = wait_pcbnew(10)
3✔
254

255
    send_keys(cfg, 'Exiting pcbnew', 'ctrl+q')
3✔
256
    try:
3✔
257
        wait_not_focused(id[0], 5)
3✔
258
    except RuntimeError:  # pragma: no cover
259
        logger.debug('PCBnew not exiting, will retry')
260
        pass
261
    # Dismiss any dialog. I.e. failed to write the project
262
    # Note: if we modified the PCB KiCad will ask for save using a broken dialog.
263
    #       It doesn't have a name and only gets focus with a WM.
264
    logger.info('Retry pcbnew exit')
3✔
265
    wait_point(cfg)
3✔
266
    xdotool(['key', 'Return', 'ctrl+q'])
3✔
267
    try:
3✔
268
        wait_not_focused(id[0], 5)
3✔
269
    except RuntimeError:  # pragma: no cover
270
        logger.debug('PCBnew not exiting, will kill')
271
        pass
272
    # If we failed to exit we will kill it anyways
273
    wait_point(cfg)
3✔
274

275

276
def open_print_dialog(cfg, print_dialog_keys, id_pcbnew):
6✔
277
    # Open the KiCad Print dialog
278
    logger.info('Open File->Print')
3✔
279
    wait_point(cfg)
3✔
280
    xdotool(['key']+print_dialog_keys)
3✔
281
    retry = False
3✔
282
    try:
3✔
283
        # Do a first try with small time-out, perhaps we sent the keys before the window was available
284
        id = wait_for_window('Print dialog', '^Print', skip_id=id_pcbnew, timeout=2)
3✔
285
    except RuntimeError:  # pragma: no cover
286
        # Perhaps the fill took too much try again
287
        retry = True
288
    # Retry the open dialog
289
    if retry:  # pragma: no cover
290
        # Excluded from coverage, only happends under conditions hard to reproduce
291
        logger.info('Open File->Print (retrying)')
292
        wait_point(cfg)
293
        xdotool(['key']+print_dialog_keys)
294
        id = wait_for_window('Print dialog', '^Print', skip_id=id_pcbnew)
295
    if len(id) == 1:
3✔
296
        # Only 1 window matched, the print dialog
297
        return id[0]
3✔
298
    if len(id) > 2:
×
299
        logger.error('Too much windows with similar names')
×
300
        exit(PCBNEW_ERROR)
×
301
    return id[1] if id[0] == id_pcbnew else id[0]
×
302

303

304
def print_layers(cfg, id_pcbnew):
6✔
305
    if not cfg.ki5:
6✔
306
        print_dialog_keys = ['ctrl+p']
4✔
307
    else:
308
        # We should be able to use Ctrl+P, unless the user configured it
309
        # otherwise. We aren't configuring hotkeys for 5.1 so is better
310
        # to just use the menu accelerators (removed on KiCad 6)
311
        print_dialog_keys = ['alt+f', 'p']
2✔
312
    if cfg.use_interposer:
6✔
313
        return print_layers_i(cfg, id_pcbnew, print_dialog_keys)
3✔
314
    return print_layers_n(cfg, id_pcbnew, print_dialog_keys)
3✔
315

316

317
def fill_zones_i(cfg):
6✔
318
    # Wait for KiCad to be sleeping
319
    wait_kicad_ready_i(cfg)
3✔
320
    # Now we fill the zones
321
    send_keys(cfg, 'Filling zones ...', 'b')
3✔
322
    # Wait fill end and inform the elapsed time (for slow fills)
323
    wait_and_show_progress(cfg, 'GTK:Window Destroy:Fill All Zones', r'(\d:\d\d:\d\d)', '0', 'Elapsed time',
3✔
324
                           skip_match='0:00:00', with_windows=True)
325

326

327
def print_layers_i(cfg, id_pcbnew, print_dialog_keys):
6✔
328
    # Create a temporal dir for the output file
329
    # GTK is limited to the names we can choose, so we let it create the name and move to the arbitrary one
330
    # Note: we can't control the extension
331
    with TemporaryDirectory() as tmpdir:
3✔
332
        # Pass the target dir, file name and format to the interposer
333
        fname = save_interposer_print_data(cfg, tmpdir, 'interposer', 'pdf' if not cfg.svg else 'svg')
3✔
334
        # Fill zones if the user asked for it
335
        if cfg.fill_zones:
3✔
336
            fill_zones_i(cfg)
3✔
337
        # Open the KiCad print dialog
338
        open_dialog_i(cfg, 'Print', ['key']+print_dialog_keys, extra_msg='KiCad')
3✔
339
        # Open the gtk print dialog
340
        # Big magic 1: we add an accelerator to the Print button, so Alt+P is enough
341
        open_dialog_i(cfg, 'Print', 'alt+p', extra_msg='GTK', no_main=True, no_show=True)
3✔
342
        # Confirm the options
343
        # Big magic 2: the interposer selected the printer, output file name and format, just confirm them
344
        send_keys(cfg, 'Print', 'Return', closes='Print', delay_io=True)
3✔
345
        # Wait until the file is created
346
        wait_create_i(cfg, 'print', fname)
3✔
347
        # Close KiCad Print dialog
348
        send_keys(cfg, 'Close Print dialog', 'Escape', closes='Print')
3✔
349
        # Move the file to the user selected name
350
        shutil.move(fname, cfg.output_file)
3✔
351
    # Exit
352
    exit_kicad_i(cfg)
3✔
353

354

355
def print_layers_n(cfg, id_pcbnew, print_dialog_keys):
6✔
356
    # Fill zones if the user asked for it
357
    if cfg.fill_zones:
3✔
358
        logger.info('Fill zones')
3✔
359
        wait_point(cfg)
3✔
360
        # Make sure KiCad is responding
361
        # We open the dialog and then we close it
362
        id_print_dialog = open_print_dialog(cfg, print_dialog_keys, id_pcbnew)
3✔
363
        xdotool(['key', 'Escape'])
3✔
364
        wait_not_focused(id_print_dialog)
3✔
365
        wait_pcbnew()
3✔
366
        # Now we fill the zones
367
        xdotool(['key', 'b'])
3✔
368
        # Wait for complation
369
        sleep(1)
3✔
370
        id_pcbnew = wait_pcbnew()[0]
3✔
371
    id_print_dialog = open_print_dialog(cfg, print_dialog_keys, id_pcbnew)
3✔
372
    # Open the gtk print dialog
373
    wait_point(cfg)
3✔
374
    # KiCad 5: Two possible options here:
375
    # 1) With WM we usually get "Exclude PCB edge ..." selected
376
    # 2) Without WM we usually get "Color" selected
377
    # In both cases sending 4 Shit+Tab moves us to one of the layer columns.
378
    # From there Return prints and Escape closes the window.
379
    # KiCad 6/7 on Debian 12: we get a button, going right selects the Print button
380
    keys = ['key', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'Right', 'Right', 'Return']
3✔
381
    xdotool(keys)
3✔
382
    # Check it is open
383
    id2 = wait_for_window('Printer dialog', '^(Print|%s)$' % cfg.print_dlg_name, skip_id=id_print_dialog)
3✔
384
    id_printer_dialog = id2[1] if id2[0] == id_print_dialog else id2[0]
3✔
385
    wait_point(cfg)
3✔
386
    # List of printers
387
    xdotool(['key', 'Tab',
3✔
388
             # Go up to the top
389
             'Home',
390
             # Output file name
391
             'Tab',
392
             # Open dialog
393
             'Return'])
394
    id_sel_f = wait_for_window('Select a filename', '(Select a filename|%s)' % cfg.select_a_filename, 2)
3✔
395
    logger.info('Pasting output dir')
3✔
396
    wait_point(cfg)
3✔
397
    text_replace(cfg.output_file)
3✔
398
    xdotool(['key',
3✔
399
             # Select this name
400
             'Return'])
401
    # Back to print
402
    retry = False
3✔
403
    try:
3✔
404
        wait_not_focused(id_sel_f[0])
3✔
405
    except RuntimeError:
×
406
        retry = True
×
407
    if retry:
3✔
408
        logger.debug('Retrying the Return to select the filename')
×
409
        xdotool(['key', 'Return'])
×
410
        wait_not_focused(id_sel_f[0])
×
411
    wait_for_window('Printer dialog', '^(Print|%s)$' % cfg.print_dlg_name, skip_id=id_print_dialog)
3✔
412
    wait_point(cfg)
3✔
413
    format = 3*['Left'] if not cfg.svg else 3*['Right']
3✔
414
    xdotool(['key',
3✔
415
             # Format options
416
             'Tab'] +
417
            # Be sure we are at left/right (PDF/SVG)
418
            format +
419
            # Print it
420
            ['Return'])
421
    # Wait until the file is created
422
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
3✔
423
    wait_not_focused(id_printer_dialog)
3✔
424
    # Now we should be in the KiCad Print dialog again
425
    wait_for_window('Print dialog', 'Print')
3✔
426
    wait_point(cfg)
3✔
427
    # Close the dialog
428
    # We are in one of the layer columns, here Escape works
429
    xdotool(['key', 'Escape'])
3✔
430
    wait_not_focused(id_print_dialog)
3✔
431
    # Exit
432
    exit_pcbnew(cfg)
3✔
433

434

435
def run_drc_5_1(cfg):
6✔
436
    logger.info('Open Inspect->DRC')
1✔
437
    wait_point(cfg)
1✔
438
    xdotool(['key', 'alt+i', 'd'])
1✔
439

440
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
1✔
441
    # Note: Refill zones on DRC gets saved in ~/.config/kicad/pcbnew as RefillZonesBeforeDrc
442
    # The space here is to enable the report of all errors for tracks
443
    logger.info('Enable reporting all errors for tracks')
1✔
444
    wait_point(cfg)
1✔
445
    xdotool(['key', 'Tab', 'Tab', 'Tab', 'Tab', 'space', 'Tab', 'Tab', 'Tab', 'Tab'])
1✔
446
    logger.info('Pasting output dir')
1✔
447
    wait_point(cfg)
1✔
448
    text_replace(cfg.output_file)
1✔
449
    xdotool(['key', 'Return'])
1✔
450

451
    wait_for_window('Report completed dialog', 'Disk File Report Completed')
1✔
452
    wait_point(cfg)
1✔
453
    xdotool(['key', 'Return'])
1✔
454
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
1✔
455

456
    logger.info('Closing the DRC dialog')
1✔
457
    wait_point(cfg)
1✔
458
    xdotool(['key', 'shift+Tab', 'Return'])
1✔
459
    wait_pcbnew()
1✔
460

461

462
def run_drc_6_0(cfg):
6✔
463
    logger.info('Open Inspect->DRC')
2✔
464
    wait_point(cfg)
2✔
465
    xdotool(['key', RULES_KEY.lower()])
2✔
466
    # Wait dialog
467
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
2✔
468
    # Run the DRC
469
    logger.info('Run DRC')
2✔
470
    wait_point(cfg)
2✔
471
    xdotool(['key', 'Return'])
2✔
472
    #
473
    # To know when KiCad finished we try this:
474
    # - Currently I can see a way, just wait some time
475
    #
476
    sleep(12*cfg.time_out_scale)
2✔
477
    # Save the DRC
478
    logger.info('Open the save dialog')
2✔
479
    wait_point(cfg)
2✔
480
    logger.info('Save DRC')
2✔
481
    wait_point(cfg)
2✔
482
    xdotool(['key', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'shift+Tab',
2✔
483
             'Right', 'Right', 'Right', 'Right', 'Up', 'Return'])
484
    # Wait for the save dialog
485
    wait_for_window('DRC File save dialog', 'Save Report to File')
2✔
486
    # Paste the name
487
    logger.info('Pasting output file')
2✔
488
    wait_point(cfg)
2✔
489
    text_replace(cfg.output_file)
2✔
490
    # Wait for report created
491
    logger.info('Wait for DRC file creation')
2✔
492
    wait_point(cfg)
2✔
493
    xdotool(['key', 'Return'])
2✔
494
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
2✔
495
    # Close the dialog
496
    logger.info('Closing the DRC dialog')
2✔
497
    wait_point(cfg)
2✔
498
    xdotool(['key', 'Escape'])
2✔
499
    wait_pcbnew()
2✔
500

501

502
def run_drc_python(cfg):
6✔
503
    logger.debug("Using Python interface instead of running KiCad")
4✔
504
    import pcbnew
4✔
505
    logger.debug("Re-filling zones")
4✔
506
    filler = pcbnew.ZONE_FILLER(cfg.board)
4✔
507
    filler.Fill(cfg.board.Zones())
4✔
508
    if cfg.kicad_version > KICAD_VERSION_6_99 and os.environ.get('KICAD7_FOOTPRINT_DIR') is None:
4✔
509
        # This is most probably a bug in the Python DRC code
510
        os.environ['KICAD7_FOOTPRINT_DIR'] = '/usr/share/kicad/footprints/'
×
511
    logger.debug("Running DRC")
4✔
512
    pcbnew.WriteDRCReport(cfg.board, cfg.output_file, pcbnew.EDA_UNITS_MILLIMETRES, True)
4✔
513
    if cfg.save:
4✔
514
        logger.info('Saving PCB')
4✔
515
        os.rename(cfg.input_file, cfg.input_file + '-bak')
4✔
516
        cfg.board.Save(cfg.input_file)
4✔
517

518

519
def run_drc_n(cfg):
6✔
520
    if not cfg.ki5:
3✔
521
        run_drc_6_0(cfg)
2✔
522
    else:
523
        run_drc_5_1(cfg)
1✔
524
    # Save the PCB
525
    if cfg.save:
3✔
526
        logger.info('Saving PCB')
1✔
527
        wait_point(cfg)
1✔
528
        os.rename(cfg.input_file, cfg.input_file + '-bak')
1✔
529
        xdotool(['key', 'ctrl+s'])
1✔
530
        logger.info('Wait for PCB file creation')
1✔
531
        wait_point(cfg)
1✔
532
        wait_for_file_created_by_process(cfg.pcbnew_pid, os.path.realpath(cfg.input_file))
1✔
533
    # Exit
534
    exit_pcbnew(cfg)
3✔
535

536

537
def run_drc_5_1_i(cfg):
6✔
538
    control_dlg, _ = open_dialog_i(cfg, cfg.drc_dialog_name, ['key', 'alt+i', 'd'], no_main=True)
1✔
539
    # Enable report all errors for tracks and go to the file name
540
    # Here we added a shortcut for "Report all errors for tracks (slower)"
541
    send_keys(cfg, 'Enable reporting all errors for tracks', ['key', 'alt+r', 'alt+c'])
1✔
542
    paste_output_file_i(cfg)
1✔
543
    # The following dialog indicates the report was finished
544
    # 'Disk File Report Completed' dialog
545
    file_dialog, _ = open_dialog_i(cfg, 'Disk File Report Completed', 'Return', no_show=True, no_main=True)
1✔
546
    send_keys(cfg, 'Close dialog', 'Return', closes=file_dialog)
1✔
547
    # Now close the DRC control
548
    send_keys(cfg, 'Closing the DRC dialog', 'alt+l', closes=control_dlg)
1✔
549

550

551
def run_drc_6_0_i(cfg):
6✔
552
    control_dlg, _ = open_dialog_i(cfg, cfg.drc_dialog_name, RULES_KEY.lower(), no_main=True)
2✔
553
    # Run the DRC
554
    send_keys(cfg, 'Run DRC', 'Return')
2✔
555
    # Wait for the end of the DRC (at the end KiCad restores the Close button)
556
    wait_queue(cfg, 'GTK:Button Label:C_lose')
2✔
557
    wait_kicad_ready_i(cfg)
2✔
558
    # Save the DRC
559
    # We added a short-cut for Save...
560
    file_dialog, _ = open_dialog_i(cfg, 'Save Report to File', 'alt+s')
2✔
561
    # Paste the name
562
    paste_bogus_filename(cfg)
2✔
563
    # Close the report dialog and also wait for the report creation (both in parallel)
564
    send_keys(cfg, 'Create report', 'Return', closes=file_dialog, delay_io=True)
2✔
565
    wait_create_i(cfg, 'DRC report')
2✔
566
    # Close the dialog
567
    # Note: The following command closes the dialog, but on GTK 3.24.34 it looks like
568
    # the dialog isn't destroyed. It seems to be safer to skip the wait.
569
    send_keys(cfg, 'Closing the DRC dialog', 'Escape')  # , closes=control_dlg)
2✔
570

571

572
def run_drc_i(cfg):
6✔
573
    if not cfg.ki5:
3✔
574
        run_drc_6_0_i(cfg)
2✔
575
    else:
576
        run_drc_5_1_i(cfg)
1✔
577
    # Save the PCB
578
    if cfg.save:
3✔
579
        os.rename(cfg.input_file, cfg.input_file+'-bak')
1✔
580
        wait_kicad_ready_i(cfg)
1✔
581
        send_keys(cfg, 'Saving PCB', 'ctrl+s')
1✔
582
        wait_create_i(cfg, 'PCB', fn=os.path.realpath(cfg.input_file))
1✔
583
    # Exit
584
    exit_kicad_i(cfg)
3✔
585

586

587
def run_drc(cfg):
6✔
588
    if cfg.use_interposer:
6✔
589
        return run_drc_i(cfg)
3✔
590
    return run_drc_n(cfg)
3✔
591

592

593
def export_gencad(cfg):
6✔
594
    wait_point(cfg)
6✔
595
    if cfg.ki5 or cfg.use_interposer:
6✔
596
        # With the interposer we add the missing accelerators to KiCad 6
597
        keys = ['key', 'alt+f', 'x', 'g']
4✔
598
    else:
599
        keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'KP_Space', 'Down', 'Return']
2✔
600
        if cfg.ki7:
2✔
601
            keys.insert(2, 'Down')
1✔
602
    if cfg.use_interposer:
6✔
603
        export_gencad_i(cfg, keys)
3✔
604
    else:
605
        export_gencad_n(cfg, keys)
3✔
606

607

608
def export_vrml_i(cfg, keys):
6✔
609
    # Open the "VRML Export Options"
610
    dialog, _ = open_dialog_i(cfg, 'VRML Export Options', keys)
3✔
611
    # Paste the name
612
    paste_output_file_i(cfg)
3✔
613
    if cfg.dir_models:
3✔
614
        # Paste the 3D path
615
        send_keys(cfg, 'Changing to 3D path', 'Down')
3✔
616
        wait_kicad_ready_i(cfg)
3✔
617
        paste_text_i(cfg, 'Pasting 3D path', cfg.dir_models)
3✔
618
    # Do it
619
    send_keys(cfg, 'Generating the VRML', ['key', 'Return'], closes=dialog, delay_io=True)
3✔
620
    wait_create_i(cfg, 'VRML')
3✔
621
    # Exit
622
    exit_kicad_i(cfg)
3✔
623

624

625
def export_vrml_n(cfg, keys):
6✔
626
    open_dialog_with_retry("Open VRML export", keys, "VRML options", 'VRML Export Options', cfg)
3✔
627
    xdotool(['key', 'ctrl+a'])
3✔
628
    xdotool(['type', cfg.output_file])
3✔
629
    if cfg.dir_models:
3✔
630
        xdotool(['key', 'Down'])
3✔
631
        xdotool(['type', cfg.dir_models])
3✔
632
    xdotool(['key', 'Return'])
3✔
633
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
3✔
634

635

636
def export_vrml(cfg):
6✔
637
    wait_point(cfg)
6✔
638
    if cfg.ki5 or cfg.use_interposer:
6✔
639
        # With the interposer we add the missing accelerators to KiCad 6
640
        keys = ['key', 'alt+f', 'x', 'v']
4✔
641
    else:
642
        keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'KP_Space', 'Down', 'Down',
2✔
643
                'Return']
644
        if cfg.ki7:
2✔
645
            keys.insert(2, 'Down')
1✔
646
    if cfg.use_interposer:
6✔
647
        export_vrml_i(cfg, keys)
3✔
648
    else:
649
        export_vrml_n(cfg, keys)
3✔
650

651

652
def export_gencad_select_options(cfg):
6✔
653
    logger.info('Changing settings')
3✔
654
    xdotool(['key', 'Down'])
3✔
655
    if cfg.flip_bottom_padstacks:
3✔
656
        xdotool(['key', 'KP_Space'])
3✔
657
    xdotool(['key', 'Down'])
3✔
658
    if cfg.unique_pin_names:
3✔
659
        xdotool(['key', 'KP_Space'])
×
660
    xdotool(['key', 'Down'])
3✔
661
    if cfg.no_reuse_shapes:
3✔
662
        xdotool(['key', 'KP_Space'])
3✔
663
    xdotool(['key', 'Down'])
3✔
664
    if cfg.aux_origin:
3✔
665
        xdotool(['key', 'KP_Space'])
×
666
    xdotool(['key', 'Down'])
3✔
667
    if cfg.save_origin:
3✔
668
        xdotool(['key', 'KP_Space'])
3✔
669
    xdotool(['key', 'Return'])
3✔
670
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
3✔
671

672

673
def export_gencad_i(cfg, keys):
6✔
674
    # Open the "Export to GenCAD settings"
675
    dialog, _ = open_dialog_i(cfg, 'Export to GenCAD settings', keys)
3✔
676
    # KiCad dialogs are unreliable, you never know which widget is selected
677
    # This goes to the top left corner of the dialog
678
    # Note 1: lamentably this widget doesn't have a label
679
    # Note 2: KiCad 7.0.7 behavior changed, up goes to the button (when no WM)
680
    xdotool(['key', 'alt+f', 'Up', 'Left'])
3✔
681
    # Paste the name
682
    paste_output_file_i(cfg)
3✔
683
    # Change the settings to what the user wants
684
    # To make it easier we add accelerators
685
    keys = ['key']
3✔
686
    if not cfg.flip_bottom_padstacks:
3✔
687
        # We used Alt+F to go to the file name, so here the logic is inverted
688
        keys.append('alt+f')
×
689
    if cfg.unique_pin_names:
3✔
690
        keys.append('alt+g')
×
691
    if cfg.no_reuse_shapes:
3✔
692
        keys.append('alt+n')
3✔
693
    if cfg.aux_origin:
3✔
694
        keys.append('alt+u')
×
695
    if cfg.save_origin:
3✔
696
        keys.append('alt+s')
3✔
697
    keys.append('Return')
3✔
698
    send_keys(cfg, 'Changing settings', keys, closes=dialog, delay_io=True)
3✔
699
    wait_create_i(cfg, 'GenCAD')
3✔
700
    # Exit
701
    exit_kicad_i(cfg)
3✔
702

703

704
def export_gencad_n(cfg, keys):
6✔
705
    open_dialog_with_retry("Open GenCAD export", keys, "GenCAD settings", 'Export to GenCAD settings', cfg)
3✔
706
    xdotool(['type', cfg.output_file])
3✔
707
    export_gencad_select_options(cfg)
3✔
708

709

710
def ipc_netlist(cfg):
6✔
711
    wait_point(cfg)
6✔
712
    if cfg.ki5:
6✔
713
        if cfg.use_interposer:
2✔
714
            keys = ['key', 'alt+f', 'f', 'i']
1✔
715
        else:
716
            keys = ['key', 'alt+f', 'f', 'Down', 'Down', 'Down', 'Return']
1✔
717
    else:
718
        keys = ['key', 'shift+alt+e']
4✔
719
    if cfg.use_interposer:
6✔
720
        ipc_netlist_i(cfg, keys)
3✔
721
    else:
722
        ipc_netlist_n(cfg, keys)
3✔
723

724

725
def ipc_netlist_n(cfg, keys):
6✔
726
    open_dialog_with_retry("Open file save for IPC netlist", keys, "Dialog to save the IPC netlist",
3✔
727
                           'Export D-356 Test File', cfg)
728
    xdotool(['key', 'ctrl+a'])
3✔
729
    xdotool(['type', cfg.output_file])
3✔
730
    xdotool(['key', 'Return'])
3✔
731
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
3✔
732

733

734
def ipc_netlist_i(cfg, keys):
6✔
735
    # Open the "Export D-356 Test File"
736
    dialog, _ = open_dialog_i(cfg, 'Export D-356 Test File', keys)
3✔
737
    # Paste the name (well, something that will be replaced by the real name)
738
    paste_bogus_filename(cfg)
3✔
739
    # Generate the netlist
740
    send_keys(cfg, 'Generate the netlist', 'Return', closes=dialog, delay_io=True)
3✔
741
    # Wait for file creation
742
    wait_create_i(cfg, 'IPC D-356')
3✔
743
    # Exit
744
    exit_kicad_i(cfg)
3✔
745

746

747
def wait_ray_tracer(cfg):
6✔
748
    # I can't find a mechanism to determine if the render finished.
749
    # I think this is a bug in KiCad, you can save unfinished images!!!!
750
    logger.info('Waiting for the final render')
3✔
751
    if cfg.detect_rt:
3✔
752
        # Try to figure out meassuring the CPU usage
753
        end = time.clock_gettime(time.CLOCK_MONOTONIC) + cfg.wait_rt
3✔
754
        counter = 5
3✔
755
        children = cfg.kicad_process.children(recursive=True)
3✔
756
        logger.debug('pcbnew pid {}'.format(cfg.kicad_process.pid))
3✔
757
        for child in children:
3✔
758
            logger.debug('- pcbnew child pid {}'.format(child.pid))
×
759
        while (counter > 0) and (time.clock_gettime(time.CLOCK_MONOTONIC) < end):
3✔
760
            cpu_usage = cfg.kicad_process.cpu_percent(0.3)
3✔
761
            for child in children:
3✔
762
                cpu_usage += child.cpu_percent(0.3)
×
763
            if cpu_usage > 5:
3✔
764
                if counter < 5:
3✔
765
                    counter = counter + 1
×
766
            else:
767
                counter = counter - 1
3✔
768
            logger.debug('CPU {} %, ({}) waiting ...'.format(cpu_usage, counter))
3✔
769
    else:
770
        sleep(cfg.wait_rt)
×
771

772

773
def wait_ray_tracer_i(cfg):
6✔
774
    # Wait until we see the 100% rendered and the time is reported
775
    wait_and_show_progress(cfg, 'PANGO:Rendering time', r'Rendering: (\d+ \%)', 'Rendering:', 'Rendering')
3✔
776
    # wait_swap(cfg) should be absorved by the wait for sleep at the end of the progress
777

778

779
def wait_3d_ready_n(cfg):
6✔
780
    if not cfg.ki5:
3✔
781
        # On my system this huge delay is needed only when using docker.
782
        # I don't know why and also don't know why the KiCad 5 methode fails.
783
        sleep(1*cfg.time_out_scale)
2✔
784
        return
2✔
785
    sleep(0.2*cfg.time_out_scale)
1✔
786
    keys = ['key', 'alt+p', 'Return']
1✔
787
    dname = '3D Display Options'
1✔
788
    if not cfg.ki5:
1✔
789
        keys.insert(2, 'Down')
×
790
        dname = 'Preferences'
×
791
    for retry in range(30):
1✔
792
        xdotool(keys)
1✔
793
        found = True
1✔
794
        try:
1✔
795
            wait_for_window('Options dialog', dname, 1)
1✔
796
        except RuntimeError:  # pragma: no cover
797
            found = False
798
        if found:
1✔
799
            break
1✔
800
    if not found:
1✔
801
        logger.error('Time-out waiting for 3D viewer to be responsive')
×
802
        exit(PCBNEW_ERROR)
×
803
    xdotool(['key', 'Escape'])
1✔
804
    wait_for_window('3D Viewer', '3D Viewer')
1✔
805

806

807
def apply_steps(steps, key, neg_key, id, cfg):
6✔
808
    if steps:
6✔
809
        k = key
6✔
810
        if steps < 0:
6✔
811
            steps = -steps
6✔
812
            k = neg_key
6✔
813
        for _ in range(steps):
6✔
814
            logger.info('Step ({})'.format(k))
6✔
815
            xdotool(['key', k], id)
6✔
816
            logger.debug('Step '+key)
6✔
817
            if cfg.use_interposer:
6✔
818
                if cfg.wait_after_move:
3✔
819
                    wait_kicad_ready_i(cfg, swaps=1)
1✔
820
                    # Arbitrary, this is a problem with KiCad 5, so I won't spend too much time looking for a better solution
821
                    sleep(1)
1✔
822
                    wait_kicad_ready_i(cfg)
1✔
823
            else:
824
                wait_3d_ready_n(cfg)
3✔
825

826

827
def open_save_image_n(cfg, id):
6✔
828
    keys = ['key', 'alt+f', 'Return']
3✔
829
    for retry in range(10):
3✔
830
        xdotool(keys, id)
3✔
831
        if (cfg.ray_tracing or cfg.use_rt_wait) and cfg.detect_rt:
3✔
832
            wait_ray_tracer(cfg)
3✔
833
        found = True
3✔
834
        try:
3✔
835
            wait_for_window('File save dialog', '3D Image File Name', 3)
3✔
836
        except RuntimeError:  # pragma: no cover
837
            found = False
838
        if found:
3✔
839
            break
3✔
840
    if not found:
3✔
841
        logger.error('Failed to open the file save dialog')
×
842
        exit(PCBNEW_ERROR)
×
843

844

845
def capture_3d_view(cfg):
6✔
846
    if not cfg.ki5 and cfg.kicad_version < 6000002:
6✔
847
        logger.error('Upgrade KiCad, this version has a bug')
×
848
        exit(MISSING_TOOL)
×
849
    # Configure KiCad 5 vs 6 differences
850
    if cfg.ki5:
6✔
851
        cfg.keys_rt = ['alt+p', 'Down']
2✔
852
        cfg.key_close_3d = 'ctrl+q'
2✔
853
    else:
854
        cfg.keys_rt = ['alt+p']
4✔
855
        cfg.key_close_3d = 'ctrl+w'
4✔
856
    if cfg.use_interposer:
6✔
857
        return capture_3d_view_i(cfg)
3✔
858
    return capture_3d_view_n(cfg)
3✔
859

860

861
def capture_3d_view_n(cfg):
6✔
862
    """ 3D View capture, normal version """
863
    # Open the 3D viewer
864
    open_keys = ['key', 'alt+3']
3✔
865
    dialog_name = '3D Viewer'
3✔
866
    id = open_dialog_with_retry('Open 3D Viewer', open_keys, '3D viewer dialog', dialog_name, cfg)[0]
3✔
867
    wait_point(cfg)
3✔
868

869
    wait_3d_ready_n(cfg)
3✔
870

871
    # Switch to orthographic (KiCad 6 has persistence)
872
    if cfg.orthographic and cfg.ki5:
3✔
873
        # Can easily break, no persistence, no option, no shortcut ... KiCad's way
874
        xdotool(['mousemove', '711', '44', 'click', '1', 'mousemove', 'restore'])
1✔
875

876
    # Apply the view axis
877
    if cfg.view != 'z':
3✔
878
        xdotool(['key', cfg.view], id)
×
879
        wait_3d_ready_n(cfg)
×
880

881
    # Apply the movements
882
    apply_steps(cfg.move_x, 'Right', 'Left', id, cfg)
3✔
883
    apply_steps(cfg.move_y, 'Up', 'Down', id, cfg)
3✔
884

885
    # Apply the rotations
886
    if not cfg.ki5:
3✔
887
        apply_steps(cfg.rotate_x, 'alt+x', 'shift+alt+x', id, cfg)
2✔
888
        apply_steps(cfg.rotate_y, 'alt+y', 'shift+alt+y', id, cfg)
2✔
889
        apply_steps(cfg.rotate_z, 'alt+z', 'shift+alt+z', id, cfg)
2✔
890

891
    # Apply the zoom steps
892
    zoom = cfg.zoom
3✔
893
    if zoom:
3✔
894
        zoom_b = '4'
3✔
895
        if zoom < 0:
3✔
896
            zoom = -zoom
×
897
            zoom_b = '5'
×
898
        for _ in range(zoom):
3✔
899
            # Wait some time before sending commands
900
            # See discussion on #13 issue at GitHub
901
            sleep(0.05)
3✔
902
            logger.info('Zoom')
3✔
903
            xdotool(['click', zoom_b], id)
3✔
904
            logger.debug('Zoom')
3✔
905
            wait_3d_ready_n(cfg)
3✔
906

907
    if cfg.ray_tracing:
3✔
908
        xdotool(['key']+cfg.keys_rt+['Return'], id)
3✔
909
        logger.debug('Ray tracing')
3✔
910

911
    if cfg.ray_tracing or cfg.use_rt_wait:
3✔
912
        wait_ray_tracer(cfg)
3✔
913

914
    # Save the image as PNG
915
    logger.info('Saving the image')
3✔
916
    open_save_image_n(cfg, id)
3✔
917

918
    # Paste the name
919
    logger.info('Pasting output file')
3✔
920
    wait_point(cfg)
3✔
921
    text_replace(cfg.output_file)
3✔
922

923
    # Wait for the image to be created
924
    logger.info('Wait for the image file creation')
3✔
925
    wait_point(cfg)
3✔
926
    # Wait before confirming the file name
927
    sleep(0.1*cfg.time_out_scale)
3✔
928
    xdotool(['key', 'Return'])
3✔
929
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
3✔
930

931
    # Close the 3D viewer
932
    logger.info('Closing the 3D viewer')
3✔
933
    wait_point(cfg)
3✔
934
    xdotool(['key', cfg.key_close_3d])
3✔
935

936
    wait_pcbnew()
3✔
937

938

939
def capture_3d_view_i(cfg):
6✔
940
    """ 3D View capture, interposer version """
941
    dialog, id = open_dialog_i(cfg, '3D Viewer', 'alt+3', no_wait=True)
3✔
942
    logger.debug('3D Viewer is drawing')
3✔
943

944
    # Detect the 3D models load (On Ki5 we can miss the "loading..." message
945
    logger.info('Loading 3D models')
3✔
946
    reload_time_msg = 'PANGO:Reload time'
3✔
947
    if not cfg.ki5:
3✔
948
        # KiCad 6 does a first bogus render without loading the models
949
        wait_queue(cfg, reload_time_msg, starts=True)
2✔
950
        # Then comes the real action
951
    wait_and_show_progress(cfg, reload_time_msg, r'Loading (.*)', 'Loading', 'Loading')
3✔
952
    logger.info('Finished loading 3D models')
3✔
953
    # Wait until the dialog is ready
954
    id = wait_for_window('3D Viewer', '3D Viewer', 1)[0]
3✔
955
    if cfg.ki5:
3✔
956
        # KiCad 5 can't handle all commands at once
957
        cfg.wait_after_move = True
1✔
958

959
    # Switch to orthographic (KiCad 6 has persistence)
960
    if cfg.orthographic and cfg.ki5:
3✔
961
        # Can easily break, no persistence, no option, no shortcut ... KiCad's way
962
        # Note: This is just for KiCad 5, and can't be patched by the interposer because
963
        # this button is handled by the WX AUI (not by the underlying widgets)
964
        xdotool(['mousemove', '711', '44', 'click', '1', 'mousemove', 'restore'])
1✔
965

966
    # Apply the view axis
967
    if cfg.view != 'z':
3✔
968
        logger.info('Changing view')
×
969
        xdotool(['key', cfg.view], id)
×
970
        if cfg.wait_after_move:
×
971
            wait_kicad_ready_i(cfg, swaps=1)
×
972

973
    # Apply the movements
974
    apply_steps(cfg.move_x, 'Right', 'Left', id, cfg)
3✔
975
    apply_steps(cfg.move_y, 'Up', 'Down', id, cfg)
3✔
976

977
    # Apply the rotations
978
    if not cfg.ki5:
3✔
979
        apply_steps(cfg.rotate_x, 'alt+x', 'shift+alt+x', id, cfg)
2✔
980
        apply_steps(cfg.rotate_y, 'alt+y', 'shift+alt+y', id, cfg)
2✔
981
        apply_steps(cfg.rotate_z, 'alt+z', 'shift+alt+z', id, cfg)
2✔
982

983
    # Apply the zoom steps
984
    zoom = cfg.zoom
3✔
985
    if zoom:
3✔
986
        zoom_b = '4'
3✔
987
        if zoom < 0:
3✔
988
            zoom = -zoom
×
989
            zoom_b = '5'
×
990
        for _ in range(zoom):
3✔
991
            # Wait some time before sending commands
992
            # See discussion on #13 issue at GitHub
993
            sleep(0.05)
3✔
994
            logger.info('Zoom')
3✔
995
            xdotool(['click', zoom_b], id)
3✔
996
            logger.debug('Zoom')
3✔
997
            # An extra swap is done because we used the mouse (mouse "moved")
998
            if cfg.wait_after_move:
3✔
999
                wait_kicad_ready_i(cfg, swaps=1)
1✔
1000

1001
    if cfg.ray_tracing:
3✔
1002
        send_keys(cfg, 'Start ray tracing', ['key']+cfg.keys_rt+['Return'])
3✔
1003
        wait_queue(cfg, 'PANGO:Raytracing')
3✔
1004
        wait_ray_tracer_i(cfg)
3✔
1005

1006
    # Save the image as PNG
1007
    # Open the Save dialog
1008
    file_dlg, _ = open_dialog_i(cfg, '3D Image File Name', ['key', 'alt+f', 'Return'])
3✔
1009
    # Paste the name
1010
    paste_bogus_filename(cfg)
3✔
1011
    # Wait for the image to be created
1012
    send_keys(cfg, 'Export the PNG', 'Return', closes=file_dlg, delay_io=True)
3✔
1013
    # Wait until the file is created (why we just see the close?)
1014
    wait_create_i(cfg, 'PNG')
3✔
1015
    # Close the 3D viewer
1016
    send_keys(cfg, 'Closing the 3D viewer', cfg.key_close_3d, closes=dialog)
3✔
1017
    exit_kicad_i(cfg)
3✔
1018

1019

1020
def load_layers(pcb):
6✔
1021
    layer_names = []
6✔
1022
    with open(pcb, "rt") as pcb_file:
6✔
1023
        collect_layers = False
6✔
1024
        for line in pcb_file:
6✔
1025
            if collect_layers:
6✔
1026
                z = re.match(r'\s+\((\d+)\s+"[^"]+"\s+\S+\s+"([^"]+)"', line)
6✔
1027
                if not z:
6✔
1028
                    z = re.match(r'\s+\((\d+)\s+(\S+)', line)
6✔
1029
                if z:
6✔
1030
                    id, name = z.groups()
6✔
1031
                    if name[0] == '"':
6✔
1032
                        name = name[1:-1]
4✔
1033
                    layer_names.append(name)
6✔
1034
                else:
1035
                    if re.search(r'^\s+\)$', line):
6✔
1036
                        collect_layers = False
6✔
1037
                        break
6✔
1038
            else:
1039
                if re.search(r'\s+\(layers', line):
6✔
1040
                    collect_layers = True
6✔
1041
    return layer_names
6✔
1042

1043

1044
class ListLayers(argparse.Action):
6✔
1045
    """A special action class to list the PCB layers and exit"""
1046
    def __call__(self, parser, namespace, values, option_string):
6✔
1047
        layer_names = load_layers(values[0])
6✔
1048
        for layer in layer_names:
6✔
1049
            print(layer)
6✔
1050
        parser.exit()  # exits the program with no more arg parsing and checking
6✔
1051

1052

1053
def restore_pcb(cfg):
6✔
1054
    if cfg.input_file and cfg.pcb_size >= 0 and cfg.pcb_date >= 0:
6✔
1055
        cur_date = os.path.getmtime(cfg.input_file)
6✔
1056
        bkp = cfg.input_file+'-bak'
6✔
1057
        if cur_date != cfg.pcb_date:
6✔
1058
            logger.debug('Current pcb date: {} (!={}), trying to restore it'.
4✔
1059
                         format(asctime(localtime(cur_date)), asctime(localtime(cfg.pcb_date))))
1060
            if os.path.isfile(bkp):
4✔
1061
                bkp_size = os.path.getsize(bkp)
4✔
1062
                if bkp_size == cfg.pcb_size:
4✔
1063
                    os.remove(cfg.input_file)
4✔
1064
                    os.rename(bkp, cfg.input_file)
4✔
1065
                    logger.debug('Moved {} -> {}'.format(bkp, cfg.input_file))
4✔
1066
                else:  # pragma: no cover
1067
                    logger.error('Corrupted back-up file! (size = {})'.format(bkp_size))
1068
            else:  # pragma: no cover
1069
                logger.error('No back-up available!')
1070
        if not cfg.ki5 and os.path.isfile(bkp):
6✔
1071
            os.remove(bkp)
4✔
1072

1073

1074
def memorize_pcb(cfg):
6✔
1075
    cfg.pcb_size = os.path.getsize(cfg.input_file)
6✔
1076
    cfg.pcb_date = os.path.getmtime(cfg.input_file)
6✔
1077
    logger.debug('Current pcb ({}) size: {} date: {}'.
6✔
1078
                 format(cfg.input_file, cfg.pcb_size, asctime(localtime(cfg.pcb_date))))
1079
    if not cfg.ki5:
6✔
1080
        # KiCad 6 no longer creates back-up, we do it
1081
        try:
4✔
1082
            shutil.copy2(cfg.input_file, cfg.input_file+'-bak')
4✔
1083
        except PermissionError:
×
1084
            logger.warning("Unable to create a back-up for the PCB (read-only?)")
×
1085
            return
×
1086
    atexit.register(restore_pcb, cfg)
6✔
1087

1088

1089
def write_color(out, name, color, post=''):
6✔
1090
    if color is None:
2✔
1091
        return
2✔
1092
    if post:
2✔
1093
        post = '_'+post
2✔
1094
    name += 'Color_'
2✔
1095
    out.write('%s=%f\n' % (name+'Red'+post, color[0]))
2✔
1096
    out.write('%s=%f\n' % (name+'Green'+post, color[1]))
2✔
1097
    out.write('%s=%f\n' % (name+'Blue'+post, color[2]))
2✔
1098

1099

1100
def to_rgb(color, bottom=False):
6✔
1101
    index = 4 if bottom and len(color) > 4 else 0
4✔
1102
    alpha = color[index+3]
4✔
1103
    if alpha == 1.0:
4✔
1104
        return "rgb({}, {}, {})".format(round(color[index]*255.0), round(color[index+1]*255.0), round(color[index+2]*255.0))
4✔
1105
    return "rgba({}, {}, {}, {})".format(round(color[index]*255.0), round(color[index+1]*255.0), round(color[index+2]*255.0),
4✔
1106
                                         alpha)
1107

1108

1109
def create_pcbnew_config(cfg):
6✔
1110
    # Mark which layers are requested
1111
    used_layers = set()
6✔
1112
    if cfg.layers:
6✔
1113
        layer_cnt = cfg.board.GetCopperLayerCount()
6✔
1114
    for layer in cfg.layers:
6✔
1115
        # Support for kiplot inner layers
1116
        if layer.startswith("Inner"):
6✔
1117
            m = re.match(r"^Inner\.([0-9]+)$", layer)
6✔
1118
            if not m:
6✔
1119
                logger.error('Malformed inner layer name: '+layer+', use Inner.N')
6✔
1120
                sys.exit(WRONG_LAYER_NAME)
6✔
1121
            layer_n = int(m.group(1))
6✔
1122
            if layer_n < 1 or layer_n >= layer_cnt - 1:
6✔
1123
                logger.error(layer+" isn't a valid layer")
6✔
1124
                sys.exit(WRONG_LAYER_NAME)
6✔
1125
            used_layers.add(layer_n)
6✔
1126
        else:
1127
            id = cfg.board.GetLayerID(layer)
6✔
1128
            if id < 0:
6✔
1129
                logger.error('Unknown layer '+layer)
6✔
1130
                sys.exit(WRONG_LAYER_NAME)
6✔
1131
            used_layers.add(id)
6✔
1132
    with open(cfg.conf_pcbnew, "wt") as text_file:
6✔
1133
        if cfg.conf_pcbnew_json:
6✔
1134
            conf = {"graphics": {"canvas_type": 2}}
4✔
1135
            conf["drc_dialog"] = {"refill_zones": True,
4✔
1136
                                  "test_track_to_zone": True,
1137
                                  "test_all_track_errors": True}
1138
            conf["system"] = {"first_run_shown": True}
4✔
1139
            conf["printing"] = {"monochrome": cfg.monochrome,
4✔
1140
                                "color_theme": cfg.color_theme,
1141
                                "use_theme": True,
1142
                                "title_block": not cfg.no_title,
1143
                                "scale": cfg.scaling,
1144
                                "layers": sorted(used_layers)}
1145
            conf["plot"] = {"check_zones_before_plotting": cfg.fill_zones,
4✔
1146
                            "mirror": cfg.mirror,
1147
                            "all_layers_on_one_page": int(not cfg.separate),
1148
                            "pads_drill_mode": cfg.pads}
1149
            conf["window"] = {"size_x": cfg.rec_width,
4✔
1150
                              "size_y": cfg.rec_height}
1151
            conf["export_vrml"] = {"copy_3d_models": cfg.copy_3d_models,
4✔
1152
                                   "origin_mode": cfg.origin_mode,
1153
                                   "ref_units": cfg.ref_units,
1154
                                   "ref_x": cfg.ref_x,
1155
                                   "ref_y": cfg.ref_y,
1156
                                   "units": cfg.units,
1157
                                   "use_relative_paths": cfg.use_relative_paths}
1158
            json_text = json.dumps(conf)
4✔
1159
            text_file.write(json_text)
4✔
1160
            logger.debug(json_text)
4✔
1161
            # Colors for the 3D Viewer
1162
            if cfg.bg_color_1 is not None:
4✔
1163
                conf = {}
4✔
1164
                conf["3d_viewer"] = {"background_bottom": to_rgb(cfg.bg_color_1),
4✔
1165
                                     "background_top": to_rgb(cfg.bg_color_2),
1166
                                     "board": to_rgb(cfg.board_color),
1167
                                     "copper": to_rgb(cfg.copper_color),
1168
                                     "silkscreen_bottom": to_rgb(cfg.silk_color, bottom=True),
1169
                                     "silkscreen_top": to_rgb(cfg.silk_color),
1170
                                     "soldermask_bottom": to_rgb(cfg.sm_color, bottom=True),
1171
                                     "soldermask_top": to_rgb(cfg.sm_color),
1172
                                     "solderpaste": to_rgb(cfg.sp_color),
1173
                                     "use_board_stackup_colors": False}
1174
                json_text = json.dumps(conf)
4✔
1175
                os.makedirs(os.path.dirname(cfg.conf_colors), exist_ok=True)
4✔
1176
                with open(cfg.conf_colors, "wt") as color_file:
4✔
1177
                    color_file.write(json_text)
4✔
1178
                logger.debug("Colors:")
4✔
1179
                logger.debug(json_text)
4✔
1180
                # 3D Viewer window
1181
                conf = {}
4✔
1182
                conf["window"] = {"pos_x": 0, "pos_y": 0, "size_x": cfg.rec_width, "size_y": cfg.rec_height}
4✔
1183
                conf["camera"] = {"projection_mode": int(not cfg.orthographic), "animation_enabled": False}
4✔
1184
                conf["render"] = {"show_footprints_insert": not cfg.no_smd,
4✔
1185
                                  "show_footprints_normal": not cfg.no_tht,
1186
                                  "show_footprints_virtual": not cfg.no_virtual,
1187
                                  "show_axis": False,
1188
                                  "opengl_AA_mode": 0,
1189
                                  "show_silkscreen": not cfg.hide_silkscreen,
1190
                                  "show_soldermask": not cfg.hide_soldermask,
1191
                                  "show_solderpaste": not cfg.hide_solderpaste,
1192
                                  "show_zones": not cfg.hide_zones,
1193
                                  "clip_silk_on_via_annulus": not cfg.dont_clip_silk_on_via_annulus,
1194
                                  "subtract_mask_from_silk": not cfg.dont_substrack_mask_from_silk,
1195
                                  "realistic": not cfg.use_layer_colors,
1196
                                  "show_board_body": not cfg.hide_board_body,
1197
                                  "show_comments": cfg.show_comments,
1198
                                  "show_eco": cfg.show_eco,
1199
                                  "show_adhesive": cfg.show_adhesive}
1200
                json_text = json.dumps(conf)
4✔
1201
                with open(cfg.conf_3dview, "wt") as viewer_file:
4✔
1202
                    viewer_file.write(json_text)
4✔
1203
                logger.debug("3D Viewer:")
4✔
1204
                logger.debug(json_text)
4✔
1205
        else:
1206
            text_file.write('canvas_type=2\n')
2✔
1207
            text_file.write('RefillZonesBeforeDrc=1\n')
2✔
1208
            text_file.write('DrcTrackToZoneTest=1\n')
2✔
1209
            text_file.write('PcbFrameFirstRunShown=1\n')
2✔
1210
            # Color
1211
            text_file.write('PrintMonochrome=%d\n' % (cfg.monochrome))
2✔
1212
            # Include frame
1213
            text_file.write('PrintPageFrame=%d\n' % (not cfg.no_title))
2✔
1214
            # Drill marks
1215
            text_file.write('PrintPadsDrillOpt=%d\n' % (cfg.pads))
2✔
1216
            # Only one file
1217
            text_file.write('PrintSinglePage=%d\n' % (not cfg.separate))
2✔
1218
            # Scaling
1219
            text_file.write('PrintScale=%3.1f\n' % (cfg.scaling))
2✔
1220
            # List all posible layers, indicating which ones are requested
1221
            for x in range(0, 50):
2✔
1222
                text_file.write('PlotLayer_%d=%d\n' % (x, int(x in used_layers)))
2✔
1223
            # The output image size is the window size!!!
1224
            text_file.write('Viewer3DFrameNamePos_x=0\n')
2✔
1225
            text_file.write('Viewer3DFrameNamePos_y=0\n')
2✔
1226
            text_file.write('Viewer3DFrameNameSize_x=%d\n' % (cfg.rec_width))
2✔
1227
            text_file.write('Viewer3DFrameNameSize_y=%d\n' % (cfg.rec_height))
2✔
1228
            # We must indicate a window size compatible with the screen.
1229
            # Otherwise events could fail to reach the main window.
1230
            text_file.write('PcbFramePos_x=0\n')
2✔
1231
            text_file.write('PcbFramePos_y=0\n')
2✔
1232
            text_file.write('PcbFrameSize_x=%d\n' % (cfg.rec_width))
2✔
1233
            text_file.write('PcbFrameSize_y=%d\n' % (cfg.rec_height))
2✔
1234
            text_file.write('ShowAxis=0\n')
2✔
1235
            text_file.write('ShowFootprints_Normal=%d\n' % (not cfg.no_tht))  # Normal?!
2✔
1236
            # Insert????!!!! please a cup of coffee for this guy ...
1237
            text_file.write('ShowFootprints_Insert=%d\n' % (not cfg.no_smd))
2✔
1238
            text_file.write('ShowFootprints_Virtual=%d\n' % (not cfg.no_virtual))
2✔
1239
            # We enable the raytracer after applying all moves
1240
            # text_file.write('RenderEngine=%d\n' % (cfg.ray_tracing))
1241
            write_color(text_file, 'Bg', cfg.bg_color_1)
2✔
1242
            write_color(text_file, 'Bg', cfg.bg_color_2, 'Top')
2✔
1243
            write_color(text_file, 'SMask', cfg.sm_color)
2✔
1244
            write_color(text_file, 'SPaste', cfg.sp_color)
2✔
1245
            write_color(text_file, 'Silk', cfg.silk_color)
2✔
1246
            write_color(text_file, 'Copper', cfg.copper_color)
2✔
1247
            write_color(text_file, 'BoardBody', cfg.board_color)
2✔
1248
            # VRML options
1249
            text_file.write('VrmlExportUnit=%d\n' % (cfg.units))
2✔
1250
            text_file.write('VrmlExportCopyFiles=%d\n' % (cfg.copy_3d_models))
2✔
1251
            text_file.write('VrmlUseRelativePaths=%d\n' % (cfg.use_relative_paths))
2✔
1252
            text_file.write('VrmlRefUnits=%d\n' % (cfg.ref_units))
2✔
1253
            text_file.write('VrmlRefX=%f\n' % (cfg.ref_x))
2✔
1254
            text_file.write('VrmlRefY=%f\n' % (cfg.ref_y))
2✔
1255
            text_file.write('VrmlUsePlainPCB=0\n')
2✔
1256
    # shutil.copy2(cfg.conf_pcbnew, '/tmp/generated')
1257

1258

1259
def load_pcb(fname):
6✔
1260
    import pcbnew
6✔
1261
    logger.info('Loading '+fname)
6✔
1262
    try:
6✔
1263
        board = pcbnew.LoadBoard(fname)
6✔
1264
    except OSError as e:
6✔
1265
        logger.error('Error loading PCB file. Corrupted?')
6✔
1266
        logger.error(e)
6✔
1267
        exit(CORRUPTED_PCB)
6✔
1268
    return board
6✔
1269

1270

1271
def process_drc_out(cfg):
6✔
1272
    error_level = 0
6✔
1273
    drc_errors, unconnected_pads = parse_drc(cfg)
6✔
1274
    logger.debug('Found {} DRC errors and {} unconnected pads/warnings'.format(drc_errors, unconnected_pads))
6✔
1275
    # Apply filters
1276
    skip_err, skip_unc = apply_filters(cfg, 'DRC error/s', 'unconnected pad/s or warnings')
6✔
1277
    drc_errors = drc_errors-skip_err
6✔
1278
    unconnected_pads = unconnected_pads-skip_unc
6✔
1279
    if drc_errors == 0 and unconnected_pads == 0:
6✔
1280
        logger.info('No errors')
6✔
1281
    else:
1282
        logger.error('Found {} DRC errors and {} unconnected pad/s or warnings'.format(drc_errors, unconnected_pads))
6✔
1283
        list_errors(cfg)
6✔
1284
        if args.ignore_unconnected:
6✔
1285
            unconnected_pads = 0
6✔
1286
        else:
1287
            list_warnings(cfg)
6✔
1288
        error_level = -(drc_errors+unconnected_pads)
6✔
1289
    return error_level
6✔
1290

1291

1292
def parse_one_color(color):
6✔
1293
    match = re.match(r'#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})?', color)
6✔
1294
    if match is None:
6✔
1295
        logger.error('Malformed color: `{}` please use `#RRGGBBAA` where RR, GG, BB and AA are hexadecimal numbers.')
×
1296
        logger.error('AA is the transparency (FF opaque) and is optional. Only supported by KiCad 6.')
×
1297
        exit(WRONG_ARGUMENTS)
×
1298
    res = match.groups()
6✔
1299
    alpha = 1.0
6✔
1300
    if res[3] is not None:
6✔
1301
        alpha = int(res[3], 16)/255.0
6✔
1302
    return (int(res[0], 16)/255.0, int(res[1], 16)/255.0, int(res[2], 16)/255.0, alpha)
6✔
1303

1304

1305
def parse_color(color):
6✔
1306
    res = color[0].split(',')
6✔
1307
    if len(res) == 2 and res[0][0] == '#' and res[1][0] == '#':
6✔
1308
        c1 = parse_one_color(res[0])
×
1309
        c2 = parse_one_color(res[1])
×
1310
        return c1+c2
×
1311
    return parse_one_color(color[0])
6✔
1312

1313

1314
def wait_pcbnew_start_by_msg(cfg):
6✔
1315
    wait_start_by_msg(cfg)
3✔
1316
    # Make sure pcbnew has the focus, I saw problems with WM pop-ups getting the focus
1317
    return wait_pcbnew()
3✔
1318

1319

1320
if __name__ == '__main__':  # noqa: C901
6✔
1321
    parser = argparse.ArgumentParser(description='KiCad PCB automation')
6✔
1322
    subparsers = parser.add_subparsers(help='Command:', dest='command')
6✔
1323

1324
    # short commands: iIrmnsSvVw
1325
    parser.add_argument('--disable_interposer', '-I', help='Avoid using the interposer lib', action='store_true')
6✔
1326
    parser.add_argument('--info', '-n', help='Show information about the installation', action=ShowInfoAction, nargs=0)
6✔
1327
    parser.add_argument('--interposer_sniff', '-i', help="Log interposer info, but don't use it", action='store_true')
6✔
1328
    parser.add_argument('--record', '-r', help='Record the UI automation', action='store_true')
6✔
1329
    parser.add_argument('--rec_width', help='Record width ['+str(REC_W)+']', type=int, default=REC_W)
6✔
1330
    parser.add_argument('--rec_height', help='Record height ['+str(REC_H)+']', type=int, default=REC_H)
6✔
1331
    parser.add_argument('--separate_info', '-S', help='Send info debug level to stdout', action='store_true')
6✔
1332
    parser.add_argument('--start_x11vnc', '-s', help='Start x11vnc (debug)', action='store_true')
6✔
1333
    parser.add_argument('--use_wm', '-m', help='Use a window manager (fluxbox)', action='store_true')
6✔
1334
    parser.add_argument('--verbose', '-v', action='count', default=0)
6✔
1335
    parser.add_argument('--version', '-V', action='version', version='%(prog)s '+__version__+' - ' +
6✔
1336
                        __copyright__+' - License: '+__license__)
1337
    parser.add_argument('--wait_key', '-w', help='Wait for key to advance (debug)', action='store_true')
6✔
1338
    parser.add_argument('--wait_start', help='Timeout to pcbnew start ['+str(WAIT_START)+']', type=int, default=WAIT_START)
6✔
1339
    parser.add_argument('--time_out_scale', help='Timeout multiplier, affects most timeouts',
6✔
1340
                        type=float, default=TIME_OUT_MULT)
1341

1342
    # short commands: cflmMopsStv
1343
    export_parser = subparsers.add_parser('export', help='Export PCB layers')
6✔
1344
    export_parser.add_argument('--color_theme', '-c', nargs=1, help='KiCad 6 color theme (i.e. _builtin_default, user)',
6✔
1345
                               default=['_builtin_classic'])
1346
    export_parser.add_argument('--fill_zones', '-f', help='Fill all zones before printing', action='store_true')
6✔
1347
    export_parser.add_argument('--list', '-l', help='Print a list of layers in LIST PCB and exit', nargs=1, action=ListLayers)
6✔
1348
    export_parser.add_argument('--monochrome', '-m', help='Print in blanck and white', action='store_true')
6✔
1349
    export_parser.add_argument('--mirror', '-M', help='Print mirrored', action='store_true')
6✔
1350
    export_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['printed.pdf'])
6✔
1351
    export_parser.add_argument('--pads', '-p', nargs=1, help='Pads style (0 none, 1 small, 2 full)', default=[2])
6✔
1352
    export_parser.add_argument('--scaling', '-s', nargs=1, help='Scale factor (0 fit page)', default=[1.0])
6✔
1353
    export_parser.add_argument('--separate', '-S', help='Layers in separated sheets', action='store_true')
6✔
1354
    export_parser.add_argument('--no-title', '-t', help='Remove the title-block', action='store_true')
6✔
1355
    export_parser.add_argument('--svg', '-v', help='SVG output instead of PDF', action='store_true')
6✔
1356
    export_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1357
    export_parser.add_argument('output_dir', help='Output directory')
6✔
1358
    export_parser.add_argument('layers', nargs='+', help='Which layers to include')
6✔
1359

1360
    # short commands: fFios
1361
    drc_parser = subparsers.add_parser('run_drc', help='Run Design Rules Checker on a PCB')
6✔
1362
    drc_parser.add_argument('--errors_filter', '-f', nargs=1, help='File with filters to exclude errors')
6✔
1363
    drc_parser.add_argument('--force_gui', '-F', help='Force the use of the GUI (KiCad 6)', action='store_true')
6✔
1364
    drc_parser.add_argument('--ignore_unconnected', '-i', help='Ignore unconnected paths', action='store_true')
6✔
1365
    drc_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['drc_result.rpt'])
6✔
1366
    drc_parser.add_argument('--save', '-s', help='Save after DRC (updating filled zones)', action='store_true')
6✔
1367
    drc_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1368
    drc_parser.add_argument('output_dir', help='Output directory')
6✔
1369

1370
    # short commands: bcdBoOrSTuvVwWxXyYzZ
1371
    v3d_parser = subparsers.add_parser('3d_view', help='Capture the 3D view')
6✔
1372
    v3d_parser.add_argument('--bg_color_1', '-b', nargs=1, help='Background color 1', default=['#66667F'])
6✔
1373
    v3d_parser.add_argument('--bg_color_2', '-B', nargs=1, help='Background color 2', default=['#CCCCE5'])
6✔
1374
    v3d_parser.add_argument('--board_color', nargs=1, help='Board body color', default=['#332B16E6'])
6✔
1375
    v3d_parser.add_argument('--copper_color', '-c', nargs=1, help='Copper color', default=['#B29C00'])
6✔
1376
    v3d_parser.add_argument('--detect_rt', '-d', help='Try to detect when the ray tracing render finshes,'
6✔
1377
                            ' wait_rt value is the time-out (experimental)', action='store_true')
1378
    v3d_parser.add_argument('--dont_clip_silk_on_via_annulus', help="Don't clip silkscreen at via annuli (KiCad 6)",
6✔
1379
                            action='store_true')
1380
    v3d_parser.add_argument('--dont_substrack_mask_from_silk', help="Don't clip silkscreen at solder mask edges (KiCad 6)",
6✔
1381
                            action='store_true')
1382
    v3d_parser.add_argument('--hide_board_body', help='Hide the body of the PCB board (KiCad 6)', action='store_true')
6✔
1383
    v3d_parser.add_argument('--hide_silkscreen', help='Hide the silkscreen layers (KiCad 6)', action='store_true')
6✔
1384
    v3d_parser.add_argument('--hide_soldermask', help='Hide the solder mask layers (KiCad 6)', action='store_true')
6✔
1385
    v3d_parser.add_argument('--hide_solderpaste', help='Hide the solder paste layers (KiCad 6)', action='store_true')
6✔
1386
    v3d_parser.add_argument('--hide_zones', help='Hide filled areas in zones (KiCad 6)', action='store_true')
6✔
1387
    v3d_parser.add_argument('--move_x', '-x', nargs=1, help='Steps to move in the X axis (positive is to the right)',
6✔
1388
                            default=[0], type=int)
1389
    v3d_parser.add_argument('--move_y', '-y', nargs=1, help='Steps to move in the Y axis (positive is up)', default=[0],
6✔
1390
                            type=int)
1391
    v3d_parser.add_argument('--rotate_x', '-X', nargs=1,
6✔
1392
                            help='Steps to rotate around the X axis (positive is clockwise) KiCad 6', default=[0], type=int)
1393
    v3d_parser.add_argument('--rotate_y', '-Y', nargs=1,
6✔
1394
                            help='Steps to rotate around the Y axis (positive is clockwise) KiCad 6', default=[0], type=int)
1395
    v3d_parser.add_argument('--rotate_z', '-Z', nargs=1,
6✔
1396
                            help='Steps to rotate around the Y axis (positive is clockwise) KiCad 6', default=[0], type=int)
1397
    v3d_parser.add_argument('--no_smd', '-S', help='Do not include surface mount components', action='store_true')
6✔
1398
    v3d_parser.add_argument('--no_tht', '-T', help='Do not include through-hole components', action='store_true')
6✔
1399
    v3d_parser.add_argument('--virtual', '-V', help='Include virtual components', action='store_true')
6✔
1400
    v3d_parser.add_argument('--orthographic', '-O', help='Enable the orthographic projection', action='store_true')
6✔
1401
    v3d_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file (PNG)', default=['capture.png'])
6✔
1402
    v3d_parser.add_argument('--ray_tracing', '-r', help='Enable the realistic render', action='store_true')
6✔
1403
    v3d_parser.add_argument('--show_adhesive', help='Show the adhesive layer', action='store_true')
6✔
1404
    v3d_parser.add_argument('--show_comments', help='Show the user comments layer', action='store_true')
6✔
1405
    v3d_parser.add_argument('--show_eco', help='Show the user eco layers', action='store_true')
6✔
1406
    v3d_parser.add_argument('--silk_color', nargs=1,
6✔
1407
                            help='Silk color (KiCad 6 supports color1,color2 for top/bottom)', default=['#E5E5E5'])
1408
    v3d_parser.add_argument('--sm_color', nargs=1, help='Solder mask color (KiCad 6 supports color1,color2 for top/bottom)',
6✔
1409
                            default=['#143324D4'])
1410
    v3d_parser.add_argument('--sp_color', nargs=1, help='Solder paste color', default=['#808080'])
6✔
1411
    v3d_parser.add_argument('--use_layer_colors', help='Use the colors of the layers, not realistic colors (KiCad 6)',
6✔
1412
                            action='store_true')
1413
    v3d_parser.add_argument('--use_rt_wait', '-u', help='Use the ray tracing end detection even on normal renders',
6✔
1414
                            action='store_true')
1415
    v3d_parser.add_argument('--view', '-v', nargs=1, help='Axis view, uppercase is reversed (i.e. Z is bottom)',
6✔
1416
                            default=['z'], choices=['x', 'y', 'z', 'X', 'Y', 'Z'],)
1417
    v3d_parser.add_argument('--wait_after_move', '-W', help='Wait after moving (KiCad 6 and interposer option)',
6✔
1418
                            action='store_true')
1419
    v3d_parser.add_argument('--wait_rt', '-w', nargs=1, help='Seconds to wait for the ray tracing render [5]', default=[5],
6✔
1420
                            type=int)
1421
    v3d_parser.add_argument('--zoom', '-z', nargs=1, help='Zoom steps (use negative to reduce)', default=[0], type=int)
6✔
1422
    v3d_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1423
    v3d_parser.add_argument('output_dir', help='Output directory')
6✔
1424

1425
    # short commands: afnoOu
1426
    export_gencad_parser = subparsers.add_parser('export_gencad', help='Export PCB as GenCAD')
6✔
1427
    export_gencad_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1428
    export_gencad_parser.add_argument('output_dir', help='Output directory')
6✔
1429
    export_gencad_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.cad'])
6✔
1430
    export_gencad_parser.add_argument('--flip_bottom_padstacks', '-f', help='Flip bottom footprint padstacks',
6✔
1431
                                      action='store_true')
1432
    export_gencad_parser.add_argument('--unique_pin_names', '-u', help='Generate unique pin names', action='store_true')
6✔
1433
    export_gencad_parser.add_argument('--no_reuse_shapes', '-n',
6✔
1434
                                      help='Generate a new shape for each footprint instance (Do not reuse shapes)',
1435
                                      action='store_true')
1436
    export_gencad_parser.add_argument('--aux_origin', '-a', help='Use auxiliary axis as origin', action='store_true')
6✔
1437
    export_gencad_parser.add_argument('--save_origin', '-O', help='Save the origin coordinates in the file',
6✔
1438
                                      action='store_true')
1439

1440
    # short commands: afnoOu
1441
    ipc_netlist_parser = subparsers.add_parser('ipc_netlist', help='Create the IPC-D-356 netlist')
6✔
1442
    ipc_netlist_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1443
    ipc_netlist_parser.add_argument('output_dir', help='Output directory')
6✔
1444
    ipc_netlist_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.d356'])
6✔
1445

1446
    # short commands: douUxy
1447
    export_vrml_parser = subparsers.add_parser('export_vrml', help='Export 3D model for the PCB in VRML format')
6✔
1448
    export_vrml_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
6✔
1449
    export_vrml_parser.add_argument('output_dir', help='Output directory')
6✔
1450
    export_vrml_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.wrl'])
6✔
1451
    export_vrml_parser.add_argument('--dir_models', '-d', help='Name of dir to store the footprint models.'
6✔
1452
                                    ' Models will be embedded if not specified.', type=str)
1453
    export_vrml_parser.add_argument('--ref_x', '-x', help='X origin, for user defined. KiCad6: Use center otherwise.',
6✔
1454
                                    type=float)
1455
    export_vrml_parser.add_argument('--ref_y', '-y', help='Y origin, for user defined. KiCad6: Use center otherwise.',
6✔
1456
                                    type=float)
1457
    export_vrml_parser.add_argument('--ref_units', '-u', help='Origin units', default='millimeters', const='millimeters',
6✔
1458
                                    nargs='?', choices=['millimeters', 'inches'])
1459
    export_vrml_parser.add_argument('--units', '-U', help='VRML units', default='millimeters', const='millimeters',
6✔
1460
                                    nargs='?', choices=VRML_UNITS)
1461

1462
    args = parser.parse_args()
6✔
1463
    logger = log.init(args.separate_info)
6✔
1464
    # Set the specified verbosity
1465
    log.set_level(logger, args.verbose)
6✔
1466

1467
    if args.command is None:
6✔
1468
        logger.error('No command selected')
×
1469
        parser.print_help()
×
1470
        exit(WRONG_ARGUMENTS)
×
1471

1472
    cfg = Config(logger, args.kicad_pcb_file, args, is_pcbnew=True)
6✔
1473
    cfg.wait_start = args.wait_start
6✔
1474
    cfg.verbose = args.verbose
6✔
1475
    set_time_out_scale(cfg.time_out_scale)
6✔
1476
    set_time_out_scale_f(cfg.time_out_scale)
6✔
1477
    # Empty values by default, we'll fill them for export
1478
    cfg.fill_zones = False
6✔
1479
    cfg.layers = []
6✔
1480
    cfg.save = args.command == 'run_drc' and args.save
6✔
1481
    cfg.input_file = args.kicad_pcb_file
6✔
1482

1483
    # Get local versions for the GTK window names
1484
    gettext.textdomain('gtk30')
6✔
1485
    cfg.select_a_filename = gettext.gettext('Select a filename')
6✔
1486
    cfg.print_dlg_name = gettext.gettext('Print')
6✔
1487
    logger.debug('Select a filename -> '+cfg.select_a_filename)
6✔
1488
    logger.debug('Print -> '+cfg.print_dlg_name)
6✔
1489

1490
    # Force english + UTF-8
1491
    en_locale = get_en_locale(logger)
6✔
1492
    os.environ['LC_ALL'] = en_locale
6✔
1493
    # Force english for GTK dialogs
1494
    os.environ['LANGUAGE'] = 'en'
6✔
1495
    # Force Mesa software rendering (otherwise, the 3D viewer may crash)
1496
    os.environ['LIBGL_ALWAYS_SOFTWARE'] = "1"
6✔
1497
    if DEBUG_KICAD:
6✔
1498
        os.environ['MESA_DEBUG'] = "1"
×
1499
        os.environ['MESA_LOG_FILE'] = "/tmp/mesa.log"
×
1500
        os.environ['MESA_NO_ERROR'] = "1"
×
1501
    # Make sure the input file exists and has an extension
1502
    check_input_file(cfg, NO_PCB, WRONG_PCB_NAME)
6✔
1503
    if args.command in ['run_drc', 'export']:
6✔
1504
        cfg.board = load_pcb(cfg.input_file)
6✔
1505
        if not cfg.save:
6✔
1506
            memorize_pcb(cfg)
6✔
1507
    else:
1508
        cfg.board = None
6✔
1509

1510
    if args.command == 'export':
6✔
1511
        # Read the layer names from the PCB
1512
        cfg.fill_zones = args.fill_zones
6✔
1513
        cfg.layers = args.layers
6✔
1514
        try:
6✔
1515
            cfg.scaling = float(args.scaling[0])
6✔
1516
        except ValueError:
6✔
1517
            logger.error('Scaling must be a floating point value')
6✔
1518
            exit(WRONG_ARGUMENTS)
6✔
1519
        try:
6✔
1520
            cfg.pads = int(args.pads[0])
6✔
1521
        except ValueError:
6✔
1522
            logger.error('Pads style must be an integer value')
6✔
1523
            exit(WRONG_ARGUMENTS)
6✔
1524
        if cfg.pads < 0 or cfg.pads > 2:
6✔
1525
            logger.error('Pad style must be 0, 1 or 2')
6✔
1526
            exit(WRONG_ARGUMENTS)
6✔
1527
        cfg.no_title = args.no_title
6✔
1528
        cfg.monochrome = args.monochrome
6✔
1529
        cfg.separate = args.separate
6✔
1530
        cfg.svg = args.svg
6✔
1531
        cfg.color_theme = args.color_theme[0]
6✔
1532
        cfg.mirror = args.mirror
6✔
1533
        if args.mirror and cfg.ki5:
6✔
1534
            logger.warning("KiCad 5 doesn't support setting mirror print from the configuration file")
2✔
1535
    else:
1536
        cfg.scaling = 1.0
6✔
1537
        cfg.pads = 2
6✔
1538
        cfg.no_title = False
6✔
1539
        cfg.monochrome = False
6✔
1540
        cfg.separate = False
6✔
1541
        cfg.mirror = False
6✔
1542
        cfg.color_theme = '_builtin_classic'
6✔
1543

1544
    if args.command == 'export_gencad':
6✔
1545
        cfg.flip_bottom_padstacks = args.flip_bottom_padstacks
6✔
1546
        cfg.unique_pin_names = args.unique_pin_names
6✔
1547
        cfg.no_reuse_shapes = args.no_reuse_shapes
6✔
1548
        cfg.aux_origin = args.aux_origin
6✔
1549
        cfg.save_origin = args.save_origin
6✔
1550

1551
    if args.command == '3d_view':
6✔
1552
        cfg.zoom = int(args.zoom[0])
6✔
1553
        cfg.view = args.view[0]
6✔
1554
        cfg.no_tht = args.no_tht
6✔
1555
        cfg.no_smd = args.no_smd
6✔
1556
        cfg.no_virtual = not args.virtual
6✔
1557
        cfg.move_x = args.move_x[0]
6✔
1558
        cfg.move_y = args.move_y[0]
6✔
1559
        cfg.rotate_x = args.rotate_x[0]
6✔
1560
        cfg.rotate_y = args.rotate_y[0]
6✔
1561
        cfg.rotate_z = args.rotate_z[0]
6✔
1562
        cfg.ray_tracing = args.ray_tracing
6✔
1563
        cfg.wait_rt = args.wait_rt[0]
6✔
1564
        cfg.detect_rt = args.detect_rt
6✔
1565
        cfg.bg_color_1 = parse_color(args.bg_color_1)
6✔
1566
        cfg.bg_color_2 = parse_color(args.bg_color_2)
6✔
1567
        cfg.board_color = parse_color(args.board_color)
6✔
1568
        cfg.copper_color = parse_color(args.copper_color)
6✔
1569
        cfg.silk_color = parse_color(args.silk_color)
6✔
1570
        cfg.sm_color = parse_color(args.sm_color)
6✔
1571
        cfg.sp_color = parse_color(args.sp_color)
6✔
1572
        cfg.orthographic = args.orthographic
6✔
1573
        cfg.use_rt_wait = args.use_rt_wait
6✔
1574
        cfg.wait_after_move = args.wait_after_move
6✔
1575
        cfg.hide_silkscreen = args.hide_silkscreen
6✔
1576
        cfg.hide_soldermask = args.hide_soldermask
6✔
1577
        cfg.hide_solderpaste = args.hide_solderpaste
6✔
1578
        cfg.hide_zones = args.hide_zones
6✔
1579
        cfg.dont_substrack_mask_from_silk = args.dont_substrack_mask_from_silk
6✔
1580
        cfg.dont_clip_silk_on_via_annulus = args.dont_clip_silk_on_via_annulus
6✔
1581
        cfg.use_layer_colors = args.use_layer_colors
6✔
1582
        cfg.hide_board_body = args.hide_board_body
6✔
1583
        cfg.show_comments = args.show_comments
6✔
1584
        cfg.show_eco = args.show_eco
6✔
1585
        cfg.show_adhesive = args.show_adhesive
6✔
1586
    else:
1587
        cfg.no_tht = False
6✔
1588
        cfg.no_smd = False
6✔
1589
        cfg.no_virtual = True
6✔
1590
        cfg.ray_tracing = False
6✔
1591
        cfg.bg_color_1 = cfg.bg_color_2 = cfg.board_color = None
6✔
1592
        cfg.copper_color = cfg.silk_color = cfg.sm_color = cfg.sp_color = None
6✔
1593
        cfg.wait_after_move = False
6✔
1594

1595
    if args.command == 'export_vrml':
6✔
1596
        cfg.copy_3d_models = args.dir_models is not None
6✔
1597
        cfg.origin_mode = 1 if args.ref_x is None and args.ref_y is None else 0
6✔
1598
        cfg.ref_units = 1 if args.ref_units == 'inches' else 0
6✔
1599
        cfg.ref_x = 0.0 if args.ref_x is None else args.ref_x
6✔
1600
        cfg.ref_y = 0.0 if args.ref_y is None else args.ref_y
6✔
1601
        cfg.units = VRML_UNITS.index(args.units)
6✔
1602
        cfg.use_relative_paths = True
6✔
1603
        cfg.dir_models = '' if args.dir_models is None else args.dir_models
6✔
1604
    else:
1605
        cfg.use_relative_paths = cfg.copy_3d_models = False
6✔
1606
        cfg.units = cfg.origin_mode = cfg.ref_units = 0
6✔
1607
        cfg.ref_x = cfg.ref_y = 0.0
6✔
1608
        cfg.dir_models = ''
6✔
1609

1610
    if args.command == 'run_drc' and args.errors_filter:
6✔
1611
        load_filters(cfg, args.errors_filter[0])
×
1612

1613
    memorize_project(cfg)
6✔
1614
    # Back-up the current pcbnew configuration
1615
    check_kicad_config_dir(cfg)
6✔
1616
    cfg.conf_pcbnew_bkp = backup_config('PCBnew', cfg.conf_pcbnew, PCBNEW_CFG_PRESENT, cfg)
6✔
1617
    if cfg.conf_colors:
6✔
1618
        cfg.conf_colors_bkp = backup_config('Colors', cfg.conf_colors, PCBNEW_CFG_PRESENT, cfg)
4✔
1619
    if cfg.conf_3dview:
6✔
1620
        cfg.conf_3dview_bkp = backup_config('3D Viewer', cfg.conf_3dview, PCBNEW_CFG_PRESENT, cfg)
4✔
1621
    # Create a suitable configuration
1622
    create_pcbnew_config(cfg)
6✔
1623
    # Hotkeys
1624
    if not cfg.ki5:
6✔
1625
        # KiCad 6 breaks menu short-cuts, but we can configure user hotkeys
1626
        # Back-up the current user.hotkeys configuration
1627
        cfg.conf_hotkeys_bkp = backup_config('User hotkeys', cfg.conf_hotkeys, USER_HOTKEYS_PRESENT, cfg)
4✔
1628
        # Create a suitable configuration
1629
        create_user_hotkeys(cfg)
4✔
1630
    # Back-up the current kicad_common configuration
1631
    cfg.conf_kicad_bkp = backup_config('KiCad common', cfg.conf_kicad, KICAD_CFG_PRESENT, cfg)
6✔
1632
    # Create a suitable configuration
1633
    create_kicad_config(cfg)
6✔
1634
    # Make sure the user has fp-lib-table
1635
    check_lib_table(cfg.user_fp_lib_table, cfg.sys_fp_lib_table)
6✔
1636
    # Create output dir, compute full name for output file and remove it
1637
    output_dir = os.path.abspath(args.output_dir)
6✔
1638
    cfg.video_dir = cfg.output_dir = output_dir
6✔
1639
    os.makedirs(output_dir, exist_ok=True)
6✔
1640
    # Remove the output file
1641
    output_file = os.path.join(output_dir, args.output_name[0])
6✔
1642
    if os.path.exists(output_file):
6✔
1643
        os.remove(output_file)
6✔
1644
    cfg.output_file = output_file
6✔
1645
    # Name for the video
1646
    cfg.video_name = 'pcbnew_'+args.command+'_screencast.ogv'
6✔
1647
    # Interposer settings
1648
    check_interposer(args, logger, cfg)
6✔
1649
    # When using the interposer inform the output file name using the environment
1650
    setup_interposer_filename(cfg)
6✔
1651
    #
1652
    # Do all the work
1653
    #
1654
    error_level = 0
6✔
1655
    if args.command == 'run_drc' and not cfg.ki5 and not args.force_gui:
6✔
1656
        # First command to migrate to Python!
1657
        run_drc_python(cfg)
4✔
1658
        error_level = process_drc_out(cfg)
4✔
1659
        do_retry = False
4✔
1660
        cfg.use_interposer = cfg.enable_interposer = False
4✔
1661
    else:
1662
        flog_out, flog_err, cfg.flog_int = get_log_files(output_dir, 'pcbnew', also_interposer=cfg.enable_interposer)
6✔
1663
        if cfg.enable_interposer:
6✔
1664
            flog_out = subprocess.PIPE
3✔
1665
            atexit.register(dump_interposer_dialog, cfg)
3✔
1666
        for retry in range(3):
6✔
1667
            do_retry = False
6✔
1668
            with recorded_xvfb(cfg, retry):
6✔
1669
                logger.debug('Starting '+cfg.pcbnew)
6✔
1670
                if DEBUG_KICAD_NG:
6✔
1671
                    os.environ['LD_LIBRARY_PATH'] = '/usr/lib/kicad-nightly/lib/x86_64-linux-gnu/:/usr/lib/kicad-nightly/lib/'
×
1672
                    os.environ['KICAD_PATH'] = '/usr/share/kicad-nightly'
×
1673
                    cmd = ['gdb', '-batch', '-n', '-ex', 'set pagination off', '-ex', 'run', '-ex', 'bt',
×
1674
                           '-ex', 'bt full', '-ex', 'thread apply all bt full', '--args', '/usr/lib/kicad-nightly/bin/pcbnew',
1675
                           cfg.input_file]
1676
                elif DEBUG_KICAD:
6✔
1677
                    cmd = ['gdb', '-batch', '-n', '-ex', 'set pagination off', '-ex', 'run', '-ex', 'bt', '-ex',
×
1678
                           'set new-console on', '-ex', 'bt full', '-ex', 'thread apply all bt full', '--args',
1679
                           '/usr/bin/pcbnew', cfg.input_file]
1680
                else:
1681
                    cmd = [cfg.pcbnew, cfg.input_file]
6✔
1682
                use_low_level_io = False
6✔
1683
                if args.command == 'export' and cfg.use_interposer:
6✔
1684
                    # We need a file to save the print options, make it unique to avoid collisions
1685
                    create_interposer_print_options_file(cfg)
3✔
1686
                    use_low_level_io = True
3✔
1687
                os.environ['KIAUTO_INTERPOSER_LOWLEVEL_IO'] = '1' if use_low_level_io else ''
6✔
1688
                logger.info('Starting pcbnew ...')
6✔
1689
                # bufsize=1 is line buffered
1690
                with PopenContext(cmd, stderr=flog_err, close_fds=True, bufsize=1, text=True,
6✔
1691
                                  stdout=flog_out, start_new_session=True) as pcbnew_proc:
1692
                    # Avoid patching our childs
1693
                    os.environ['LD_PRELOAD'] = ''
6✔
1694
                    cfg.pcbnew_pid = pcbnew_proc.pid
6✔
1695
                    set_kicad_process(cfg, pcbnew_proc.pid)
6✔
1696
                    cfg.popen_obj = pcbnew_proc
6✔
1697
                    start_queue(cfg)
6✔
1698
                    id_pcbnew = wait_pcbnew_start_by_msg(cfg) if cfg.use_interposer else wait_pcbew_start(cfg)
6✔
1699
                    if pcbnew_proc.poll() is not None:
6✔
1700
                        do_retry = True
×
1701
                    else:
1702
                        if args.command == 'export':
6✔
1703
                            # TODO: Implement KiCad 7 cli version.
1704
                            # Currently useless since PDF is always B/W and KiBot has a much faster and better print
1705
                            # KiCad 7 bug: https://gitlab.com/kicad/code/kicad/-/issues/13805
1706
                            print_layers(cfg, id_pcbnew)
6✔
1707
                        elif args.command == '3d_view':
6✔
1708
                            capture_3d_view(cfg)
6✔
1709
                        elif args.command == 'export_gencad':
6✔
1710
                            export_gencad(cfg)
6✔
1711
                        elif args.command == 'ipc_netlist':
6✔
1712
                            ipc_netlist(cfg)
6✔
1713
                        elif args.command == 'export_vrml':
6✔
1714
                            export_vrml(cfg)
6✔
1715
                        else:  # run_drc
1716
                            run_drc(cfg)
6✔
1717
                            error_level = process_drc_out(cfg)
6✔
1718
            if not do_retry:
6✔
1719
                break
6✔
1720
            logger.warning("Pcbnew failed to start retrying ...")
×
1721
    if do_retry:
6✔
1722
        logger.error("Pcbnew failed to start try with --time_out_scale")
×
1723
        error_level = PCBNEW_ERROR
×
1724
    #
1725
    # Exit clean-up
1726
    #
1727
    # The following code is here only to make coverage tool properly meassure atexit code.
1728
    if not cfg.save and args.command in ['run_drc', 'export']:
6✔
1729
        atexit.unregister(restore_pcb)
6✔
1730
        restore_pcb(cfg)
6✔
1731
    atexit.unregister(restore_config)
6✔
1732
    restore_config(cfg)
6✔
1733
    atexit.unregister(restore_project)
6✔
1734
    restore_project(cfg)
6✔
1735
    # We dump the dialog only on abnormal situations
1736
    if cfg.use_interposer:
6✔
1737
        logger.debug('Removing interposer dialog ({})'.format(cfg.flog_int.name))
3✔
1738
        atexit.unregister(dump_interposer_dialog)
3✔
1739
        cfg.flog_int.close()
3✔
1740
        os.remove(cfg.flog_int.name)
3✔
1741
    exit(error_level)
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc