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

INTI-CMNB / KiAuto / 13396140335

18 Feb 2025 05:05PM UTC coverage: 80.17% (-0.5%) from 80.677%
13396140335

push

github

set-soft
[PCBNew][KiCad 9][Added] Support for gencad CLI export

1 of 15 new or added lines in 1 file covered. (6.67%)

2 existing lines in 1 file now uncovered.

2074 of 2587 relevant lines covered (80.17%)

3.8 hits per line

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

80.82
/src/pcbnew_do
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
# Copyright (c) 2020-2025 Salvador E. Tropea
4
# Copyright (c) 2020-2025 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
"""
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
8✔
21
import atexit
8✔
22
import gettext
8✔
23
import json
8✔
24
import os
8✔
25
import psutil
8✔
26
import re
8✔
27
import shutil
8✔
28
import subprocess
8✔
29
import sys
8✔
30
from tempfile import TemporaryDirectory
8✔
31
from time import (asctime, localtime, sleep)
8✔
32
import time
8✔
33

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

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

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

68

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

111
    return int(drc_errors), int(unconnected_pads)
2✔
112

113

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

161
    return len(drc_errors), len(drc_warns)
4✔
162

163

164
def parse_item(v, units):
8✔
165
    severity = v['severity']
2✔
166
    type = v['type']
2✔
167
    desc = v['description']
2✔
168
    txt = ''
2✔
169
    for ln in v['items']:
2✔
170
        x = ln['pos']['x']
2✔
171
        y = ln['pos']['y']
2✔
172
        dsc = ln['description']
2✔
173
        txt += f'    @({x} {units}, {y} {units}): {dsc}\n'
2✔
174
    return severity, type, desc, txt
2✔
175

176

177
def render_items(items, units):
8✔
178
    rpt = ''
2✔
179
    for v in items:
2✔
180
        excluded = ' (excluded)' if v.get('excluded') else ''
2✔
181
        severity, type, desc, context = parse_item(v, units)
2✔
182
        rpt += f'[{type}]: {desc}\n'
2✔
183
        rpt += f'    ; {severity}{excluded}\n'
2✔
184
        rpt += context
2✔
185
    rpt += '\n'
2✔
186
    return rpt
2✔
187

188

189
def parse_drc_json(cfg, lines):
8✔
190
    js_txt = '\n'.join(lines)
2✔
191
    if log.get_level() > 2:
2✔
192
        logger.debug(js_txt)
2✔
193
    data = json.loads(js_txt)
2✔
194
    if data['$schema'] != 'https://schemas.kicad.org/drc.v1.json':
2✔
195
        logger.warning('Unknown JSON schema, DRC might fail')
×
196
    units = data['coordinate_units']
2✔
197

198
    # Reconstruct the report
199
    rpt = f'** Drc report for {os.path.basename(cfg.input_file)} **\n'
2✔
200
    rpt += f"** Created on {data['date']} **\n\n"
2✔
201

202
    violations = data['violations']
2✔
203
    rpt += f'** Found {len(violations)} DRC violations **\n'
2✔
204
    rpt += render_items(violations, units)
2✔
205

206
    unconnected_items = data['unconnected_items']
2✔
207
    rpt += f'** Found {len(unconnected_items)} unconnected pads **\n'
2✔
208
    rpt += render_items(unconnected_items, units)
2✔
209

210
    schematic_parity = data['schematic_parity']
2✔
211
    rpt += f'** Found {len(schematic_parity)} Footprint errors **\n'
2✔
212
    rpt += render_items(schematic_parity, units)
2✔
213

214
    rpt += '** End of Report **\n'
2✔
215

216
    with open(cfg.output_file, 'wt') as f:
2✔
217
        f.write(rpt)
2✔
218
    # Adapt the data
219
    cfg.errs = []
2✔
220
    cfg.wrns = []
2✔
221
    for v in violations+schematic_parity:
2✔
222
        if v.get('excluded'):
2✔
223
            continue
2✔
224
        severity, type, desc, context = parse_item(v, units)
2✔
225
        txt = f'({type}) {desc}; Severity: {severity}\n{context}'
2✔
226
        if severity == 'error':
2✔
227
            cfg.errs.append(txt)
2✔
228
        else:
×
229
            cfg.wrns.append(txt)
×
230
    for v in unconnected_items:
2✔
231
        if v.get('excluded'):
2✔
232
            continue
×
233
        severity, type, desc, context = parse_item(v, units)
2✔
234
        cfg.wrns.append(f'({type}) {desc}; Severity: warning\n{context}')
2✔
235

236
    return len(cfg.errs), len(cfg.wrns)
2✔
237

238

239
def parse_drc(cfg):
8✔
240
    with open(cfg.output_file, 'rt') as f:
8✔
241
        lines = f.read().splitlines()
8✔
242
    if cfg.ki5:
8✔
243
        return parse_drc_ki5(lines)
2✔
244
    elif cfg.ki8:
6✔
245
        return parse_drc_json(cfg, lines)
2✔
246
    else:
×
247
        return parse_drc_ki6(cfg, lines)
4✔
248

249

250
def dismiss_already_running():
8✔
251
    # The "Confirmation" modal pops up if pcbnew is already running
252
    nf_title = TITLE_CONFIRMATION
1✔
253
    wait_for_window(nf_title, nf_title, 1)
1✔
254

255
    logger.info('Dismiss pcbnew already running')
1✔
256
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
1✔
257
    logger.debug('Found, sending Return')
1✔
258
    xdotool(['key', 'Return'])
1✔
259
    logger.debug('Wait a little, this dialog is slow')
1✔
260
    sleep(5)
1✔
261

262

263
def dismiss_warning():  # pragma: no cover
8✔
264
    nf_title = TITLE_WARNING
×
265
    wait_for_window(nf_title, nf_title, 1)
×
266

267
    logger.error('Dismiss pcbnew warning, will fail')
×
268
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
×
269
    xdotool(['key', 'Return'])
×
270

271

272
def dismiss_file_open_error():
8✔
273
    nf_title = TITLE_FILE_OPEN_ERROR
3✔
274
    wait_for_window(nf_title, nf_title, 1)
3✔
275

276
    logger.warning('This file is already opened ({})'.format(cfg.input_file))
3✔
277
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
3✔
278
    xdotool(['key', 'Left', 'Return'])
3✔
279

280

281
def dismiss_error():
8✔
282
    nf_title = TITLE_ERROR
×
283
    wait_for_window(nf_title, nf_title, 1)
×
284

285
    logger.debug('Dismiss pcbnew error')
×
286
    xdotool(['search', '--onlyvisible', '--name', nf_title, 'windowfocus'])
×
287
    logger.debug('Found, sending Return')
×
288
    xdotool(['key', 'Return'])
×
289

290

291
def wait_pcbnew(time=10, others=None):
8✔
292
    return wait_for_window('Main pcbnew window', cfg.pn_window_title, time, others=others, popen_obj=cfg.popen_obj)
8✔
293

294

295
def wait_pcbew_start(cfg):
8✔
296
    failed_focuse = False
4✔
297
    other = None
4✔
298
    try:
4✔
299
        id = wait_pcbnew(cfg.wait_start, [TITLE_CONFIRMATION, TITLE_WARNING, TITLE_ERROR, TITLE_FILE_OPEN_ERROR])
4✔
300
    except RuntimeError:  # pragma: no cover
4✔
301
        logger.debug('Time-out waiting for pcbnew, will retry')
×
302
        failed_focuse = True
×
303
    except ValueError as err:
4✔
304
        other = str(err)
4✔
305
        logger.debug('Found "'+other+'" window instead of pcbnew')
4✔
306
        failed_focuse = True
4✔
307
    except subprocess.CalledProcessError:
×
308
        logger.debug('Pcbnew is no longer running (returned {})'.format(cfg.popen_obj.poll()))
×
309
        id = [0]
×
310
    if failed_focuse:
4✔
311
        wait_point(cfg)
4✔
312
        if other == TITLE_ERROR:
4✔
313
            dismiss_error()
×
314
            logger.error('pcbnew reported an error')
×
315
            exit(PCBNEW_ERROR)
×
316
        elif other == TITLE_CONFIRMATION:
4✔
317
            dismiss_already_running()
1✔
318
        elif other == TITLE_WARNING:  # pragma: no cover
3✔
319
            dismiss_warning()
×
320
        elif other == TITLE_FILE_OPEN_ERROR:
3✔
321
            dismiss_file_open_error()
3✔
322
        try:
4✔
323
            id = wait_pcbnew(5)
4✔
324
        except RuntimeError:  # pragma: no cover
×
325
            logger.error('Time-out waiting for pcbnew, giving up')
×
326
            exit(PCBNEW_ERROR)
×
327
    if len(id) > 1:
4✔
328
        logger.error('More than one PCBNew windows detected, one could be a misnamed error dialog')
×
329
        exit(PCBNEW_ERROR)
×
330
    return id[0]
4✔
331

332

333
def exit_pcbnew(cfg):
8✔
334
    # Wait until the dialog is closed, useful when more than one file are created
335
    id = wait_pcbnew(10)
4✔
336

337
    send_keys(cfg, 'Exiting pcbnew', 'ctrl+q')
4✔
338
    try:
4✔
339
        wait_not_focused(id[0], 5)
4✔
340
    except RuntimeError:  # pragma: no cover
4✔
341
        logger.debug('PCBnew not exiting, will retry')
4✔
342
        pass
4✔
343
    # Dismiss any dialog. I.e. failed to write the project
344
    # Note: if we modified the PCB KiCad will ask for save using a broken dialog.
345
    #       It doesn't have a name and only gets focus with a WM.
346
    logger.info('Retry pcbnew exit')
4✔
347
    wait_point(cfg)
4✔
348
    xdotool(['key', 'Return', 'ctrl+q'])
4✔
349
    try:
4✔
350
        wait_not_focused(id[0], 5)
4✔
351
    except RuntimeError:  # pragma: no cover
×
352
        logger.debug('PCBnew not exiting, will kill')
×
353
        pass
×
354
    # If we failed to exit we will kill it anyways
355
    wait_point(cfg)
4✔
356

357

358
def open_print_dialog(cfg, print_dialog_keys, id_pcbnew):
8✔
359
    # Open the KiCad Print dialog
360
    logger.info('Open File->Print')
4✔
361
    wait_point(cfg)
4✔
362
    xdotool(['key']+print_dialog_keys)
4✔
363
    retry = False
4✔
364
    try:
4✔
365
        # Do a first try with small time-out, perhaps we sent the keys before the window was available
366
        id = wait_for_window('Print dialog', '^Print', skip_id=id_pcbnew, timeout=2)
4✔
367
    except RuntimeError:  # pragma: no cover
×
368
        # Perhaps the fill took too much try again
369
        retry = True
×
370
    # Retry the open dialog
371
    if retry:  # pragma: no cover
4✔
372
        # Excluded from coverage, only happends under conditions hard to reproduce
373
        logger.info('Open File->Print (retrying)')
×
374
        wait_point(cfg)
×
375
        xdotool(['key']+print_dialog_keys)
×
376
        id = wait_for_window('Print dialog', '^Print', skip_id=id_pcbnew)
×
377
    if len(id) == 1:
4✔
378
        # Only 1 window matched, the print dialog
379
        return id[0]
4✔
380
    if len(id) > 2:
1✔
381
        logger.error('Too much windows with similar names')
×
382
        exit(PCBNEW_ERROR)
×
383
    return id[1] if id[0] == id_pcbnew else id[0]
1✔
384

385

386
def print_layers(cfg, id_pcbnew):
8✔
387
    if not cfg.ki5:
8✔
388
        print_dialog_keys = ['ctrl+p']
6✔
389
    else:
×
390
        # We should be able to use Ctrl+P, unless the user configured it
391
        # otherwise. We aren't configuring hotkeys for 5.1 so is better
392
        # to just use the menu accelerators (removed on KiCad 6)
393
        print_dialog_keys = ['alt+f', 'p']
2✔
394
    if cfg.use_interposer:
8✔
395
        return print_layers_i(cfg, id_pcbnew, print_dialog_keys)
4✔
396
    return print_layers_n(cfg, id_pcbnew, print_dialog_keys)
4✔
397

398

399
def fill_zones_i(cfg):
8✔
400
    # Wait for KiCad to be sleeping
401
    wait_kicad_ready_i(cfg)
4✔
402
    # Now we fill the zones
403
    send_keys(cfg, 'Filling zones ...', 'b')
4✔
404
    # Wait fill end and inform the elapsed time (for slow fills)
405
    wait_and_show_progress(cfg, 'GTK:Window Destroy:Fill All Zones', r'(\d:\d\d:\d\d)', '0', 'Elapsed time',
4✔
406
                           skip_match='0:00:00', with_windows=True)
4✔
407

408

409
def print_layers_i(cfg, id_pcbnew, print_dialog_keys):
8✔
410
    # Create a temporal dir for the output file
411
    # GTK is limited to the names we can choose, so we let it create the name and move to the arbitrary one
412
    # Note: we can't control the extension
413
    with TemporaryDirectory() as tmpdir:
4✔
414
        # Pass the target dir, file name and format to the interposer
415
        fname = save_interposer_print_data(cfg, tmpdir, 'interposer', 'pdf' if not cfg.svg else 'svg')
4✔
416
        # Fill zones if the user asked for it
417
        if cfg.fill_zones:
4✔
418
            fill_zones_i(cfg)
4✔
419
        # Open the KiCad print dialog
420
        open_dialog_i(cfg, 'Print', ['key']+print_dialog_keys, extra_msg='KiCad')
4✔
421
        # Open the gtk print dialog
422
        # Big magic 1: we add an accelerator to the Print button, so Alt+P is enough
423
        open_dialog_i(cfg, 'Print', 'alt+p', extra_msg='GTK', no_main=True, no_show=True)
4✔
424
        # Confirm the options
425
        # Big magic 2: the interposer selected the printer, output file name and format, just confirm them
426
        send_keys(cfg, 'Print', 'Return', closes='Print', delay_io=True)
4✔
427
        # Wait until the file is created
428
        wait_create_i(cfg, 'print', fname)
4✔
429
        # Close KiCad Print dialog
430
        send_keys(cfg, 'Close Print dialog', 'Escape', closes='Print')
4✔
431
        # Move the file to the user selected name
432
        shutil.move(fname, cfg.output_file)
4✔
433
    # Exit
434
    exit_kicad_i(cfg)
4✔
435

436

437
def print_layers_n(cfg, id_pcbnew, print_dialog_keys):
8✔
438
    # Fill zones if the user asked for it
439
    if cfg.fill_zones:
4✔
440
        logger.info('Fill zones')
4✔
441
        wait_point(cfg)
4✔
442
        # Make sure KiCad is responding
443
        # We open the dialog and then we close it
444
        id_print_dialog = open_print_dialog(cfg, print_dialog_keys, id_pcbnew)
4✔
445
        xdotool(['key', 'Escape'])
4✔
446
        wait_not_focused(id_print_dialog)
4✔
447
        wait_pcbnew()
4✔
448
        # Now we fill the zones
449
        xdotool(['key', 'b'])
4✔
450
        # Wait for complation
451
        sleep(1)
4✔
452
        id_pcbnew = wait_pcbnew()[0]
4✔
453
    id_print_dialog = open_print_dialog(cfg, print_dialog_keys, id_pcbnew)
4✔
454
    # Open the gtk print dialog
455
    wait_point(cfg)
4✔
456
    # KiCad 5: Two possible options here:
457
    # 1) With WM we usually get "Exclude PCB edge ..." selected
458
    # 2) Without WM we usually get "Color" selected
459
    # In both cases sending 4 Shit+Tab moves us to one of the layer columns.
460
    # From there Return prints and Escape closes the window.
461
    # KiCad 6/7 on Debian 12: we get a button, going right selects the Print button
462
    keys = ['key', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'Right', 'Right', 'Return']
4✔
463
    xdotool(keys)
4✔
464
    # Check it is open
465
    id2 = wait_for_window('Printer dialog', '^(Print|%s)$' % cfg.print_dlg_name, skip_id=id_print_dialog)
4✔
466
    id_printer_dialog = id2[1] if id2[0] == id_print_dialog else id2[0]
4✔
467
    wait_point(cfg)
4✔
468
    # List of printers
469
    xdotool(['key', 'Tab',
4✔
470
             # Go up to the top
471
             'Home',
×
472
             # Output file name
473
             'Tab',
×
474
             # Open dialog
475
             'Return'])
×
476
    id_sel_f = wait_for_window('Select a filename', '(Select a filename|%s)' % cfg.select_a_filename, 2)
4✔
477
    logger.info('Pasting output dir')
4✔
478
    wait_point(cfg)
4✔
479
    text_replace(cfg.output_file)
4✔
480
    xdotool(['key',
4✔
481
             # Select this name
482
             'Return'])
4✔
483
    # Back to print
484
    retry = False
4✔
485
    try:
4✔
486
        wait_not_focused(id_sel_f[0])
4✔
487
    except RuntimeError:
×
488
        retry = True
×
489
    if retry:
4✔
490
        logger.debug('Retrying the Return to select the filename')
×
491
        xdotool(['key', 'Return'])
×
492
        wait_not_focused(id_sel_f[0])
×
493
    wait_for_window('Printer dialog', '^(Print|%s)$' % cfg.print_dlg_name, skip_id=id_print_dialog)
4✔
494
    wait_point(cfg)
4✔
495
    format = 3*['Left'] if not cfg.svg else 3*['Right']
4✔
496
    xdotool(['key',
4✔
497
             # Format options
498
             'Tab'] +
4✔
499
            # Be sure we are at left/right (PDF/SVG)
500
            format +
4✔
501
            # Print it
502
            ['Return'])
4✔
503
    # Wait until the file is created
504
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
4✔
505
    wait_not_focused(id_printer_dialog)
4✔
506
    # Now we should be in the KiCad Print dialog again
507
    wait_for_window('Print dialog', 'Print')
4✔
508
    wait_point(cfg)
4✔
509
    # Close the dialog
510
    # We are in one of the layer columns, here Escape works
511
    xdotool(['key', 'Escape'])
4✔
512
    wait_not_focused(id_print_dialog)
4✔
513
    # Exit
514
    exit_pcbnew(cfg)
4✔
515

516

517
def run_drc_5_1(cfg):
8✔
518
    logger.info('Open Inspect->DRC')
1✔
519
    wait_point(cfg)
1✔
520
    xdotool(['key', 'alt+i', 'd'])
1✔
521

522
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
1✔
523
    # Note: Refill zones on DRC gets saved in ~/.config/kicad/pcbnew as RefillZonesBeforeDrc
524
    # The space here is to enable the report of all errors for tracks
525
    logger.info('Enable reporting all errors for tracks')
1✔
526
    wait_point(cfg)
1✔
527
    xdotool(['key', 'Tab', 'Tab', 'Tab', 'Tab', 'space', 'Tab', 'Tab', 'Tab', 'Tab'])
1✔
528
    logger.info('Pasting output dir')
1✔
529
    wait_point(cfg)
1✔
530
    text_replace(cfg.output_file)
1✔
531
    xdotool(['key', 'Return'])
1✔
532

533
    wait_for_window('Report completed dialog', 'Disk File Report Completed')
1✔
534
    wait_point(cfg)
1✔
535
    xdotool(['key', 'Return'])
1✔
536
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
1✔
537

538
    logger.info('Closing the DRC dialog')
1✔
539
    wait_point(cfg)
1✔
540
    xdotool(['key', 'shift+Tab', 'Return'])
1✔
541
    wait_pcbnew()
1✔
542

543

544
def run_drc_6_0(cfg):
8✔
545
    logger.info('Open Inspect->DRC')
2✔
546
    wait_point(cfg)
2✔
547
    xdotool(['key', RULES_KEY.lower()])
2✔
548
    # Wait dialog
549
    wait_for_window('DRC modal window', cfg.drc_dialog_name)
2✔
550
    # Run the DRC
551
    logger.info('Run DRC')
2✔
552
    wait_point(cfg)
2✔
553
    xdotool(['key', 'Return'])
2✔
554
    #
555
    # To know when KiCad finished we try this:
556
    # - Currently I can see a way, just wait some time
557
    #
558
    sleep(12*cfg.time_out_scale)
2✔
559
    # Save the DRC
560
    logger.info('Open the save dialog')
2✔
561
    wait_point(cfg)
2✔
562
    logger.info('Save DRC')
2✔
563
    wait_point(cfg)
2✔
564
    xdotool(['key', 'shift+Tab', 'shift+Tab', 'shift+Tab', 'shift+Tab',
2✔
565
             'Right', 'Right', 'Right', 'Right', 'Up', 'Return'])
×
566
    # Wait for the save dialog
567
    wait_for_window('DRC File save dialog', 'Save Report to File')
2✔
568
    # Paste the name
569
    logger.info('Pasting output file')
2✔
570
    wait_point(cfg)
2✔
571
    text_replace(cfg.output_file)
2✔
572
    # Wait for report created
573
    logger.info('Wait for DRC file creation')
2✔
574
    wait_point(cfg)
2✔
575
    xdotool(['key', 'Return'])
2✔
576
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
2✔
577
    # Close the dialog
578
    logger.info('Closing the DRC dialog')
2✔
579
    wait_point(cfg)
2✔
580
    xdotool(['key', 'Escape'])
2✔
581
    wait_pcbnew()
2✔
582

583

584
def run_drc_python(cfg):
8✔
585
    logger.debug("Using Python interface instead of running KiCad")
4✔
586
    import pcbnew
4✔
587
    logger.debug("Re-filling zones")
4✔
588
    filler = pcbnew.ZONE_FILLER(cfg.board)
4✔
589
    filler.Fill(cfg.board.Zones())
4✔
590
    if cfg.kicad_version > KICAD_VERSION_6_99 and os.environ.get('KICAD7_FOOTPRINT_DIR') is None:
4✔
591
        # This is most probably a bug in the Python DRC code
592
        os.environ['KICAD7_FOOTPRINT_DIR'] = '/usr/share/kicad/footprints/'
×
593
    logger.debug("Running DRC")
4✔
594
    pcbnew.WriteDRCReport(cfg.board, cfg.output_file, pcbnew.EDA_UNITS_MILLIMETRES, True)
4✔
595
    if cfg.save:
4✔
596
        logger.info('Saving PCB')
4✔
597
        os.rename(cfg.input_file, cfg.input_file + '-bak')
4✔
598
        cfg.board.Save(cfg.input_file)
4✔
599

600

601
def run_drc_cli(cfg, schematic_parity, all_track_errors, units):
8✔
602
    logger.debug("Using kicad-cli instead of running KiCad")
2✔
603
    # Currently not available in the CLI
604
    import pcbnew
2✔
605
    logger.debug("Re-filling zones")
2✔
606
    filler = pcbnew.ZONE_FILLER(cfg.board)
2✔
607
    filler.Fill(cfg.board.Zones())
2✔
608
    cfg.board.Save(cfg.input_file)
2✔
609
    # The save removes the exclusions ...
610
    restore_project(cfg)
2✔
611
    # Run the DRC from CLI
612
    cmd = [cfg.kicad_cli, 'pcb', 'drc', '-o', cfg.output_file, '--format', 'json', '--severity-all', '--units', units]
2✔
613
    if schematic_parity:
2✔
614
        cmd.append('--schematic-parity')
×
615
    if all_track_errors:
2✔
616
        cmd.append('--all-track-errors')
×
617
    cmd.append(cfg.input_file)
2✔
618
    run_command(cmd)
2✔
619

620

621
def run_drc_n(cfg):
8✔
622
    if not cfg.ki5:
3✔
623
        run_drc_6_0(cfg)
2✔
624
    else:
×
625
        run_drc_5_1(cfg)
1✔
626
    # Save the PCB
627
    if cfg.save:
3✔
628
        logger.info('Saving PCB')
1✔
629
        wait_point(cfg)
1✔
630
        os.rename(cfg.input_file, cfg.input_file + '-bak')
1✔
631
        xdotool(['key', 'ctrl+s'])
1✔
632
        logger.info('Wait for PCB file creation')
1✔
633
        wait_point(cfg)
1✔
634
        wait_for_file_created_by_process(cfg.pcbnew_pid, os.path.realpath(cfg.input_file))
1✔
635
    # Exit
636
    exit_pcbnew(cfg)
3✔
637

638

639
def run_drc_5_1_i(cfg):
8✔
640
    control_dlg, _ = open_dialog_i(cfg, cfg.drc_dialog_name, ['key', 'alt+i', 'd'], no_main=True)
1✔
641
    # Enable report all errors for tracks and go to the file name
642
    # Here we added a shortcut for "Report all errors for tracks (slower)"
643
    send_keys(cfg, 'Enable reporting all errors for tracks', ['key', 'alt+r', 'alt+c'])
1✔
644
    paste_output_file_i(cfg)
1✔
645
    # The following dialog indicates the report was finished
646
    # 'Disk File Report Completed' dialog
647
    file_dialog, _ = open_dialog_i(cfg, 'Disk File Report Completed', 'Return', no_show=True, no_main=True)
1✔
648
    send_keys(cfg, 'Close dialog', 'Return', closes=file_dialog)
1✔
649
    # Now close the DRC control
650
    send_keys(cfg, 'Closing the DRC dialog', 'alt+l', closes=control_dlg)
1✔
651

652

653
def run_drc_6_0_i(cfg):
8✔
654
    control_dlg, _ = open_dialog_i(cfg, cfg.drc_dialog_name, RULES_KEY.lower(), no_main=True)
2✔
655
    # Run the DRC
656
    send_keys(cfg, 'Run DRC', 'Return')
2✔
657
    # Wait for the end of the DRC (at the end KiCad restores the Close button)
658
    wait_queue(cfg, 'GTK:Button Label:C_lose')
2✔
659
    wait_kicad_ready_i(cfg)
2✔
660
    # Save the DRC
661
    # We added a short-cut for Save...
662
    file_dialog, _ = open_dialog_i(cfg, 'Save Report to File', 'alt+s')
2✔
663
    # Paste the name
664
    paste_bogus_filename(cfg)
2✔
665
    # Close the report dialog and also wait for the report creation (both in parallel)
666
    send_keys(cfg, 'Create report', 'Return', closes=file_dialog, delay_io=True)
2✔
667
    wait_create_i(cfg, 'DRC report')
2✔
668
    # Close the dialog
669
    # Note: The following command closes the dialog, but on GTK 3.24.34 it looks like
670
    # the dialog isn't destroyed. It seems to be safer to skip the wait.
671
    send_keys(cfg, 'Closing the DRC dialog', 'Escape')  # , closes=control_dlg)
2✔
672

673

674
def run_drc_i(cfg):
8✔
675
    if not cfg.ki5:
3✔
676
        run_drc_6_0_i(cfg)
2✔
677
    else:
×
678
        run_drc_5_1_i(cfg)
1✔
679
    # Save the PCB
680
    if cfg.save:
3✔
681
        os.rename(cfg.input_file, cfg.input_file+'-bak')
1✔
682
        wait_kicad_ready_i(cfg)
1✔
683
        send_keys(cfg, 'Saving PCB', 'ctrl+s')
1✔
684
        wait_create_i(cfg, 'PCB', fn=os.path.realpath(cfg.input_file))
1✔
685
    # Exit
686
    exit_kicad_i(cfg)
3✔
687

688

689
def run_drc(cfg):
8✔
690
    if cfg.use_interposer:
6✔
691
        return run_drc_i(cfg)
3✔
692
    return run_drc_n(cfg)
3✔
693

694

695
def export_gencad(cfg):
8✔
696
    wait_point(cfg)
8✔
697
    if cfg.ki5 or cfg.use_interposer:
8✔
698
        # With the interposer we add the missing accelerators to KiCad 6
699
        keys = ['key', 'alt+f', 'x', 'g']
5✔
700
    else:
×
701
        keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'KP_Space', 'Down', 'Return']
3✔
702
        if cfg.ki7:
3✔
703
            keys.insert(2, 'Down')
2✔
704
    if cfg.use_interposer:
8✔
705
        export_gencad_i(cfg, keys)
4✔
706
    else:
×
707
        export_gencad_n(cfg, keys)
4✔
708

709

710
def export_gencad_cli(cfg):
8✔
NEW
711
    logger.debug("Using kicad-cli instead of running KiCad")
×
712
    # Run the export from CLI
NEW
713
    cmd = [cfg.kicad_cli, 'pcb', 'export', 'gencad', '-o', cfg.output_file]
×
NEW
714
    if cfg.flip_bottom_padstacks:
×
NEW
715
        cmd.append('--flip-bottom-pads')
×
NEW
716
    if cfg.unique_pin_names:
×
NEW
717
        cmd.append('--unique-pins')
×
NEW
718
    if cfg.no_reuse_shapes:
×
NEW
719
        cmd.append('--unique-footprints')
×
NEW
720
    if cfg.aux_origin:
×
NEW
721
        cmd.append('--use-drill-origin')
×
NEW
722
    if cfg.save_origin:
×
NEW
723
        cmd.append('--store-origin-coord')
×
NEW
724
    cmd.append(cfg.input_file)
×
NEW
725
    run_command(cmd)
×
726

727

728
def export_vrml_i(cfg, keys):
8✔
729
    # Open the "VRML Export Options"
730
    dialog, _ = open_dialog_i(cfg, 'VRML Export Options', keys)
4✔
731
    # Paste the name
732
    paste_output_file_i(cfg)
4✔
733
    if cfg.dir_models:
4✔
734
        # Paste the 3D path
735
        send_keys(cfg, 'Changing to 3D path', 'Down')
4✔
736
        wait_kicad_ready_i(cfg)
4✔
737
        paste_text_i(cfg, 'Pasting 3D path', cfg.dir_models)
4✔
738
    # Do it
739
    send_keys(cfg, 'Generating the VRML', ['key', 'Return'], closes=dialog, delay_io=True)
4✔
740
    wait_create_i(cfg, 'VRML')
4✔
741
    # Exit
742
    exit_kicad_i(cfg)
4✔
743

744

745
def export_vrml_n(cfg, keys):
8✔
746
    open_dialog_with_retry("Open VRML export", keys, "VRML options", 'VRML Export Options', cfg)
4✔
747
    xdotool(['key', 'ctrl+a'])
4✔
748
    xdotool(['type', cfg.output_file])
4✔
749
    if cfg.dir_models:
4✔
750
        xdotool(['key', 'Down'])
4✔
751
        xdotool(['type', cfg.dir_models])
4✔
752
    xdotool(['key', 'Return'])
4✔
753
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
4✔
754

755

756
def export_vrml(cfg):
8✔
757
    wait_point(cfg)
8✔
758
    if cfg.ki5 or cfg.use_interposer:
8✔
759
        # With the interposer we add the missing accelerators to KiCad 6
760
        keys = ['key', 'alt+f', 'x', 'v']
5✔
761
    else:
×
762
        keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'Down', 'KP_Space', 'Down', 'Down',
3✔
763
                'Return']
×
764
        if cfg.ki7:
3✔
765
            keys.insert(2, 'Down')
2✔
766
    if cfg.use_interposer:
8✔
767
        export_vrml_i(cfg, keys)
4✔
768
    else:
×
769
        export_vrml_n(cfg, keys)
4✔
770

771

772
def export_vrml_cli(cfg):
8✔
773
    logger.debug("Using kicad-cli instead of running KiCad")
×
774
    # Run the export from CLI
775
    cmd = [cfg.kicad_cli, 'pcb', 'export', 'vrml', '-o', cfg.output_file, '-f', '--units', VRML_UNITS_2_KCLI[cfg.units]]
×
776
    if cfg.copy_3d_models:
×
777
        # Don't embed the 3D models
778
        cmd.extend(['--models-relative', '--models-dir', cfg.dir_models if cfg.dir_models else 'shapes3D'])
×
779
    if cfg.origin_mode == 0:
×
780
        # User origin
781
        units = VRML_UNITS_2_KCLI[VRML_UNITS.index(cfg.ref_units_ori)]
×
782
        cmd.extend(['--user-origin', f'{cfg.ref_x}x{cfg.ref_y}{units}'])
×
783
    cmd.append(cfg.input_file)
×
784
    run_command(cmd)
×
785

786

787
def convert_pcb_i(cfg):
8✔
788
    keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Return']
×
789
    dialog, _ = open_dialog_i(cfg, 'Save Board File As', keys)
×
790
    send_keys(cfg, 'Select all', 'ctrl+a')
×
791
    paste_output_file_i(cfg)
×
792
    send_keys(cfg, 'Converting the file', 'Return', closes=dialog, delay_io=True)
×
793
    wait_create_i(cfg, 'Converted PCB')
×
794
    exit_kicad_i(cfg)
×
795

796

797
def convert_pcb_n(cfg):
8✔
798
    keys = ['key', 'alt+f', 'Down', 'Down', 'Down', 'Down', 'Return']
×
799
    open_dialog_with_retry("Saving the PCB", keys, "Save As", 'Save Board File As', cfg)
×
800
    xdotool(['key', 'ctrl+a'])
×
801
    xdotool(['type', cfg.output_file])
×
802
    xdotool(['key', 'Return'])
×
803
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
×
804
    exit_pcbnew(cfg)
×
805

806

807
def convert_pcb(cfg):
8✔
808
    if cfg.use_interposer:
×
809
        convert_pcb_i(cfg)
×
810
    else:
×
811
        convert_pcb_n(cfg)
×
812

813

814
def export_gencad_select_options(cfg):
8✔
815
    logger.info('Changing settings')
4✔
816
    xdotool(['key', 'Down'])
4✔
817
    if cfg.flip_bottom_padstacks:
4✔
818
        xdotool(['key', 'KP_Space'])
4✔
819
    xdotool(['key', 'Down'])
4✔
820
    if cfg.unique_pin_names:
4✔
821
        xdotool(['key', 'KP_Space'])
×
822
    xdotool(['key', 'Down'])
4✔
823
    if cfg.no_reuse_shapes:
4✔
824
        xdotool(['key', 'KP_Space'])
4✔
825
    xdotool(['key', 'Down'])
4✔
826
    if cfg.aux_origin:
4✔
827
        xdotool(['key', 'KP_Space'])
×
828
    xdotool(['key', 'Down'])
4✔
829
    if cfg.save_origin:
4✔
830
        xdotool(['key', 'KP_Space'])
4✔
831
    xdotool(['key', 'Return'])
4✔
832
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
4✔
833

834

835
def export_gencad_i(cfg, keys):
8✔
836
    # Open the "Export to GenCAD settings"
837
    dialog, _ = open_dialog_i(cfg, 'Export to GenCAD settings', keys)
4✔
838
    # KiCad dialogs are unreliable, you never know which widget is selected
839
    # This goes to the top left corner of the dialog
840
    # Note 1: lamentably this widget doesn't have a label
841
    # Note 2: KiCad 7.0.7 behavior changed, up goes to the button (when no WM)
842
    xdotool(['key', 'alt+f', 'Up', 'Left'])
4✔
843
    # Paste the name
844
    paste_output_file_i(cfg)
4✔
845
    # Change the settings to what the user wants
846
    # To make it easier we add accelerators
847
    keys = ['key']
4✔
848
    if not cfg.flip_bottom_padstacks:
4✔
849
        # We used Alt+F to go to the file name, so here the logic is inverted
850
        keys.append('alt+f')
×
851
    if cfg.unique_pin_names:
4✔
852
        keys.append('alt+g')
×
853
    if cfg.no_reuse_shapes:
4✔
854
        keys.append('alt+n')
4✔
855
    if cfg.aux_origin:
4✔
856
        keys.append('alt+u')
×
857
    if cfg.save_origin:
4✔
858
        keys.append('alt+s')
4✔
859
    keys.append('Return')
4✔
860
    send_keys(cfg, 'Changing settings', keys, closes=dialog, delay_io=True)
4✔
861
    wait_create_i(cfg, 'GenCAD')
4✔
862
    # Exit
863
    exit_kicad_i(cfg)
4✔
864

865

866
def export_gencad_n(cfg, keys):
8✔
867
    open_dialog_with_retry("Open GenCAD export", keys, "GenCAD settings", 'Export to GenCAD settings', cfg)
4✔
868
    xdotool(['type', cfg.output_file])
4✔
869
    export_gencad_select_options(cfg)
4✔
870

871

872
def ipc_netlist(cfg):
8✔
873
    wait_point(cfg)
8✔
874
    if cfg.ki5:
8✔
875
        if cfg.use_interposer:
2✔
876
            keys = ['key', 'alt+f', 'f', 'i']
1✔
877
        else:
×
878
            keys = ['key', 'alt+f', 'f', 'Down', 'Down', 'Down', 'Return']
1✔
879
    else:
×
880
        keys = ['key', 'shift+alt+e']
6✔
881
    if cfg.use_interposer:
8✔
882
        ipc_netlist_i(cfg, keys)
4✔
883
    else:
×
884
        ipc_netlist_n(cfg, keys)
4✔
885

886

887
def ipc_netlist_n(cfg, keys):
8✔
888
    open_dialog_with_retry("Open file save for IPC netlist", keys, "Dialog to save the IPC netlist",
4✔
889
                           'Export D-356 Test File', cfg)
4✔
890
    xdotool(['key', 'ctrl+a'])
4✔
891
    xdotool(['type', cfg.output_file])
4✔
892
    xdotool(['key', 'Return'])
4✔
893
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
4✔
894

895

896
def ipc_netlist_i(cfg, keys):
8✔
897
    # Open the "Export D-356 Test File"
898
    dialog, _ = open_dialog_i(cfg, 'Export D-356 Test File', keys)
4✔
899
    # Paste the name (well, something that will be replaced by the real name)
900
    paste_bogus_filename(cfg)
4✔
901
    # Generate the netlist
902
    send_keys(cfg, 'Generate the netlist', 'Return', closes=dialog, delay_io=True)
4✔
903
    # Wait for file creation
904
    wait_create_i(cfg, 'IPC D-356')
4✔
905
    # Exit
906
    exit_kicad_i(cfg)
4✔
907

908

909
def wait_ray_tracer(cfg):
8✔
910
    # I can't find a mechanism to determine if the render finished.
911
    # I think this is a bug in KiCad, you can save unfinished images!!!!
912
    logger.info('Waiting for the final render')
4✔
913
    if cfg.detect_rt:
4✔
914
        # Try to figure out meassuring the CPU usage
915
        end = time.clock_gettime(time.CLOCK_MONOTONIC) + cfg.wait_rt
4✔
916
        counter = 5
4✔
917
        children = cfg.kicad_process.children(recursive=True)
4✔
918
        logger.debug('pcbnew pid {}'.format(cfg.kicad_process.pid))
4✔
919
        for child in children:
4✔
920
            logger.debug('- pcbnew child pid {}'.format(child.pid))
×
921
        while (counter > 0) and (time.clock_gettime(time.CLOCK_MONOTONIC) < end):
4✔
922
            cpu_usage = cfg.kicad_process.cpu_percent(0.3)
4✔
923
            for child in children:
4✔
924
                try:
×
925
                    cpu_usage += child.cpu_percent(0.3)
×
926
                except psutil.NoSuchProcess:
×
927
                    pass
×
928
            if cpu_usage > 5:
4✔
929
                if counter < 5:
4✔
930
                    counter = counter + 1
×
931
            else:
×
932
                counter = counter - 1
4✔
933
            logger.debug('CPU {} %, ({}) waiting ...'.format(cpu_usage, counter))
4✔
934
    else:
×
935
        sleep(cfg.wait_rt)
×
936

937

938
def wait_ray_tracer_i(cfg):
8✔
939
    # Wait until we see the 100% rendered and the time is reported
940
    wait_and_show_progress(cfg, 'PANGO:Rendering time', r'Rendering: (\d+ \%)', 'Rendering:', 'Rendering')
4✔
941
    # wait_swap(cfg) should be absorved by the wait for sleep at the end of the progress
942

943

944
def wait_3d_ready_n(cfg):
8✔
945
    if not cfg.ki5:
4✔
946
        # On my system this huge delay is needed only when using docker.
947
        # I don't know why and also don't know why the KiCad 5 methode fails.
948
        sleep(1*cfg.time_out_scale)
3✔
949
        return
3✔
950
    sleep(0.2*cfg.time_out_scale)
1✔
951
    keys = ['key', 'alt+p', 'Return']
1✔
952
    dname = '3D Display Options'
1✔
953
    if not cfg.ki5:
1✔
954
        keys.insert(2, 'Down')
×
955
        dname = 'Preferences'
×
956
    for retry in range(30):
1✔
957
        xdotool(keys)
1✔
958
        found = True
1✔
959
        try:
1✔
960
            wait_for_window('Options dialog', dname, 1)
1✔
961
        except RuntimeError:  # pragma: no cover
×
962
            found = False
×
963
        if found:
1✔
964
            break
1✔
965
    if not found:
1✔
966
        logger.error('Time-out waiting for 3D viewer to be responsive')
×
967
        exit(PCBNEW_ERROR)
×
968
    xdotool(['key', 'Escape'])
1✔
969
    wait_for_window('3D Viewer', '3D Viewer')
1✔
970

971

972
def apply_steps(steps, key, neg_key, id, cfg):
8✔
973
    if steps:
8✔
974
        k = key
8✔
975
        if steps < 0:
8✔
976
            steps = -steps
8✔
977
            k = neg_key
8✔
978
        for _ in range(steps):
8✔
979
            logger.info('Step ({})'.format(k))
8✔
980
            xdotool(['key', k], id)
8✔
981
            logger.debug('Step '+key)
8✔
982
            if cfg.use_interposer:
8✔
983
                if cfg.wait_after_move:
4✔
984
                    wait_kicad_ready_i(cfg, swaps=1)
1✔
985
                    # Arbitrary, this is a problem with KiCad 5, so I won't spend too much time looking for a better solution
986
                    sleep(1)
1✔
987
                    wait_kicad_ready_i(cfg)
1✔
988
            else:
×
989
                wait_3d_ready_n(cfg)
4✔
990

991

992
def open_save_image_n(cfg, id):
8✔
993
    keys = ['key', 'alt+f', 'Return']
4✔
994
    for retry in range(10):
4✔
995
        xdotool(keys, id)
4✔
996
        if (cfg.ray_tracing or cfg.use_rt_wait) and cfg.detect_rt:
4✔
997
            wait_ray_tracer(cfg)
4✔
998
        found = True
4✔
999
        try:
4✔
1000
            wait_for_window('File save dialog', '3D Image File Name', 3)
4✔
1001
        except RuntimeError:  # pragma: no cover
×
1002
            found = False
×
1003
        if found:
4✔
1004
            break
4✔
1005
    if not found:
4✔
1006
        logger.error('Failed to open the file save dialog')
×
1007
        exit(PCBNEW_ERROR)
×
1008

1009

1010
def capture_3d_view(cfg):
8✔
1011
    if not cfg.ki5 and cfg.kicad_version < 6000002:
8✔
1012
        logger.error('Upgrade KiCad, this version has a bug')
×
1013
        exit(MISSING_TOOL)
×
1014
    # Configure KiCad 5 vs 6 differences
1015
    if cfg.ki5:
8✔
1016
        cfg.keys_rt = ['alt+p', 'Down']
2✔
1017
        cfg.key_close_3d = 'ctrl+q'
2✔
1018
    else:
×
1019
        cfg.keys_rt = ['alt+p']
6✔
1020
        cfg.key_close_3d = 'ctrl+w'
6✔
1021
    if cfg.use_interposer:
8✔
1022
        return capture_3d_view_i(cfg)
4✔
1023
    return capture_3d_view_n(cfg)
4✔
1024

1025

1026
def capture_3d_view_n(cfg):
8✔
1027
    """ 3D View capture, normal version """
1028
    # Open the 3D viewer
1029
    open_keys = ['key', 'alt+3']
1030
    dialog_name = '3D Viewer'
1031
    id = open_dialog_with_retry('Open 3D Viewer', open_keys, '3D viewer dialog', dialog_name, cfg)[0]
1032
    wait_point(cfg)
1033

1034
    wait_3d_ready_n(cfg)
1035

1036
    # Switch to orthographic (KiCad 6 has persistence)
1037
    if cfg.orthographic and cfg.ki5:
1038
        # Can easily break, no persistence, no option, no shortcut ... KiCad's way
1039
        xdotool(['mousemove', '711', '44', 'click', '1', 'mousemove', 'restore'])
1040

1041
    # Apply the view axis
1042
    if cfg.view != 'z':
1043
        xdotool(['key', cfg.view], id)
1044
        wait_3d_ready_n(cfg)
1045

1046
    # Apply the movements
1047
    apply_steps(cfg.move_x, 'Right', 'Left', id, cfg)
1048
    apply_steps(cfg.move_y, 'Up', 'Down', id, cfg)
1049

1050
    # Apply the rotations
1051
    if not cfg.ki5:
1052
        apply_steps(cfg.rotate_x, 'alt+x', 'shift+alt+x', id, cfg)
1053
        apply_steps(cfg.rotate_y, 'alt+y', 'shift+alt+y', id, cfg)
1054
        apply_steps(cfg.rotate_z, 'alt+z', 'shift+alt+z', id, cfg)
1055

1056
    # Apply the zoom steps
1057
    zoom = cfg.zoom
1058
    if zoom:
1059
        zoom_b = '4'
1060
        if zoom < 0:
1061
            zoom = -zoom
1062
            zoom_b = '5'
1063
        for _ in range(zoom):
1064
            # Wait some time before sending commands
1065
            # See discussion on #13 issue at GitHub
1066
            sleep(0.05)
1067
            logger.info('Zoom')
1068
            xdotool(['click', zoom_b], id)
1069
            logger.debug('Zoom')
1070
            wait_3d_ready_n(cfg)
1071

1072
    if cfg.ray_tracing:
1073
        xdotool(['key']+cfg.keys_rt+['Return'], id)
1074
        logger.debug('Ray tracing')
1075

1076
    if cfg.ray_tracing or cfg.use_rt_wait:
1077
        wait_ray_tracer(cfg)
1078

1079
    # Save the image as PNG
1080
    logger.info('Saving the image')
1081
    open_save_image_n(cfg, id)
1082

1083
    # Paste the name
1084
    logger.info('Pasting output file')
1085
    wait_point(cfg)
1086
    text_replace(cfg.output_file)
1087

1088
    # Wait for the image to be created
1089
    logger.info('Wait for the image file creation')
1090
    wait_point(cfg)
1091
    # Wait before confirming the file name
1092
    sleep(0.1*cfg.time_out_scale)
1093
    xdotool(['key', 'Return'])
1094
    wait_for_file_created_by_process(cfg.pcbnew_pid, cfg.output_file)
1095

1096
    # Close the 3D viewer
1097
    logger.info('Closing the 3D viewer')
1098
    wait_point(cfg)
1099
    xdotool(['key', cfg.key_close_3d])
1100

1101
    wait_pcbnew()
1102

1103

1104
def capture_3d_view_i(cfg):
1105
    """ 3D View capture, interposer version """
1106
    dialog, id = open_dialog_i(cfg, '3D Viewer', 'alt+3', no_wait=True)
4✔
1107
    logger.debug('3D Viewer is drawing')
4✔
1108

1109
    # Detect the 3D models load (On Ki5 we can miss the "loading..." message
1110
    logger.info('Loading 3D models')
4✔
1111
    reload_time_msg = 'PANGO:Reload time'
4✔
1112
    if not cfg.ki5:
4✔
1113
        # KiCad 6 does a first bogus render without loading the models
1114
        wait_queue(cfg, reload_time_msg, starts=True)
3✔
1115
        # Then comes the real action
1116
    wait_and_show_progress(cfg, reload_time_msg, r'Loading (.*)', 'Loading', 'Loading')
4✔
1117
    logger.info('Finished loading 3D models')
4✔
1118
    # Wait until the dialog is ready
1119
    id = wait_for_window('3D Viewer', '3D Viewer', 1)[0]
4✔
1120
    if cfg.ki5:
4✔
1121
        # KiCad 5 can't handle all commands at once
1122
        cfg.wait_after_move = True
1✔
1123

1124
    # Switch to orthographic (KiCad 6 has persistence)
1125
    if cfg.orthographic and cfg.ki5:
4✔
1126
        # Can easily break, no persistence, no option, no shortcut ... KiCad's way
1127
        # Note: This is just for KiCad 5, and can't be patched by the interposer because
1128
        # this button is handled by the WX AUI (not by the underlying widgets)
1129
        xdotool(['mousemove', '711', '44', 'click', '1', 'mousemove', 'restore'])
1✔
1130

1131
    # Apply the view axis
1132
    if cfg.view != 'z':
4✔
1133
        logger.info('Changing view')
×
1134
        xdotool(['key', cfg.view], id)
×
1135
        if cfg.wait_after_move:
×
1136
            wait_kicad_ready_i(cfg, swaps=1)
×
1137

1138
    # Apply the movements
1139
    apply_steps(cfg.move_x, 'Right', 'Left', id, cfg)
4✔
1140
    apply_steps(cfg.move_y, 'Up', 'Down', id, cfg)
4✔
1141

1142
    # Apply the rotations
1143
    if not cfg.ki5:
4✔
1144
        apply_steps(cfg.rotate_x, 'alt+x', 'shift+alt+x', id, cfg)
3✔
1145
        apply_steps(cfg.rotate_y, 'alt+y', 'shift+alt+y', id, cfg)
3✔
1146
        apply_steps(cfg.rotate_z, 'alt+z', 'shift+alt+z', id, cfg)
3✔
1147

1148
    # Apply the zoom steps
1149
    zoom = cfg.zoom
4✔
1150
    if zoom:
4✔
1151
        zoom_b = '4'
4✔
1152
        if zoom < 0:
4✔
1153
            zoom = -zoom
×
1154
            zoom_b = '5'
×
1155
        for _ in range(zoom):
4✔
1156
            # Wait some time before sending commands
1157
            # See discussion on #13 issue at GitHub
1158
            sleep(0.05)
4✔
1159
            logger.info('Zoom')
4✔
1160
            xdotool(['click', zoom_b], id)
4✔
1161
            logger.debug('Zoom')
4✔
1162
            # An extra swap is done because we used the mouse (mouse "moved")
1163
            if cfg.wait_after_move:
4✔
1164
                wait_kicad_ready_i(cfg, swaps=1)
1✔
1165

1166
    if cfg.ray_tracing:
4✔
1167
        send_keys(cfg, 'Start ray tracing', ['key']+cfg.keys_rt+['Return'])
4✔
1168
        wait_queue(cfg, 'PANGO:Raytracing')
4✔
1169
        wait_ray_tracer_i(cfg)
4✔
1170

1171
    # Save the image as PNG
1172
    # Open the Save dialog
1173
    file_dlg, _ = open_dialog_i(cfg, '3D Image File Name', ['key', 'alt+f', 'Return'])
4✔
1174
    # Paste the name
1175
    paste_bogus_filename(cfg)
4✔
1176
    # Wait for the image to be created
1177
    send_keys(cfg, 'Export the PNG', 'Return', closes=file_dlg, delay_io=True)
4✔
1178
    # Wait until the file is created (why we just see the close?)
1179
    wait_create_i(cfg, 'PNG')
4✔
1180
    # Close the 3D viewer
1181
    send_keys(cfg, 'Closing the 3D viewer', cfg.key_close_3d, closes=dialog)
4✔
1182
    exit_kicad_i(cfg)
4✔
1183

1184

1185
def load_layers(pcb):
8✔
1186
    layer_names = []
8✔
1187
    with open(pcb, "rt") as pcb_file:
8✔
1188
        collect_layers = False
8✔
1189
        for line in pcb_file:
8✔
1190
            if collect_layers:
8✔
1191
                z = re.match(r'\s+\((\d+)\s+"[^"]+"\s+\S+\s+"([^"]+)"', line)
8✔
1192
                if not z:
8✔
1193
                    z = re.match(r'\s+\((\d+)\s+(\S+)', line)
8✔
1194
                if z:
8✔
1195
                    id, name = z.groups()
8✔
1196
                    if name[0] == '"':
8✔
1197
                        name = name[1:-1]
6✔
1198
                    layer_names.append(name)
8✔
1199
                else:
×
1200
                    if re.search(r'^\s+\)$', line):
8✔
1201
                        collect_layers = False
8✔
1202
                        break
8✔
1203
            else:
×
1204
                if re.search(r'\s+\(layers', line):
8✔
1205
                    collect_layers = True
8✔
1206
    return layer_names
8✔
1207

1208

1209
class ListLayers(argparse.Action):
8✔
1210
    """A special action class to list the PCB layers and exit"""
1211
    def __call__(self, parser, namespace, values, option_string):
1212
        layer_names = load_layers(values[0])
1213
        for layer in layer_names:
1214
            print(layer)
1215
        parser.exit()  # exits the program with no more arg parsing and checking
1216

1217

1218
def restore_pcb(cfg):
1219
    if cfg.input_file and cfg.pcb_size >= 0 and cfg.pcb_date >= 0:
1220
        cur_date = os.path.getmtime(cfg.input_file)
1221
        bkp = cfg.input_file+'-bak'
1222
        if cur_date != cfg.pcb_date:
1223
            logger.debug('Current pcb date: {} (!={}), trying to restore it'.
1224
                         format(asctime(localtime(cur_date)), asctime(localtime(cfg.pcb_date))))
1225
            if os.path.isfile(bkp):
1226
                bkp_size = os.path.getsize(bkp)
1227
                if bkp_size == cfg.pcb_size:
1228
                    os.remove(cfg.input_file)
1229
                    os.rename(bkp, cfg.input_file)
1230
                    logger.debug('Moved {} -> {}'.format(bkp, cfg.input_file))
1231
                else:  # pragma: no cover
1232
                    logger.error('Corrupted back-up file! (size = {})'.format(bkp_size))
1233
            else:  # pragma: no cover
1234
                logger.error('No back-up available!')
1235
        if not cfg.ki5 and os.path.isfile(bkp):
1236
            os.remove(bkp)
1237

1238

1239
def memorize_pcb(cfg):
1240
    cfg.pcb_size = os.path.getsize(cfg.input_file)
1241
    cfg.pcb_date = os.path.getmtime(cfg.input_file)
1242
    logger.debug('Current pcb ({}) size: {} date: {}'.
1243
                 format(cfg.input_file, cfg.pcb_size, asctime(localtime(cfg.pcb_date))))
1244
    if not cfg.ki5:
1245
        # KiCad 6 no longer creates back-up, we do it
1246
        try:
1247
            shutil.copy2(cfg.input_file, cfg.input_file+'-bak')
1248
        except PermissionError:
1249
            logger.warning("Unable to create a back-up for the PCB (read-only?)")
1250
            return
1251
    atexit.register(restore_pcb, cfg)
1252

1253

1254
def write_color(out, name, color, post=''):
1255
    if color is None:
1256
        return
1257
    if post:
1258
        post = '_'+post
1259
    name += 'Color_'
1260
    out.write('%s=%f\n' % (name+'Red'+post, color[0]))
1261
    out.write('%s=%f\n' % (name+'Green'+post, color[1]))
1262
    out.write('%s=%f\n' % (name+'Blue'+post, color[2]))
1263

1264

1265
def to_rgb(color, bottom=False):
1266
    index = 4 if bottom and len(color) > 4 else 0
1267
    alpha = color[index+3]
1268
    if alpha == 1.0:
1269
        return "rgb({}, {}, {})".format(round(color[index]*255.0), round(color[index+1]*255.0), round(color[index+2]*255.0))
1270
    return "rgba({}, {}, {}, {})".format(round(color[index]*255.0), round(color[index+1]*255.0), round(color[index+2]*255.0),
1271
                                         alpha)
1272

1273

1274
def create_pcbnew_config(cfg):
1275
    # Mark which layers are requested
1276
    used_layers = set()
1277
    if cfg.layers:
1278
        layer_cnt = cfg.board.GetCopperLayerCount()
1279
    for layer in cfg.layers:
1280
        # Support for kiplot inner layers
1281
        if layer.startswith("Inner"):
1282
            m = re.match(r"^Inner\.([0-9]+)$", layer)
1283
            if not m:
1284
                logger.error('Malformed inner layer name: '+layer+', use Inner.N')
1285
                sys.exit(WRONG_LAYER_NAME)
1286
            layer_n = int(m.group(1))
1287
            if layer_n < 1 or layer_n >= layer_cnt - 1:
1288
                logger.error(layer+" isn't a valid layer")
1289
                sys.exit(WRONG_LAYER_NAME)
1290
            used_layers.add((layer_n+1)*2 if cfg.ki9 else layer_n)
1291
        else:
1292
            id = cfg.board.GetLayerID(layer)
1293
            if id < 0:
1294
                logger.error('Unknown layer '+layer)
1295
                sys.exit(WRONG_LAYER_NAME)
1296
            used_layers.add(id)
1297
    with open(cfg.conf_pcbnew, "wt") as text_file:
1298
        if cfg.conf_pcbnew_json:
1299
            conf = {"graphics": {"canvas_type": 2}}
1300
            conf["drc_dialog"] = {"refill_zones": True,
1301
                                  "test_track_to_zone": True,
1302
                                  "test_all_track_errors": True}
1303
            conf["system"] = {"first_run_shown": True}
1304
            conf["printing"] = {"monochrome": cfg.monochrome,
1305
                                "color_theme": cfg.color_theme,
1306
                                "use_theme": True,
1307
                                "title_block": not cfg.no_title,
1308
                                "scale": cfg.scaling,
1309
                                "layers": sorted(used_layers)}
1310
            conf["plot"] = {"check_zones_before_plotting": cfg.fill_zones,
1311
                            "mirror": cfg.mirror,
1312
                            "all_layers_on_one_page": int(not cfg.separate),
1313
                            "pads_drill_mode": cfg.pads}
1314
            conf["window"] = {"size_x": cfg.rec_width,
1315
                              "size_y": cfg.rec_height}
1316
            conf["export_vrml"] = {"copy_3d_models": cfg.copy_3d_models,
1317
                                   "origin_mode": cfg.origin_mode,
1318
                                   "ref_units": cfg.ref_units,
1319
                                   "ref_x": cfg.ref_x,
1320
                                   "ref_y": cfg.ref_y,
1321
                                   "units": cfg.units,
1322
                                   "use_relative_paths": cfg.use_relative_paths}
1323
            json_text = json.dumps(conf)
1324
            text_file.write(json_text)
1325
            logger.debug(json_text)
1326
            # Colors for the 3D Viewer
1327
            if cfg.bg_color_1 is not None:
1328
                conf = {}
1329
                if cfg.ki8 and cfg.use_layer_colors:
1330
                    # KiCad 8 doesn't have a mode for this, we must emulate
1331
                    copper = "rgb(77, 127, 196)"  # Top
1332
                    silk_bottom = "rgb(232, 178, 167)"
1333
                    silk_top = "rgb(242, 237, 161)"
1334
                    solder_bottom = "rgba(2, 255, 238, 0.400)"
1335
                    solder_top = "rgba(216, 100, 255, 0.400)"
1336
                    solder_paste = "rgba(180, 160, 154, 0.902)"  # Top
1337
                else:
1338
                    copper = to_rgb(cfg.copper_color)
1339
                    silk_bottom = to_rgb(cfg.silk_color, bottom=True)
1340
                    silk_top = to_rgb(cfg.silk_color)
1341
                    solder_bottom = to_rgb(cfg.sm_color, bottom=True)
1342
                    solder_top = to_rgb(cfg.sm_color)
1343
                    solder_paste = to_rgb(cfg.sp_color)
1344
                conf["3d_viewer"] = {"background_bottom": to_rgb(cfg.bg_color_1),
1345
                                     "background_top": to_rgb(cfg.bg_color_2),
1346
                                     "board": to_rgb(cfg.board_color),
1347
                                     "copper": copper,
1348
                                     "silkscreen_bottom": silk_bottom,
1349
                                     "silkscreen_top": silk_top,
1350
                                     "soldermask_bottom": solder_bottom,
1351
                                     "soldermask_top": solder_top,
1352
                                     "solderpaste": solder_paste,
1353
                                     "use_board_stackup_colors": cfg.use_stackup_colors}
1354
                json_text = json.dumps(conf)
1355
                os.makedirs(os.path.dirname(cfg.conf_colors), exist_ok=True)
1356
                with open(cfg.conf_colors, "wt") as color_file:
1357
                    color_file.write(json_text)
1358
                logger.debug("Colors:")
1359
                logger.debug(json_text)
1360
                # 3D Viewer window
1361
                conf = {}
1362
                conf["window"] = {"pos_x": 0, "pos_y": 0, "size_x": cfg.rec_width, "size_y": cfg.rec_height}
1363
                conf["camera"] = {"projection_mode": int(not cfg.orthographic), "animation_enabled": False}
1364
                conf["aui"] = {"show_layer_manager": False}
1365
                conf["render"] = {"show_footprints_insert": not cfg.no_smd,
1366
                                  "show_footprints_normal": not cfg.no_tht,
1367
                                  "show_footprints_virtual": not cfg.no_virtual,
1368
                                  "show_axis": False,
1369
                                  "opengl_AA_mode": 0,
1370
                                  "show_silkscreen": not cfg.hide_silkscreen,
1371
                                  "show_soldermask": not cfg.hide_soldermask,
1372
                                  "show_solderpaste": not cfg.hide_solderpaste,
1373
                                  "show_zones": not cfg.hide_zones,
1374
                                  "clip_silk_on_via_annulus": not cfg.dont_clip_silk_on_via_annulus,
1375
                                  "subtract_mask_from_silk": not cfg.dont_substrack_mask_from_silk,
1376
                                  "show_board_body": not cfg.hide_board_body,
1377
                                  "show_comments": cfg.show_comments,
1378
                                  "show_adhesive": cfg.show_adhesive}
1379
                render = conf["render"]
1380
                if cfg.ki8:
1381
                    render["show_drawings"] = cfg.show_drawings
1382
                    if cfg.show_eco and not (cfg.show_eco1 or cfg.show_eco2):
1383
                        # Some compatibility
1384
                        cfg.show_eco1 = cfg.show_eco2 = True
1385
                    render["show_eco1"] = cfg.show_eco1
1386
                    render["show_eco2"] = cfg.show_eco2
1387
                    conf["use_stackup_colors"] = cfg.use_stackup_colors
1388
                    conf["current_layer_preset"] = ""
1389
                elif cfg.ki6:
1390
                    render["show_eco"] = cfg.show_eco
1391
                    render["realistic"] = not cfg.use_layer_colors
1392
                json_text = json.dumps(conf, sort_keys=True)
1393
                with open(cfg.conf_3dview, "wt") as viewer_file:
1394
                    viewer_file.write(json_text)
1395
                logger.debug("3D Viewer:")
1396
                logger.debug(json_text)
1397
        else:
1398
            text_file.write('canvas_type=2\n')
1399
            text_file.write('RefillZonesBeforeDrc=1\n')
1400
            text_file.write('DrcTrackToZoneTest=1\n')
1401
            text_file.write('PcbFrameFirstRunShown=1\n')
1402
            # Color
1403
            text_file.write('PrintMonochrome=%d\n' % (cfg.monochrome))
1404
            # Include frame
1405
            text_file.write('PrintPageFrame=%d\n' % (not cfg.no_title))
1406
            # Drill marks
1407
            text_file.write('PrintPadsDrillOpt=%d\n' % (cfg.pads))
1408
            # Only one file
1409
            text_file.write('PrintSinglePage=%d\n' % (not cfg.separate))
1410
            # Scaling
1411
            text_file.write('PrintScale=%3.1f\n' % (cfg.scaling))
1412
            # List all posible layers, indicating which ones are requested
1413
            for x in range(0, 50):
1414
                text_file.write('PlotLayer_%d=%d\n' % (x, int(x in used_layers)))
1415
            # The output image size is the window size!!!
1416
            text_file.write('Viewer3DFrameNamePos_x=0\n')
1417
            text_file.write('Viewer3DFrameNamePos_y=0\n')
1418
            text_file.write('Viewer3DFrameNameSize_x=%d\n' % (cfg.rec_width))
1419
            text_file.write('Viewer3DFrameNameSize_y=%d\n' % (cfg.rec_height))
1420
            # We must indicate a window size compatible with the screen.
1421
            # Otherwise events could fail to reach the main window.
1422
            text_file.write('PcbFramePos_x=0\n')
1423
            text_file.write('PcbFramePos_y=0\n')
1424
            text_file.write('PcbFrameSize_x=%d\n' % (cfg.rec_width))
1425
            text_file.write('PcbFrameSize_y=%d\n' % (cfg.rec_height))
1426
            text_file.write('ShowAxis=0\n')
1427
            text_file.write('ShowFootprints_Normal=%d\n' % (not cfg.no_tht))  # Normal?!
1428
            # Insert????!!!! please a cup of coffee for this guy ...
1429
            text_file.write('ShowFootprints_Insert=%d\n' % (not cfg.no_smd))
1430
            text_file.write('ShowFootprints_Virtual=%d\n' % (not cfg.no_virtual))
1431
            text_file.write('ShowRealisticMode=%d\n' % (not cfg.use_layer_colors))
1432
            text_file.write('ShowEcoLayers=%d\n' % (cfg.show_eco))
1433
            text_file.write('ShowCommentsLayers=%d\n' % (cfg.show_comments))
1434
            # We enable the raytracer after applying all moves
1435
            # text_file.write('RenderEngine=%d\n' % (cfg.ray_tracing))
1436
            write_color(text_file, 'Bg', cfg.bg_color_1)
1437
            write_color(text_file, 'Bg', cfg.bg_color_2, 'Top')
1438
            write_color(text_file, 'SMask', cfg.sm_color)
1439
            write_color(text_file, 'SPaste', cfg.sp_color)
1440
            write_color(text_file, 'Silk', cfg.silk_color)
1441
            write_color(text_file, 'Copper', cfg.copper_color)
1442
            write_color(text_file, 'BoardBody', cfg.board_color)
1443
            # VRML options
1444
            text_file.write('VrmlExportUnit=%d\n' % (cfg.units))
1445
            text_file.write('VrmlExportCopyFiles=%d\n' % (cfg.copy_3d_models))
1446
            text_file.write('VrmlUseRelativePaths=%d\n' % (cfg.use_relative_paths))
1447
            text_file.write('VrmlRefUnits=%d\n' % (cfg.ref_units))
1448
            text_file.write('VrmlRefX=%f\n' % (cfg.ref_x))
1449
            text_file.write('VrmlRefY=%f\n' % (cfg.ref_y))
1450
            text_file.write('VrmlUsePlainPCB=0\n')
1451
    # shutil.copy2(cfg.conf_pcbnew, '/tmp/generated')
1452

1453

1454
def load_pcb(fname):
1455
    import pcbnew
1456
    logger.info('Loading '+fname)
1457
    try:
1458
        board = pcbnew.LoadBoard(fname)
1459
    except OSError as e:
1460
        logger.error('Error loading PCB file. Corrupted?')
1461
        logger.error(e)
1462
        exit(CORRUPTED_PCB)
1463
    return board
1464

1465

1466
def process_drc_out(cfg):
1467
    error_level = 0
1468
    drc_errors, unconnected_pads = parse_drc(cfg)
1469
    logger.debug('Found {} DRC errors and {} unconnected pads/warnings'.format(drc_errors, unconnected_pads))
1470
    # Apply filters
1471
    skip_err, skip_unc = apply_filters(cfg, 'DRC error/s', 'unconnected pad/s or warnings')
1472
    drc_errors = drc_errors-skip_err
1473
    unconnected_pads = unconnected_pads-skip_unc
1474
    if drc_errors == 0 and unconnected_pads == 0:
1475
        logger.info('No errors')
1476
    else:
1477
        logger.error('Found {} DRC errors and {} unconnected pad/s or warnings'.format(drc_errors, unconnected_pads))
1478
        list_errors(cfg)
1479
        if args.ignore_unconnected:
1480
            unconnected_pads = 0
1481
        else:
1482
            list_warnings(cfg)
1483
        error_level = -(drc_errors+unconnected_pads)
1484
    return error_level
1485

1486

1487
def parse_one_color(color):
1488
    match = re.match(r'#([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})([\da-fA-F]{2})?', color)
1489
    if match is None:
1490
        logger.error('Malformed color: `{}` please use `#RRGGBBAA` where RR, GG, BB and AA are hexadecimal numbers.')
1491
        logger.error('AA is the transparency (FF opaque) and is optional. Only supported by KiCad 6.')
1492
        exit(WRONG_ARGUMENTS)
1493
    res = match.groups()
1494
    alpha = 1.0
1495
    if res[3] is not None:
1496
        alpha = int(res[3], 16)/255.0
1497
    return (int(res[0], 16)/255.0, int(res[1], 16)/255.0, int(res[2], 16)/255.0, alpha)
1498

1499

1500
def parse_color(color):
1501
    res = color[0].split(',')
1502
    if len(res) == 2 and res[0][0] == '#' and res[1][0] == '#':
1503
        c1 = parse_one_color(res[0])
1504
        c2 = parse_one_color(res[1])
1505
        return c1+c2
1506
    return parse_one_color(color[0])
1507

1508

1509
def wait_pcbnew_start_by_msg(cfg):
1510
    wait_start_by_msg(cfg)
1511
    # Make sure pcbnew has the focus, I saw problems with WM pop-ups getting the focus
1512
    return wait_pcbnew()
1513

1514

1515
def setup_config_files(cfg):
1516
    # Back-up the current pcbnew configuration
1517
    cfg.conf_pcbnew_bkp = backup_config('PCBnew', cfg.conf_pcbnew, PCBNEW_CFG_PRESENT, cfg)
1518
    if cfg.conf_colors:
1519
        cfg.conf_colors_bkp = backup_config('Colors', cfg.conf_colors, PCBNEW_CFG_PRESENT, cfg)
1520
    if cfg.conf_3dview:
1521
        cfg.conf_3dview_bkp = backup_config('3D Viewer', cfg.conf_3dview, PCBNEW_CFG_PRESENT, cfg)
1522
    # Create a suitable configuration
1523
    create_pcbnew_config(cfg)
1524
    # Hotkeys
1525
    if not cfg.ki5:
1526
        # KiCad 6 breaks menu short-cuts, but we can configure user hotkeys
1527
        # Back-up the current user.hotkeys configuration
1528
        cfg.conf_hotkeys_bkp = backup_config('User hotkeys', cfg.conf_hotkeys, USER_HOTKEYS_PRESENT, cfg)
1529
        # Create a suitable configuration
1530
        create_user_hotkeys(cfg)
1531
    # Back-up the current kicad_common configuration
1532
    cfg.conf_kicad_bkp = backup_config('KiCad common', cfg.conf_kicad, KICAD_CFG_PRESENT, cfg)
1533
    # Create a suitable configuration
1534
    create_kicad_config(cfg)
1535

1536

1537
if __name__ == '__main__':  # noqa: C901
1538
    parser = argparse.ArgumentParser(description='KiCad PCB automation')
1539
    subparsers = parser.add_subparsers(help='Command:', dest='command')
1540

1541
    # short commands: iIrmnsSvVw
1542
    parser.add_argument('--disable_interposer', '-I', help='Avoid using the interposer lib', action='store_true')
1543
    parser.add_argument('--info', '-n', help='Show information about the installation', action=ShowInfoAction, nargs=0)
1544
    parser.add_argument('--interposer_sniff', '-i', help="Log interposer info, but don't use it", action='store_true')
1545
    parser.add_argument('--record', '-r', help='Record the UI automation', action='store_true')
1546
    parser.add_argument('--rec_width', help='Record width ['+str(REC_W)+']', type=int, default=REC_W)
1547
    parser.add_argument('--rec_height', help='Record height ['+str(REC_H)+']', type=int, default=REC_H)
1548
    parser.add_argument('--separate_info', '-S', help='Send info debug level to stdout', action='store_true')
1549
    parser.add_argument('--start_x11vnc', '-s', help='Start x11vnc (debug)', action='store_true')
1550
    parser.add_argument('--use_wm', '-m', help='Use a window manager (fluxbox)', action='store_true')
1551
    parser.add_argument('--verbose', '-v', action='count', default=0)
1552
    parser.add_argument('--version', '-V', action='version', version='%(prog)s '+__version__+' - ' +
1553
                        __copyright__+' - License: '+__license__)
1554
    parser.add_argument('--wait_key', '-w', help='Wait for key to advance (debug)', action='store_true')
1555
    parser.add_argument('--wait_start', help='Timeout to pcbnew start ['+str(WAIT_START)+']', type=int, default=WAIT_START)
1556
    parser.add_argument('--time_out_scale', help='Timeout multiplier, affects most timeouts',
1557
                        type=float, default=TIME_OUT_MULT)
1558

1559
    # short commands: cflmMopsStv
1560
    export_parser = subparsers.add_parser('export', help='Export PCB layers')
1561
    export_parser.add_argument('--color_theme', '-c', nargs=1, help='KiCad 6 color theme (i.e. _builtin_default, user)',
1562
                               default=['_builtin_classic'])
1563
    export_parser.add_argument('--fill_zones', '-f', help='Fill all zones before printing', action='store_true')
1564
    export_parser.add_argument('--list', '-l', help='Print a list of layers in LIST PCB and exit', nargs=1, action=ListLayers)
1565
    export_parser.add_argument('--monochrome', '-m', help='Print in blanck and white', action='store_true')
1566
    export_parser.add_argument('--mirror', '-M', help='Print mirrored', action='store_true')
1567
    export_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['printed.pdf'])
1568
    export_parser.add_argument('--pads', '-p', nargs=1, help='Pads style (0 none, 1 small, 2 full)', default=[2])
1569
    export_parser.add_argument('--scaling', '-s', nargs=1, help='Scale factor (0 fit page)', default=[1.0])
1570
    export_parser.add_argument('--separate', '-S', help='Layers in separated sheets', action='store_true')
1571
    export_parser.add_argument('--no-title', '-t', help='Remove the title-block', action='store_true')
1572
    export_parser.add_argument('--svg', '-v', help='SVG output instead of PDF', action='store_true')
1573
    export_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1574
    export_parser.add_argument('output_dir', help='Output directory')
1575
    export_parser.add_argument('layers', nargs='+', help='Which layers to include')
1576

1577
    # short commands: fFiopstu
1578
    drc_parser = subparsers.add_parser('run_drc', help='Run Design Rules Checker on a PCB')
1579
    drc_parser.add_argument('--errors_filter', '-f', nargs=1, help='File with filters to exclude errors')
1580
    drc_parser.add_argument('--force_gui', '-F', help='Force the use of the GUI (KiCad 6/7 not 8+)', action='store_true')
1581
    drc_parser.add_argument('--ignore_unconnected', '-i', help='Ignore unconnected paths', action='store_true')
1582
    drc_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['drc_result.rpt'])
1583
    drc_parser.add_argument('--save', '-s', help='Save after DRC (updating filled zones)', action='store_true')
1584
    drc_parser.add_argument('--schematic-parity', '-p', help='Check PCB vs SCH parity (KiCad 8+)', action='store_true')
1585
    drc_parser.add_argument('--all-track-errors', '-t', help='Report all errors for each track (KiCad 8+)',
1586
                            action='store_true')
1587
    drc_parser.add_argument('--units', '-u', help='Report units (KiCad 8+)', default='mm', choices=['in', 'mm', 'mils'],
1588
                            type=str)
1589
    drc_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1590
    drc_parser.add_argument('output_dir', help='Output directory')
1591

1592
    # short commands: bcdBoOrSTuvVwWxXyYzZ
1593
    v3d_parser = subparsers.add_parser('3d_view', help='Capture the 3D view')
1594
    v3d_parser.add_argument('--bg_color_1', '-b', nargs=1, help='Background color 1', default=['#66667F'])
1595
    v3d_parser.add_argument('--bg_color_2', '-B', nargs=1, help='Background color 2', default=['#CCCCE5'])
1596
    v3d_parser.add_argument('--board_color', nargs=1, help='Board body color', default=['#332B16E6'])
1597
    v3d_parser.add_argument('--copper_color', '-c', nargs=1, help='Copper color', default=['#B29C00'])
1598
    v3d_parser.add_argument('--detect_rt', '-d', help='Try to detect when the ray tracing render finshes,'
1599
                            ' wait_rt value is the time-out (experimental)', action='store_true')
1600
    v3d_parser.add_argument('--dont_clip_silk_on_via_annulus', help="Don't clip silkscreen at via annuli (KiCad 6)",
1601
                            action='store_true')
1602
    v3d_parser.add_argument('--dont_substrack_mask_from_silk', help="Don't clip silkscreen at solder mask edges (KiCad 6)",
1603
                            action='store_true')
1604
    v3d_parser.add_argument('--hide_board_body', help='Hide the body of the PCB board (KiCad 6)', action='store_true')
1605
    v3d_parser.add_argument('--hide_silkscreen', help='Hide the silkscreen layers (KiCad 6)', action='store_true')
1606
    v3d_parser.add_argument('--hide_soldermask', help='Hide the solder mask layers (KiCad 6)', action='store_true')
1607
    v3d_parser.add_argument('--hide_solderpaste', help='Hide the solder paste layers (KiCad 6)', action='store_true')
1608
    v3d_parser.add_argument('--hide_zones', help='Hide filled areas in zones (KiCad 6)', action='store_true')
1609
    v3d_parser.add_argument('--move_x', '-x', nargs=1, help='Steps to move in the X axis (positive is to the right)',
1610
                            default=[0], type=int)
1611
    v3d_parser.add_argument('--move_y', '-y', nargs=1, help='Steps to move in the Y axis (positive is up)', default=[0],
1612
                            type=int)
1613
    v3d_parser.add_argument('--rotate_x', '-X', nargs=1,
1614
                            help='Steps to rotate around the X axis (positive is clockwise) KiCad 6', default=[0], type=int)
1615
    v3d_parser.add_argument('--rotate_y', '-Y', nargs=1,
1616
                            help='Steps to rotate around the Y axis (positive is clockwise) KiCad 6', default=[0], type=int)
1617
    v3d_parser.add_argument('--rotate_z', '-Z', nargs=1,
1618
                            help='Steps to rotate around the Y axis (positive is clockwise) KiCad 6', default=[0], type=int)
1619
    v3d_parser.add_argument('--no_smd', '-S', help='Do not include surface mount components', action='store_true')
1620
    v3d_parser.add_argument('--no_tht', '-T', help='Do not include through-hole components', action='store_true')
1621
    v3d_parser.add_argument('--virtual', '-V', help='Include virtual components', action='store_true')
1622
    v3d_parser.add_argument('--orthographic', '-O', help='Enable the orthographic projection', action='store_true')
1623
    v3d_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file (PNG)', default=['capture.png'])
1624
    v3d_parser.add_argument('--ray_tracing', '-r', help='Enable the realistic render', action='store_true')
1625
    v3d_parser.add_argument('--show_adhesive', help='Show the adhesive layer', action='store_true')
1626
    v3d_parser.add_argument('--show_comments', help='Show the user comments layer (KiCad 7: also drawings)',
1627
                            action='store_true')
1628
    v3d_parser.add_argument('--show_drawings', help='Show the user comments layer (KiCad 8)', action='store_true')
1629
    v3d_parser.add_argument('--show_eco', help='Show both user eco layers (KiCad 7)', action='store_true')
1630
    v3d_parser.add_argument('--show_eco1', help='Show the user eco layer 1 (KiCad 8)', action='store_true')
1631
    v3d_parser.add_argument('--show_eco2', help='Show the user eco layer 2 (KiCad 8)', action='store_true')
1632
    v3d_parser.add_argument('--silk_color', nargs=1,
1633
                            help='Silk color (KiCad 6 supports color1,color2 for top/bottom)', default=['#E5E5E5'])
1634
    v3d_parser.add_argument('--sm_color', nargs=1, help='Solder mask color (KiCad 6 supports color1,color2 for top/bottom)',
1635
                            default=['#143324D4'])
1636
    v3d_parser.add_argument('--sp_color', nargs=1, help='Solder paste color', default=['#808080'])
1637
    group = v3d_parser.add_mutually_exclusive_group()
1638
    group.add_argument('--use_layer_colors', help='Use the colors of the layers, not realistic colors (KiCad 6+)',
1639
                       action='store_true')
1640
    group.add_argument('--use_stackup_colors', help='Use the colors defined in the stackup (KiCad 6+)',
1641
                       action='store_true')
1642
    v3d_parser.add_argument('--use_rt_wait', '-u', help='Use the ray tracing end detection even on normal renders',
1643
                            action='store_true')
1644
    v3d_parser.add_argument('--view', '-v', nargs=1, help='Axis view, uppercase is reversed (i.e. Z is bottom)',
1645
                            default=['z'], choices=['x', 'y', 'z', 'X', 'Y', 'Z'],)
1646
    v3d_parser.add_argument('--wait_after_move', '-W', help='Wait after moving (KiCad 6 and interposer option)',
1647
                            action='store_true')
1648
    v3d_parser.add_argument('--wait_rt', '-w', nargs=1, help='Seconds to wait for the ray tracing render [5]', default=[5],
1649
                            type=int)
1650
    v3d_parser.add_argument('--zoom', '-z', nargs=1, help='Zoom steps (use negative to reduce)', default=[0], type=int)
1651
    v3d_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1652
    v3d_parser.add_argument('output_dir', help='Output directory')
1653

1654
    # short commands: afnoOu
1655
    export_gencad_parser = subparsers.add_parser('export_gencad', help='Export PCB as GenCAD')
1656
    export_gencad_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1657
    export_gencad_parser.add_argument('output_dir', help='Output directory')
1658
    export_gencad_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.cad'])
1659
    export_gencad_parser.add_argument('--flip_bottom_padstacks', '-f', help='Flip bottom footprint padstacks',
1660
                                      action='store_true')
1661
    export_gencad_parser.add_argument('--unique_pin_names', '-u', help='Generate unique pin names', action='store_true')
1662
    export_gencad_parser.add_argument('--no_reuse_shapes', '-n',
1663
                                      help='Generate a new shape for each footprint instance (Do not reuse shapes)',
1664
                                      action='store_true')
1665
    export_gencad_parser.add_argument('--aux_origin', '-a', help='Use auxiliary axis as origin', action='store_true')
1666
    export_gencad_parser.add_argument('--save_origin', '-O', help='Save the origin coordinates in the file',
1667
                                      action='store_true')
1668

1669
    # short commands: afnoOu
1670
    ipc_netlist_parser = subparsers.add_parser('ipc_netlist', help='Create the IPC-D-356 netlist')
1671
    ipc_netlist_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1672
    ipc_netlist_parser.add_argument('output_dir', help='Output directory')
1673
    ipc_netlist_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.d356'])
1674

1675
    # short commands: douUxy
1676
    export_vrml_parser = subparsers.add_parser('export_vrml', help='Export 3D model for the PCB in VRML format')
1677
    export_vrml_parser.add_argument('kicad_pcb_file', help='KiCad PCB file')
1678
    export_vrml_parser.add_argument('output_dir', help='Output directory')
1679
    export_vrml_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['pcb.wrl'])
1680
    export_vrml_parser.add_argument('--dir_models', '-d', help='Name of dir to store the footprint models.'
1681
                                    ' Models will be embedded if not specified.', type=str)
1682
    export_vrml_parser.add_argument('--ref_x', '-x', help='X origin, for user defined. KiCad6: Use center otherwise.',
1683
                                    type=float)
1684
    export_vrml_parser.add_argument('--ref_y', '-y', help='Y origin, for user defined. KiCad6: Use center otherwise.',
1685
                                    type=float)
1686
    export_vrml_parser.add_argument('--ref_units', '-u', help='Origin units', default='millimeters', const='millimeters',
1687
                                    nargs='?', choices=['millimeters', 'inches'])
1688
    export_vrml_parser.add_argument('--units', '-U', help='VRML units', default='millimeters', const='millimeters',
1689
                                    nargs='?', choices=VRML_UNITS)
1690

1691
    # short commands:
1692
    convert_parser = subparsers.add_parser('convert', help='Convert a PCB (i.e. from Altium)')
1693
    convert_parser.add_argument('pcb_file', help='Foreign PCB file')
1694
    convert_parser.add_argument('output_dir', help='Output directory')
1695
    convert_parser.add_argument('--output_name', '-o', nargs=1, help='Name of the output file', default=['converted.kicad_pcb'])
1696

1697
    args = parser.parse_args()
1698
    logger = log.init(args.separate_info)
1699
    # Set the specified verbosity
1700
    log.set_level(logger, args.verbose)
1701

1702
    if args.command is None:
1703
        logger.error('No command selected')
1704
        parser.print_help()
1705
        exit(WRONG_ARGUMENTS)
1706

1707
    input_file = args.pcb_file if args.command == 'convert' else args.kicad_pcb_file
1708
    cfg = Config(logger, input_file, args, is_pcbnew=True)
1709
    cfg.wait_start = args.wait_start
1710
    cfg.verbose = args.verbose
1711
    set_time_out_scale(cfg.time_out_scale)
1712
    set_time_out_scale_f(cfg.time_out_scale)
1713
    # Empty values by default, we'll fill them for export
1714
    cfg.fill_zones = False
1715
    cfg.layers = []
1716
    cfg.save = args.command == 'run_drc' and args.save
1717
    cfg.input_file = input_file
1718

1719
    # Get local versions for the GTK window names
1720
    gettext.textdomain('gtk30')
1721
    cfg.select_a_filename = gettext.gettext('Select a filename')
1722
    cfg.print_dlg_name = gettext.gettext('Print')
1723
    logger.debug('Select a filename -> '+cfg.select_a_filename)
1724
    logger.debug('Print -> '+cfg.print_dlg_name)
1725

1726
    # Force english + UTF-8
1727
    # Note: KiCad 6 needs a valid locale, C uses english
1728
    #       KiCad 7 doesn't need a valid locale 'en' is fine, C uses what LANG says
1729
    os.environ['LC_ALL'] = 'en' if cfg.ki7 else get_en_locale(logger)
1730
    # Force english for GTK dialogs
1731
    os.environ['LANGUAGE'] = 'en'
1732
    # Force Mesa software rendering (otherwise, the 3D viewer may crash)
1733
    os.environ['LIBGL_ALWAYS_SOFTWARE'] = "1"
1734
    if DEBUG_KICAD:
1735
        os.environ['MESA_DEBUG'] = "1"
1736
        os.environ['MESA_LOG_FILE'] = "/tmp/mesa.log"
1737
        os.environ['MESA_NO_ERROR'] = "1"
1738
    # Make sure the input file exists and has an extension
1739
    check_input_file(cfg, NO_PCB, WRONG_PCB_NAME)
1740
    if args.command in ['run_drc', 'export']:
1741
        cfg.board = load_pcb(cfg.input_file)
1742
        if not cfg.save:
1743
            memorize_pcb(cfg)
1744
    else:
1745
        cfg.board = None
1746

1747
    if args.command == 'export':
1748
        # Read the layer names from the PCB
1749
        cfg.fill_zones = args.fill_zones
1750
        cfg.layers = args.layers
1751
        try:
1752
            cfg.scaling = float(args.scaling[0])
1753
        except ValueError:
1754
            logger.error('Scaling must be a floating point value')
1755
            exit(WRONG_ARGUMENTS)
1756
        try:
1757
            cfg.pads = int(args.pads[0])
1758
        except ValueError:
1759
            logger.error('Pads style must be an integer value')
1760
            exit(WRONG_ARGUMENTS)
1761
        if cfg.pads < 0 or cfg.pads > 2:
1762
            logger.error('Pad style must be 0, 1 or 2')
1763
            exit(WRONG_ARGUMENTS)
1764
        cfg.no_title = args.no_title
1765
        cfg.monochrome = args.monochrome
1766
        cfg.separate = args.separate
1767
        cfg.svg = args.svg
1768
        cfg.color_theme = args.color_theme[0]
1769
        cfg.mirror = args.mirror
1770
        if args.mirror and cfg.ki5:
1771
            logger.warning("KiCad 5 doesn't support setting mirror print from the configuration file")
1772
    else:
1773
        cfg.scaling = 1.0
1774
        cfg.pads = 2
1775
        cfg.no_title = False
1776
        cfg.monochrome = False
1777
        cfg.separate = False
1778
        cfg.mirror = False
1779
        cfg.color_theme = '_builtin_classic'
1780

1781
    if args.command == 'export_gencad':
1782
        cfg.flip_bottom_padstacks = args.flip_bottom_padstacks
1783
        cfg.unique_pin_names = args.unique_pin_names
1784
        cfg.no_reuse_shapes = args.no_reuse_shapes
1785
        cfg.aux_origin = args.aux_origin
1786
        cfg.save_origin = args.save_origin
1787

1788
    if args.command == '3d_view':
1789
        cfg.zoom = int(args.zoom[0])
1790
        cfg.view = args.view[0]
1791
        cfg.no_tht = args.no_tht
1792
        cfg.no_smd = args.no_smd
1793
        cfg.no_virtual = not args.virtual
1794
        cfg.move_x = args.move_x[0]
1795
        cfg.move_y = args.move_y[0]
1796
        cfg.rotate_x = args.rotate_x[0]
1797
        cfg.rotate_y = args.rotate_y[0]
1798
        cfg.rotate_z = args.rotate_z[0]
1799
        cfg.ray_tracing = args.ray_tracing
1800
        cfg.wait_rt = args.wait_rt[0]
1801
        cfg.detect_rt = args.detect_rt
1802
        cfg.bg_color_1 = parse_color(args.bg_color_1)
1803
        cfg.bg_color_2 = parse_color(args.bg_color_2)
1804
        cfg.board_color = parse_color(args.board_color)
1805
        cfg.copper_color = parse_color(args.copper_color)
1806
        cfg.silk_color = parse_color(args.silk_color)
1807
        cfg.sm_color = parse_color(args.sm_color)
1808
        cfg.sp_color = parse_color(args.sp_color)
1809
        cfg.orthographic = args.orthographic
1810
        cfg.use_rt_wait = args.use_rt_wait
1811
        cfg.wait_after_move = args.wait_after_move
1812
        cfg.hide_silkscreen = args.hide_silkscreen
1813
        cfg.hide_soldermask = args.hide_soldermask
1814
        cfg.hide_solderpaste = args.hide_solderpaste
1815
        cfg.hide_zones = args.hide_zones
1816
        cfg.dont_substrack_mask_from_silk = args.dont_substrack_mask_from_silk
1817
        cfg.dont_clip_silk_on_via_annulus = args.dont_clip_silk_on_via_annulus
1818
        cfg.use_layer_colors = args.use_layer_colors
1819
        cfg.use_stackup_colors = args.use_stackup_colors
1820
        cfg.hide_board_body = args.hide_board_body
1821
        cfg.show_comments = args.show_comments
1822
        cfg.show_drawings = args.show_drawings
1823
        cfg.show_eco = args.show_eco
1824
        cfg.show_eco1 = args.show_eco1
1825
        cfg.show_eco2 = args.show_eco2
1826
        cfg.show_adhesive = args.show_adhesive
1827
    else:
1828
        cfg.no_tht = False
1829
        cfg.no_smd = False
1830
        cfg.no_virtual = True
1831
        cfg.ray_tracing = False
1832
        cfg.bg_color_1 = cfg.bg_color_2 = cfg.board_color = None
1833
        cfg.copper_color = cfg.silk_color = cfg.sm_color = cfg.sp_color = None
1834
        cfg.wait_after_move = False
1835
        cfg.show_adhesive = cfg.show_eco2 = cfg.show_eco1 = cfg.show_eco = cfg.show_drawings = cfg.show_comments = False
1836
        cfg.use_layer_colors = False
1837
        cfg.use_stackup_colors = True
1838

1839
    if args.command == 'export_vrml':
1840
        cfg.copy_3d_models = args.dir_models is not None
1841
        cfg.origin_mode = 1 if args.ref_x is None and args.ref_y is None else 0
1842
        cfg.ref_units = 1 if args.ref_units == 'inches' else 0
1843
        cfg.ref_units_ori = args.ref_units
1844
        cfg.ref_x = 0.0 if args.ref_x is None else args.ref_x
1845
        cfg.ref_y = 0.0 if args.ref_y is None else args.ref_y
1846
        cfg.units = VRML_UNITS.index(args.units)
1847
        cfg.use_relative_paths = True
1848
        cfg.dir_models = '' if args.dir_models is None else args.dir_models
1849
    else:
1850
        cfg.use_relative_paths = cfg.copy_3d_models = False
1851
        cfg.units = cfg.origin_mode = cfg.ref_units = 0
1852
        cfg.ref_x = cfg.ref_y = 0.0
1853
        cfg.dir_models = ''
1854

1855
    if args.command == 'run_drc' and args.errors_filter:
1856
        load_filters(cfg, args.errors_filter[0])
1857

1858
    memorize_project(cfg)
1859
    # Ensure we have a config dir
1860
    check_kicad_config_dir(cfg)
1861
    # Make sure the user has fp-lib-table
1862
    check_lib_table(cfg.user_fp_lib_table, cfg.sys_fp_lib_table)
1863
    # Create output dir, compute full name for output file and remove it
1864
    output_dir = os.path.abspath(args.output_dir)
1865
    cfg.video_dir = cfg.output_dir = output_dir
1866
    os.makedirs(output_dir, exist_ok=True)
1867
    # Remove the output file
1868
    output_file = os.path.join(output_dir, args.output_name[0])
1869
    if os.path.exists(output_file):
1870
        os.remove(output_file)
1871
    cfg.output_file = output_file
1872
    # Name for the video
1873
    cfg.video_name = 'pcbnew_'+args.command+'_screencast.ogv'
1874
    #
1875
    # Do all the work
1876
    #
1877
    error_level = 0
1878
    do_retry = False
1879
    if args.command == 'export_vrml' and cfg.ki9:
1880
        export_vrml_cli(cfg)
1881
        cfg.use_interposer = cfg.enable_interposer = False
1882
    elif args.command == 'export_gencad' and cfg.ki9:
1883
        export_gencad_cli(cfg)
1884
        cfg.use_interposer = cfg.enable_interposer = False
1885
    elif args.command == 'run_drc' and not cfg.ki5 and (not args.force_gui or cfg.ki8):
1886
        if cfg.ki8:
1887
            # CLI implementation
1888
            run_drc_cli(cfg, args.schematic_parity, args.all_track_errors, args.units)
1889
        else:
1890
            # First command to migrate to Python!
1891
            run_drc_python(cfg)
1892
        error_level = process_drc_out(cfg)
1893
        cfg.use_interposer = cfg.enable_interposer = False
1894
    else:
1895
        setup_config_files(cfg)
1896
        # Interposer settings
1897
        check_interposer(args, logger, cfg)
1898
        # When using the interposer inform the output file name using the environment
1899
        setup_interposer_filename(cfg)
1900
        flog_out, flog_err, cfg.flog_int = get_log_files(output_dir, 'pcbnew', also_interposer=cfg.enable_interposer)
1901
        if cfg.enable_interposer:
1902
            flog_out = subprocess.PIPE
1903
            atexit.register(dump_interposer_dialog, cfg)
1904
        for retry in range(3):
1905
            do_retry = False
1906
            with recorded_xvfb(cfg, retry):
1907
                logger.debug('Starting '+cfg.pcbnew)
1908
                if DEBUG_KICAD_NG:
1909
                    os.environ['LD_LIBRARY_PATH'] = '/usr/lib/kicad-nightly/lib/x86_64-linux-gnu/:/usr/lib/kicad-nightly/lib/'
1910
                    os.environ['KICAD_PATH'] = '/usr/share/kicad-nightly'
1911
                    cmd = ['gdb', '-batch', '-n', '-ex', 'set pagination off', '-ex', 'run', '-ex', 'bt',
1912
                           '-ex', 'bt full', '-ex', 'thread apply all bt full', '--args', '/usr/lib/kicad-nightly/bin/pcbnew',
1913
                           cfg.input_file]
1914
                elif DEBUG_KICAD:
1915
                    cmd = ['gdb', '-batch', '-n', '-ex', 'set pagination off', '-ex', 'run', '-ex', 'bt', '-ex',
1916
                           'set new-console on', '-ex', 'bt full', '-ex', 'thread apply all bt full', '--args',
1917
                           '/usr/bin/pcbnew', cfg.input_file]
1918
                else:
1919
                    cmd = [cfg.pcbnew, cfg.input_file]
1920
                use_low_level_io = False
1921
                if args.command == 'export' and cfg.use_interposer:
1922
                    # We need a file to save the print options, make it unique to avoid collisions
1923
                    create_interposer_print_options_file(cfg)
1924
                    use_low_level_io = True
1925
                os.environ['KIAUTO_INTERPOSER_LOWLEVEL_IO'] = '1' if use_low_level_io else ''
1926
                logger.info('Starting pcbnew ...')
1927
                # bufsize=1 is line buffered
1928
                with PopenContext(cmd, stderr=flog_err, close_fds=True, bufsize=1, text=True,
1929
                                  stdout=flog_out, start_new_session=True) as pcbnew_proc:
1930
                    # Avoid patching our childs
1931
                    os.environ['LD_PRELOAD'] = ''
1932
                    cfg.pcbnew_pid = pcbnew_proc.pid
1933
                    set_kicad_process(cfg, pcbnew_proc.pid)
1934
                    cfg.popen_obj = pcbnew_proc
1935
                    start_queue(cfg)
1936
                    id_pcbnew = wait_pcbnew_start_by_msg(cfg) if cfg.use_interposer else wait_pcbew_start(cfg)
1937
                    if pcbnew_proc.poll() is not None:
1938
                        do_retry = True
1939
                    else:
1940
                        if args.command == 'export':
1941
                            # TODO: Implement KiCad 7 cli version.
1942
                            # Currently useless since PDF is always B/W and KiBot has a much faster and better print
1943
                            # KiCad 7 bug: https://gitlab.com/kicad/code/kicad/-/issues/13805
1944
                            print_layers(cfg, id_pcbnew)
1945
                        elif args.command == '3d_view':
1946
                            capture_3d_view(cfg)
1947
                        elif args.command == 'export_gencad':
1948
                            export_gencad(cfg)
1949
                        elif args.command == 'ipc_netlist':
1950
                            ipc_netlist(cfg)
1951
                        elif args.command == 'export_vrml':
1952
                            export_vrml(cfg)
1953
                        elif args.command == 'convert':
1954
                            convert_pcb(cfg)
1955
                        else:  # run_drc
1956
                            run_drc(cfg)
1957
                            error_level = process_drc_out(cfg)
1958
            if not do_retry:
1959
                break
1960
            logger.warning("Pcbnew failed to start retrying ...")
1961
    if do_retry:
1962
        logger.error("Pcbnew failed to start try with --time_out_scale")
1963
        error_level = PCBNEW_ERROR
1964
    #
1965
    # Exit clean-up
1966
    #
1967
    # The following code is here only to make coverage tool properly meassure atexit code.
1968
    if not cfg.save and args.command in ['run_drc', 'export']:
1969
        atexit.unregister(restore_pcb)
1970
        restore_pcb(cfg)
1971
    atexit.unregister(restore_config)
1972
    restore_config(cfg)
1973
    atexit.unregister(restore_project)
1974
    restore_project(cfg)
1975
    # We dump the dialog only on abnormal situations
1976
    if cfg.use_interposer:
1977
        logger.debug('Removing interposer dialog ({})'.format(cfg.flog_int.name))
1978
        atexit.unregister(dump_interposer_dialog)
1979
        cfg.flog_int.close()
1980
        os.remove(cfg.flog_int.name)
1981
    exit(error_level)
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

© 2026 Coveralls, Inc