Four small Python scripts for Zuken E3.series that you can copy and use today, and a step-by-step guide to putting them on your E3 Add-ons menu. Each one does a single everyday job, writes what it found to the Messages window, and asks before it changes or prints anything.
The scripts
- Check SetupRead only
Run this first. It confirms Python, the e3series package and your menu entry all work.
- Print ProjectRead only
Opens a print menu for the sheets you select in the tree, or the whole project, grouped under their structures with a quantity for each sheet.
- Wire List To SheetAdds a sheet
Lists every wire under a structure, with colour, size, length and both ends, and draws the table on a new sheet in the empty space your border leaves.
- Replace ComponentChanges devices
Swaps the part on the selected devices for another from your database, and reports every device it changed.
They need nothing except Python and Zuken's own e3series
package. Every line in the Messages window that mentions a sheet,
wire or device is linked to it: double-click the line to jump
straight there.
Before you run any script
A script run from E3 can do anything you can do: edit the project, delete files, or send data somewhere. That is true of these scripts and of any script you find online. Five habits keep you safe.
- Copy and paste the code, don't download a file. Pasting means the only thing on your PC is the text you can see on this page. A downloaded file can differ from what a page shows, can arrive with something else attached, and is easy to run without ever opening it.
- Read it before you run it. You don't need to follow every line. Check what it imports at the top and skim for the warning signs below. Every script here starts with a plain-English description of what it does and what it changes.
- Try it on a copy of a project first, and save before running anything that changes the drawing.
-
Install packages by their exact name. The package is
e3series, published on PyPI by Zuken E3 GmbH. Look-alike names are a known trick for slipping in malicious code. - Ask if you're unsure. A colleague who knows Python, your IT team, or an AI assistant can tell you what a script does in a few seconds. Your company may also have rules about running scripts, so check them first.
subprocessoros.system: runs other programsrequests,urllib,socket: talks to the internetos.remove,shutil.rmtree: deletes filesexec,eval,base64: hides or builds code at run time- Long strings of random-looking characters
.Delete()or.Save()on the project with no question asked first
Apart from e3series, the scripts on this page import
only modules that come with Python. None of them uses the network
or runs other programs. The one os.remove is in Wire
List To Sheet, and it deletes the temporary picture of the blank
sheet it made a moment earlier, which is exactly the kind of reason
the comments should give you.
Step 1: install Python and e3series
- Download Python from python.org (version 3.9 or newer). Avoid the Microsoft Store version: Zuken's own notes say it causes problems with E3.
-
Run the installer and tick Add python.exe to PATH. Leave the
optional features at their defaults, which include
tcl/tk: the Print Project and Replace Component scripts use it for their menus. -
Open a Command Prompt (not as administrator) and install Zuken's
package. It brings
pywin32andpsutilwith it, which is how Python talks to E3.py -m pip install e3series -
Find the exact path of the Python you just installed. You will
need it for the menu in step 3.
It prints something likepy -c "import sys; print(sys.executable)"C:\Users\you\AppData\Local\Programs\Python\Python313\python.exe. In the same folder ispythonw.exe, which runs a script without opening a black console window. That is the one to use.
Step 2: save a script
-
Make a folder for your scripts with a short path, such as
C:\E3Scripts. - Click Copy code on Check Setup below, open Notepad and paste.
-
Choose File > Save as, set Save as type to
All files and save it as
CheckSetup.pyin your folder. Without the change of type, Notepad saves it asCheckSetup.py.txtand it won't run.
Step 3: add it to the E3 Add-ons menu
These are the steps Zuken gives for recent versions of E3.series. If a name differs in yours, look for the Add-ons tab in the Customize dialog.
- In E3.series, open Tools > Customize and go to the Add-ons tab.
- Add a new add-on with the small button at the top right, and give it a name, such as Check Setup.
-
Set Command to the full path of
pythonw.exefrom step 1. -
Set Arguments to the full path of the script, inside
quotes so a space in the path doesn't break it:
"C:\E3Scripts\CheckSetup.py" - Go to the Commands tab, pick the Add-ons category and drag your new command onto a toolbar or menu. Close the dialog.
Repeat steps 2 and 3 for each script you want. Each script is one self-contained file, so you can add only the ones you need.
Step 4: run it and read the Messages window
Open a project and click your new Check Setup button. The Messages window should show your Python version, the e3series version, the E3 version and the open project, ending with a green Setup OK. Double-click the project line and E3 jumps to its first sheet.
Every script writes to the Messages window the same way: blue lines are headings, green means the job is done, and problems show as E3 warnings and errors. Anything that refers to an item in the project can be double-clicked to go to it, which is the quickest way to check what a script found.
If nothing happens
pythonw.exe has no window, so a script that fails
before it reaches E3 fails silently. To see the error, run the same
script from a Command Prompt while E3 is open:
py "C:\E3Scripts\CheckSetup.py"
No module named 'e3series': the package went into a different Python. Run the pip command with the exactpython.exepath from step 1 in place ofpy.No instance of E3.series is currently running: E3 isn't open, or is running as administrator while the script isn't. Run both the same way.- A question box never appears: it may be behind E3. Check the taskbar. The scripts ask Windows to keep it on top, but some setups ignore that.
- Nothing in the Messages window: check the Command and Arguments in step 3, especially the quotes.
The code
Each script follows the same layout: a description and its limits at
the top, main() telling the story in order, then small
functions grouped into reading E3, plain logic, writing to E3 and
messages. Comments explain why something is done, not just what.
Check Setup CheckSetup.py
Reads only. Confirms everything is installed and connected.
"""Check Setup - free demo script for Zuken E3.series.
Run this one first. If it works, Python, the e3series package and your Add-ons menu entry are all
set up correctly, and the other scripts will run the same way.
It writes to the E3 Messages window:
- which Python ran the script, and its version
- the e3series package version and the E3.series version
- the open project and how many sheets it has - double-click that line to jump to the first sheet
It only reads. Nothing in the project is changed.
Needs: Python 3.9 or newer, and the e3series package (py -m pip install e3series)
Run: from the E3 Add-ons menu - see https://james-bowen.co.uk/e3-scripting/free-scripts/
Author: James Bowen - https://james-bowen.co.uk
Licence: MIT - free to use, copy and change. Provided as is, with no warranty.
Version history
1.0 13/10/26 J Bowen Initial release
1.1 13/10/26 J Bowen Process ends straight away so E3 is never left waiting
1.2 13/10/26 J Bowen Ends with TerminateProcess - os._exit could still leave E3 waiting
"""
import ctypes
import sys
import traceback
from importlib.metadata import version
import e3series
import e3series.tools
SCRIPT_NAME = "CHECK SETUP"
SCRIPT_VERSION = "1.2"
# Messages window colours (red, green, blue)
HEADING_COLOUR = (0, 84, 166)
GOOD_COLOUR = (0, 128, 0)
def main():
# With no process id, e3series connects to the E3 that launched this script from its menu
e3 = e3series.Application()
# pythonw.exe has no console window, so an unexpected error would vanish without a trace.
# Sending stderr to E3 means any crash report appears in the Messages window in red.
sys.stderr = e3series.tools.E3seriesOutput(e3, sys.stderr, (200, 0, 0))
e3.ClearOutputWindow()
e3.PutInfoEx(0, f"==== {SCRIPT_NAME} v{SCRIPT_VERSION} ====", 0, *HEADING_COLOUR)
# The Python install that ran this script - the one pip must install packages into
e3.PutInfo(0, f"Python {sys.version.split()[0]} ({sys.executable})")
e3.PutInfo(0, f"e3series {version('e3series')}")
e3.PutInfo(0, f"E3.series {e3.GetVersion()}")
prj = e3.CreateJobObject()
if prj.GetId() == 0:
# Not an error - the setup is fine, there is just nothing open to look at
e3.PutWarning(0, "No project is open - open one to see its details here.")
else:
sheet_count, sheet_ids = prj.GetSheetIds()
# Passing an id as the last argument makes the line clickable: double-click jumps to it
first_sheet = sheet_ids[0] if sheet_ids else 0
e3.PutInfo(0, f"Project {prj.GetName()} ({sheet_count} sheets)", first_sheet)
e3.PutInfoEx(0, "Setup OK - you are ready to run the other scripts.", 0, *GOOD_COLOUR)
def end_process():
"""Ends this Python process immediately.
E3 waits for an add-on to finish before it gives back right-click and drag and drop. A normal
Python exit unloads every library first, and some (tkinter's Tcl among them) can hang while
unloading - leaving E3 waiting on a script that looks finished. TerminateProcess skips all of
that. ctypes ships with Python, so this needs nothing extra.
"""
if sys.stderr:
sys.stderr.flush()
kernel32 = ctypes.windll.kernel32
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
kernel32.TerminateProcess.argtypes = [ctypes.c_void_p, ctypes.c_uint]
kernel32.TerminateProcess(kernel32.GetCurrentProcess(), 0)
if __name__ == "__main__":
try:
main()
except Exception:
traceback.print_exc() # reaches the Messages window once main() has redirected stderr
finally:
end_process()
Print Project PrintProject.py
Reads only. Select sheets in the project tree first, or nothing for the whole project, then set a quantity for each sheet in the print menu, grouped by structure. Every sheet starts at 1, and 0 skips a sheet.
"""Print Project - free demo script for Zuken E3.series.
Prints sheets from the open project on E3's current printer, with a quantity for each sheet.
- Select sheets in the project tree first to print just those.
- Select nothing to offer every sheet in the project.
The sheets are listed in the Messages window under their structures - double-click a line to jump
to that sheet - and a print menu opens, grouped the same way, with a quantity box for each sheet.
Every sheet starts at 1. Set a quantity to 0 to leave a sheet out. Nothing is sent to the
printer until you press Print.
Free version limits (the full version does these):
- Quantities start at 1 every run - nothing is remembered or worked out from sheet types.
- Prints on whichever printer E3 is currently set to.
- No choosing sheets by revision, or by what has changed since the last release.
Needs: Python 3.9 or newer, and the e3series package (py -m pip install e3series)
The print menu uses tkinter, which comes with the standard Python installer.
Run: from the E3 Add-ons menu - see https://james-bowen.co.uk/e3-scripting/free-scripts/
Author: James Bowen - https://james-bowen.co.uk
Licence: MIT - free to use, copy and change. Provided as is, with no warranty.
Version history
1.0 13/10/26 J Bowen Initial release
1.1 13/10/26 J Bowen Print menu with a quantity for each sheet
1.2 13/10/26 J Bowen Process ends straight away so E3 is never left waiting
1.3 13/10/26 J Bowen Sheets grouped under their structure, in the menu and the Messages window
"""
import ctypes
import sys
import traceback
from itertools import groupby
import e3series
import e3series.tools
SCRIPT_NAME = "PRINT PROJECT"
SCRIPT_VERSION = "1.3"
# Messages window colours (red, green, blue)
HEADING_COLOUR = (0, 84, 166)
GOOD_COLOUR = (0, 128, 0)
# Scale passed to PrintOut - 0 fits each sheet to the paper
FIT_TO_PAGE = 0
DEFAULT_QTY = 1
MAX_QTY = 99
# Set in main(). Module level so every function can write to the Messages window.
e3 = None
prj = None
def main():
global e3, prj
# With no process id, e3series connects to the E3 that launched this script from its menu
e3 = e3series.Application()
# pythonw.exe has no console, so send any crash report to the Messages window in red instead
sys.stderr = e3series.tools.E3seriesOutput(e3, sys.stderr, (200, 0, 0))
prj = e3.CreateJobObject()
e3.ClearOutputWindow()
heading(f"==== {SCRIPT_NAME} v{SCRIPT_VERSION} ====")
if prj.GetId() == 0:
e3.PutError(0, "No project is open - open a project and run the script again.")
return
sheet_ids, source = get_sheets_to_print()
if not sheet_ids:
e3.PutWarning(0, "The project has no sheets - nothing to print.")
return
sheets = read_sheets(sheet_ids)
list_sheets(sheets, source)
printer = e3.GetPrinterName()
quantities = ask_quantities(sheets, printer, source)
if quantities is None:
e3.PutWarning(0, "Cancelled - nothing was printed.")
return
jobs = build_jobs(sheets, quantities)
if not jobs:
e3.PutWarning(0, "Every quantity was 0 - nothing was printed.")
return
print_jobs(jobs, printer)
# ###############################################################################
# ############################ READING E3 #######################################
# ###############################################################################
def get_sheets_to_print():
"""The sheets picked in the project tree, or every sheet if none are picked.
Returns (sheet ids, a short description of where they came from) so the user is always told
which of the two happened - a silent fallback to 'everything' would be a nasty surprise.
"""
_, selected_ids = prj.GetTreeSelectedSheetIds()
if selected_ids:
return selected_ids, "selected in the tree"
_, all_ids = prj.GetSheetIds()
return all_ids, "whole project"
def read_sheets(sheet_ids):
"""Each sheet as a dict - id, name, and the structure it sits under - in tree order.
One Sheet object is pointed at each id in turn: creating a new object per sheet works, but is
much slower in Python."""
owners = read_sheet_owners()
sheet = prj.CreateSheetObject()
sheets = []
for sheet_id in sheet_ids:
sheet.SetId(sheet_id)
path, node_id = owners.get(sheet_id, ("(not in a structure)", 0))
sheets.append({"id": sheet_id, "name": sheet.GetName(), "structure": path, "node_id": node_id})
return sheets
def read_sheet_owners():
"""{sheet id: (structure path, structure id)} for every sheet in the tree, where the path reads
like the tree itself - "+Chassis parts / +Floor"."""
node = prj.CreateStructureNodeObject()
names, parents = {}, {}
# Walk down from the top so every structure is found, however deep
_, to_visit = prj.GetStructureNodeIds()
to_visit = list(to_visit)
while to_visit:
node_id = to_visit.pop()
if node_id in names:
continue
node.SetId(node_id)
names[node_id] = node.GetName()
parents[node_id] = node.GetParentId()
to_visit.extend(node.GetStructureNodeIds()[1])
def path(node_id):
parts = []
# Stop at the root - it has no parent, and its name is the project, not a structure
while node_id in names and parents.get(node_id):
parts.append(names[node_id])
node_id = parents[node_id]
return " / ".join(reversed(parts))
owners = {}
for node_id in names:
node.SetId(node_id)
for sheet_id in node.GetSheetIds()[1]:
owners[sheet_id] = (path(node_id), node_id)
return owners
# ###############################################################################
# ############################ PURE LOGIC #######################################
# ###############################################################################
def build_jobs(sheets, quantities):
"""Turns {sheet id: qty} into print jobs of [qty, [sheet ids]].
E3 sets the number of copies for a whole job, so sheets that need the same quantity share a
job. Only NEIGHBOURING sheets are joined, so the pages still come out in tree order.
Sheets with a quantity of 0 are left out.
"""
wanted = [(sheet["id"], quantities.get(sheet["id"], 0)) for sheet in sheets]
wanted = [(sheet_id, qty) for sheet_id, qty in wanted if qty > 0]
return [[qty, [sheet_id for sheet_id, _ in group]]
for qty, group in groupby(wanted, key=lambda item: item[1])]
def group_by_structure(sheets):
"""[(structure path, structure id, [sheets]), ...] - neighbouring sheets under the same
structure, so the order still follows the tree."""
return [(path, node_id, list(group))
for (path, node_id), group in groupby(sheets, key=lambda s: (s["structure"], s["node_id"]))]
def plural(count, word):
return word if count == 1 else word + "s"
# ###############################################################################
# ############################ PRINTING #########################################
# ###############################################################################
def print_jobs(jobs, printer):
heading("---- Printing ----")
total_pages = 0
try:
for number, (qty, sheet_ids) in enumerate(jobs, start=1):
# Copies is an E3 setting, not a project one, so it is set for every job
e3.SetPrinterCopies(qty)
if prj.PrintOut(FIT_TO_PAGE, sheet_ids) != 1:
e3.PutError(0, f"Job {number} was not accepted - check the printer in File > Print setup.",
sheet_ids[0])
continue
pages = qty * len(sheet_ids)
total_pages += pages
# The line jumps to the first sheet of the job
e3.PutInfo(0, f"Job {number} of {len(jobs)}: {len(sheet_ids)} {plural(len(sheet_ids), 'sheet')}"
f" x {qty} = {pages} {plural(pages, 'page')}", sheet_ids[0])
finally:
# Put copies back to 1 so the next print from E3 itself is not a surprise
e3.SetPrinterCopies(1)
e3.PutInfoEx(0, f"Sent {total_pages} {plural(total_pages, 'page')} to {printer}.", 0, *GOOD_COLOUR)
# ###############################################################################
# ############################ MESSAGES AND PRINT MENU ##########################
# ###############################################################################
def list_sheets(sheets, source):
"""The sheets under their structures, one clickable line each - double-click a line in the
Messages window to open that sheet."""
heading(f"---- {len(sheets)} {plural(len(sheets), 'sheet')} ({source}) ----")
for path, node_id, group in group_by_structure(sheets):
e3.PutInfo(0, path, node_id)
for sheet in group:
e3.PutInfo(0, f" {sheet['name']}", sheet["id"])
def ask_quantities(sheets, printer, source):
"""The print menu - the sheets under their structure headings, each with a quantity box.
Returns {sheet id: qty}, or None if the window is cancelled or closed.
Built with tkinter, which ships with Python, so there is nothing extra to install. It is
imported here rather than at the top, so it is only loaded once there is a menu to show.
"""
import tkinter
from tkinter import ttk
root = tkinter.Tk()
root.title(SCRIPT_NAME)
root.attributes("-topmost", True) # otherwise it can open hidden behind E3
root.resizable(False, True)
result = {}
# Only whole numbers 0-99 can be typed, so reading a box never fails. Empty counts as 0.
def is_valid(text):
return text == "" or (text.isdigit() and int(text) <= MAX_QTY)
check = (root.register(is_valid), "%P")
outer = ttk.Frame(root, padding=12)
outer.pack(fill="both", expand=True)
ttk.Label(outer, text=f"{len(sheets)} {plural(len(sheets), 'sheet')} ({source}) - set 0 to skip a sheet",
font=("Segoe UI", 10, "bold")).pack(anchor="w")
ttk.Label(outer, text=f"Printer: {printer}").pack(anchor="w", pady=(2, 8))
# A scrolling area, so a project with hundreds of sheets still fits on screen
area = ttk.Frame(outer)
area.pack(fill="both", expand=True)
canvas = tkinter.Canvas(area, highlightthickness=0)
scrollbar = ttk.Scrollbar(area, orient="vertical", command=canvas.yview)
rows = ttk.Frame(canvas)
rows.bind("<Configure>", lambda _: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=rows, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
root.bind("<MouseWheel>", lambda e: canvas.yview_scroll(-1 * (e.delta // 120), "units"))
qty_vars = {}
row = 0
for path, _, group in group_by_structure(sheets):
# Structure heading, as it reads in the project tree
ttk.Label(rows, text=path, font=("Segoe UI", 9, "bold"), wraplength=380).grid(
row=row, column=0, columnspan=2, sticky="w", pady=(8 if row else 0, 2))
row += 1
for sheet in group:
ttk.Label(rows, text=sheet["name"], width=36).grid(row=row, column=0, sticky="w",
padx=(16, 0), pady=1)
# The box's value lives in a StringVar, so the total updates however it changes -
# typing, the arrows, or the All buttons
qty = tkinter.StringVar(value=str(DEFAULT_QTY))
qty.trace_add("write", lambda *_: update_total())
ttk.Spinbox(rows, from_=0, to=MAX_QTY, width=5, textvariable=qty, validate="key",
validatecommand=check).grid(row=row, column=1, padx=(8, 0), pady=1)
qty_vars[sheet["id"]] = qty
row += 1
# Size the scrolling area to fit the rows, so the boxes are never hidden behind the scrollbar
rows.update_idletasks()
canvas.configure(width=rows.winfo_reqwidth(), height=min(rows.winfo_reqheight(), 460))
def read_quantities():
return {sheet_id: int(qty.get() or 0) for sheet_id, qty in qty_vars.items()}
# A running total, so a stray 50 is obvious before anything is printed
total = ttk.Label(outer)
total.pack(anchor="w", pady=(10, 0))
def update_total():
pages = sum(read_quantities().values())
total.configure(text=f"Total: {pages} {plural(pages, 'page')}")
def set_all(qty):
for qty_var in qty_vars.values():
qty_var.set(str(qty))
update_total()
def on_print():
result.update(read_quantities())
root.destroy()
buttons = ttk.Frame(outer)
buttons.pack(fill="x", pady=(10, 0))
ttk.Button(buttons, text="All 1", command=lambda: set_all(1)).pack(side="left")
ttk.Button(buttons, text="All 0", command=lambda: set_all(0)).pack(side="left", padx=(6, 0))
ttk.Button(buttons, text="Cancel", command=root.destroy).pack(side="right")
ttk.Button(buttons, text="Print", command=on_print).pack(side="right", padx=(0, 6))
root.bind("<Escape>", lambda _: root.destroy())
update_total()
root.focus_force()
root.mainloop()
return result or None
def heading(text):
e3.PutInfoEx(0, text, 0, *HEADING_COLOUR)
def end_process():
"""Ends this Python process immediately.
E3 waits for an add-on to finish before it gives back right-click and drag and drop. A normal
Python exit unloads every library first, and some (tkinter's Tcl among them) can hang while
unloading - leaving E3 waiting on a script that looks finished. TerminateProcess skips all of
that. ctypes ships with Python, so this needs nothing extra.
"""
if sys.stderr:
sys.stderr.flush()
kernel32 = ctypes.windll.kernel32
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
kernel32.TerminateProcess.argtypes = [ctypes.c_void_p, ctypes.c_uint]
kernel32.TerminateProcess(kernel32.GetCurrentProcess(), 0)
if __name__ == "__main__":
try:
main()
except Exception:
traceback.print_exc() # reaches the Messages window once main() has redirected stderr
finally:
end_process()
Wire List To Sheet WireListToSheet.py
Adds one sheet under the selected structure. Select a structure in the project tree first. It renders the blank sheet to a picture to find the empty space, so the table works with any border.
"""Wire List To Sheet - free demo script for Zuken E3.series.
Lists every wire under one structure and draws the list as a table on a new sheet.
1. Select a structure (for example +Main Harness) in the project tree.
2. Run the script. Each wire is listed in the Messages window - double-click a line to jump to it.
3. Say yes and a new "WIRE_LIST" sheet is added under that structure with the table on it.
A wire counts as "under" the structure if it belongs to a cable or wire group assigned to the
structure (or any structure below it), or is drawn on one of its sheets.
The new sheet uses the same border as the structure's other sheets. Before drawing, the script
renders the blank sheet to a picture and finds the biggest empty space on it, so the table sits
clear of the title block, logo and revision boxes whatever border your company uses.
Free version limits (the full version does these):
- One structure per run, and the table fills one sheet - wires that do not fit are listed in the
Messages window but not drawn.
- Fixed columns, no export to Excel or CSV, and the sheet is not updated - re-running adds another.
Needs: Python 3.9 or newer, and the e3series package (py -m pip install e3series)
Run: from the E3 Add-ons menu - see https://james-bowen.co.uk/e3-scripting/free-scripts/
Author: James Bowen - https://james-bowen.co.uk
Licence: MIT - free to use, copy and change. Provided as is, with no warranty.
Version history
1.0 13/10/26 J Bowen Initial release
1.1 13/10/26 J Bowen Sheet named WIRE_LIST (E3 rejects spaces), tries _2.._9 if taken.
Process ends straight away so E3 is never left waiting
1.2 13/10/26 J Bowen Sheet goes under the selected structure, not the last one below it.
Table placed in the biggest empty space on the border.
Ends with TerminateProcess - os._exit could still leave E3 waiting
"""
import ctypes
import os
import re
import struct
import sys
import tempfile
import traceback
from collections import Counter
import e3series
import e3series.tools
SCRIPT_NAME = "WIRE LIST TO SHEET"
SCRIPT_VERSION = "1.2"
# E3 rejects sheet names with spaces ("Invalid sheet name"), so underscores it is
SHEET_NAME = "WIRE_LIST"
# Tried in turn if the name is taken - WIRE_LIST, WIRE_LIST_2 ... WIRE_LIST_9
MAX_NAME_TRIES = 9
# Messages window colours (red, green, blue)
HEADING_COLOUR = (0, 84, 166)
GOOD_COLOUR = (0, 128, 0)
# Table layout, in sheet units (normally mm)
MARGIN = 5.0
TEXT_HEIGHT = 2.5
TITLE_HEIGHT = 5.0
ROW_HEIGHT = 5.0
# Finding empty space: the blank sheet is rendered at this many pixels per sheet unit
PIXELS_PER_UNIT = 2
# A pixel this different from the background colour (0-765, summed RGB) counts as ink
INK_THRESHOLD = 60
# (heading, key in the wire dict, where the column starts as a fraction of the table width)
COLUMNS = [
("CABLE / GROUP", "cable", 0.00),
("WIRE", "wire", 0.14),
("COLOUR", "colour", 0.26),
("CSA", "csa", 0.38),
("LENGTH", "length", 0.46),
("FROM", "from", 0.55),
("TO", "to", 0.78),
]
# Set in main(). Module level so every function can use them.
e3 = None
prj = None
def main():
global e3, prj
# With no process id, e3series connects to the E3 that launched this script from its menu
e3 = e3series.Application()
# pythonw.exe has no console, so send any crash report to the Messages window in red instead
sys.stderr = e3series.tools.E3seriesOutput(e3, sys.stderr, (200, 0, 0))
prj = e3.CreateJobObject()
e3.ClearOutputWindow()
heading(f"==== {SCRIPT_NAME} v{SCRIPT_VERSION} ====")
if prj.GetId() == 0:
e3.PutError(0, "No project is open - open a project and run the script again.")
return
node_id = get_selected_structure()
if node_id == 0:
return
node = prj.CreateStructureNodeObject()
node.SetId(node_id)
node_name = node.GetName()
core_ids, sheets_found = set(), []
collect_wires_and_sheets(node_id, core_ids, sheets_found)
wires = sorted(read_wires(core_ids), key=sort_key)
if not wires:
e3.PutWarning(0, f"No wires found under {node_name}.", node_id)
return
list_wires(wires, node_name)
if not sheets_found:
# The new sheet borrows its border and tree position from an existing sheet
e3.PutError(0, f"{node_name} has no sheets, so there is no border to copy for the wire list.",
node_id)
return
if not ask_yes_no(f"Add a {SHEET_NAME} sheet under {node_name}\n"
f"with {len(wires)} {plural(len(wires), 'wire')}?"):
e3.PutWarning(0, "Cancelled - the project was not changed.")
return
sheet_id, sheet_name = create_sheet_under(node_id, sheets_found)
if sheet_id == 0:
e3.PutError(0, f"E3 could not create a {SHEET_NAME} sheet - nothing was added. "
"See E3's own message above for the reason.")
return
if not is_sheet_under(node_id, sheet_id):
e3.PutWarning(0, f"{sheet_name} could not be put directly under {node_name} - "
"double-click to find it, and drag it in the tree if needed.", sheet_id)
area, found_space = find_empty_area(sheet_id)
if not found_space:
e3.PutWarning(0, "Could not check the border for empty space - the table uses the whole "
"working area and may overlap the title block.", sheet_id)
drawn = draw_wire_table(sheet_id, area, node_name, wires)
e3.PutInfoEx(0, f"Added {sheet_name} with {drawn} {plural(drawn, 'wire')} - "
"double-click this line to open it.", sheet_id, *GOOD_COLOUR)
if drawn < len(wires):
e3.PutWarning(0, f"{len(wires) - drawn} more wires did not fit on one sheet. "
"The full version carries on over as many sheets as it needs.")
prj.JumpToID(sheet_id)
# ###############################################################################
# ############################ READING E3 #######################################
# ###############################################################################
def get_selected_structure():
"""The one structure picked in the project tree, or 0 after telling the user what to do."""
_, node_ids = prj.GetTreeSelectedStructureNodeIds()
if len(node_ids) == 1:
return node_ids[0]
if not node_ids:
e3.PutError(0, "Select a structure in the project tree first, then run the script again.")
else:
e3.PutError(0, f"{len(node_ids)} structures are selected - the free version lists one at a time.")
return 0
def collect_wires_and_sheets(node_id, core_ids, sheets_found):
"""Walks the structure and every structure below it, adding wire ids to core_ids (a set, so a
wire found twice is only counted once) and (sheet id, structure id) pairs, in tree order, to
sheets_found."""
node = prj.CreateStructureNodeObject()
device = prj.CreateDeviceObject()
sheet = prj.CreateSheetObject()
segment = prj.CreateNetSegmentObject()
def walk(current_id):
node.SetId(current_id)
# Read everything needed from this node BEFORE recursing - the recursion moves `node` on
_, device_ids = node.GetAllDeviceIds()
_, own_sheet_ids = node.GetSheetIds()
_, child_ids = node.GetStructureNodeIds()
# Wires that belong to a cable or wire group assigned to this structure
for device_id in device_ids:
device.SetId(device_id)
if device.IsCable() or device.IsWireGroup():
# GetAllCoreIds also finds cores nested inside an overbraid; GetCoreIds would not
core_ids.update(device.GetAllCoreIds()[1])
# Wires drawn on this structure's sheets
for sheet_id in own_sheet_ids:
sheets_found.append((sheet_id, current_id))
sheet.SetId(sheet_id)
for segment_id in sheet.GetNetSegmentIds()[1]:
segment.SetId(segment_id)
core_ids.update(segment.GetCoreIds()[1])
for child_id in child_ids:
walk(child_id)
walk(node_id)
def read_wires(core_ids):
"""Every wire as a plain dict, so the sorting and drawing below never touch E3 again.
Four E3 objects are made once and pointed at each id in turn with SetId. Making new objects
for every wire works, but in Python it is by far the slowest part on a big harness."""
wire = prj.CreatePinObject() # a wire (core) is a Pin object in the E3 API
parent = prj.CreateDeviceObject()
end_pin = prj.CreatePinObject()
end_device = prj.CreateDeviceObject()
def end_text(end):
"""'X1:3' - the device and pin at one end of the wire, or '' if that end is not connected."""
pin_id = wire.GetEndPinId(end)
if pin_id == 0:
return ""
end_pin.SetId(pin_id)
end_device.SetId(pin_id) # a pin id on a Device object gives the device it is on
return f"{end_device.GetName()}:{end_pin.GetName()}"
wires = []
for core_id in core_ids:
wire.SetId(core_id)
parent.SetId(core_id) # a core id on a Device object gives its cable or wire group
wires.append({
"id": core_id,
"cable": parent.GetName(),
"wire": wire.GetName(),
"colour": wire.GetColourDescription(),
"csa": wire.GetCrossSectionDescription() or format_number(wire.GetCrossSection()),
"length": format_number(wire.GetLength()),
"from": end_text(1),
"to": end_text(2),
})
return wires
# ###############################################################################
# ############################ PURE LOGIC #######################################
# ###############################################################################
def sort_key(wire):
"""Cable then wire, in natural order - so W2 comes before W10, not after it."""
return natural_key(wire["cable"]), natural_key(wire["wire"])
def natural_key(text):
return [int(part) if part.isdigit() else part.upper() for part in re.split(r"(\d+)", text)]
def format_number(value):
"""2.5 -> '2.5', 1.0 -> '1', 0 or unset -> ''."""
return f"{value:g}" if value and value > 0 else ""
def trim_levels(value, levels):
"""Drops the last `levels` parts of an E3 location or assignment, which are joined with '.':
trim_levels('+Chassis parts.230V Chassis parts', 1) -> '+Chassis parts'."""
for _ in range(levels):
value = value.rsplit(".", 1)[0] if "." in value else ""
return value
def read_bmp(path):
"""A Windows bitmap as rows of (r, g, b) tuples, top row first. Handles the 24 and 32-bit
files E3 writes, using only the standard library."""
with open(path, "rb") as file:
data = file.read()
if data[:2] != b"BM":
raise ValueError("not a bitmap")
pixel_offset = struct.unpack_from("<I", data, 10)[0]
width, height, _, bits = struct.unpack_from("<iiHH", data, 18)
if bits not in (24, 32):
raise ValueError(f"{bits}-bit bitmaps are not handled")
step = bits // 8
row_size = (width * step + 3) // 4 * 4 # every row is padded to a multiple of 4 bytes
rows = []
for row in range(abs(height)):
start = pixel_offset + row * row_size
# Bytes are stored blue, green, red
rows.append([(data[i + 2], data[i + 1], data[i]) for i in range(start, start + width * step, step)])
# A positive height means the file is stored bottom row first
return rows[::-1] if height > 0 else rows
def ink_grid(rows):
"""True wherever a pixel differs from the background. The background is whatever colour is most
common, so a white or a coloured sheet background both work."""
background = Counter(pixel for row in rows[::4] for pixel in row[::4]).most_common(1)[0][0]
return [[sum(abs(a - b) for a, b in zip(pixel, background)) > INK_THRESHOLD for pixel in row]
for row in rows]
def largest_empty_rectangle(ink):
"""(left, top, right, bottom) in pixels of the biggest all-False rectangle in a True/False grid.
The classic 'largest rectangle in a histogram' method: each row keeps, per column, how many empty
pixels run straight up from it, and a stack finds the widest box that height allows. One pass
over the grid, so it stays quick on a full A3 sheet.
"""
best, best_area = (0, 0, 0, 0), 0
heights = [0] * (len(ink[0]) if ink else 0)
for row_number, row in enumerate(ink):
heights = [0 if is_ink else h + 1 for h, is_ink in zip(heights, row)]
stack = [] # columns with rising heights
for col, h in enumerate(heights + [0]): # the extra 0 empties the stack at the end
start = col
while stack and stack[-1][1] >= h:
start, tall = stack.pop()
area = tall * (col - start)
if area > best_area:
best_area = area
best = (start, row_number - tall + 1, col, row_number + 1)
stack.append((start, h))
return best
# ###############################################################################
# ############################ WRITING E3 #######################################
# ###############################################################################
def create_sheet_under(node_id, sheets_found):
"""Adds the wire list sheet under the selected structure itself.
A sheet's place in the tree comes from its assignment (=) and location (+). They are copied from
a sheet already under the structure - its own last sheet if it has one. Otherwise the first
sheet further down is used and one level is trimmed off for each structure in between:
'+Chassis parts.230V Chassis parts' becomes '+Chassis parts'.
Returns (sheet id, name it was given), or (0, "") if E3 refused every name tried.
"""
own = [sheet_id for sheet_id, owner_id in sheets_found if owner_id == node_id]
reference_id, owner_id = (own[-1], node_id) if own else sheets_found[0]
reference = prj.CreateSheetObject()
reference.SetId(reference_id)
assignment, location = reference.GetAssignment(), reference.GetLocation()
up_assignment, up_location = levels_between(owner_id, node_id)
assignment = trim_levels(assignment, up_assignment)
location = trim_levels(location, up_location)
new_sheet = prj.CreateSheetObject()
for name in sheet_names_to_try():
# Arguments: module (0 = none), name, border symbol, insert next to this sheet, 0 = after it
sheet_id = new_sheet.Create(0, name, reference.GetFormat(), reference_id, 0)
if sheet_id:
new_sheet.SetId(sheet_id)
new_sheet.SetAssignment(assignment)
new_sheet.SetLocation(location)
return sheet_id, name
return 0, ""
def levels_between(lower_id, upper_id):
"""How many assignment (=) and location (+) levels sit between a structure and one above it,
counting the lower one and not the upper one."""
node = prj.CreateStructureNodeObject()
up_assignment = up_location = 0
current_id = lower_id
while current_id and current_id != upper_id:
node.SetId(current_id)
name = node.GetName()
if name.startswith("="):
up_assignment += 1
elif name.startswith("+"):
up_location += 1
current_id = node.GetParentId()
return up_assignment, up_location
def is_sheet_under(node_id, sheet_id):
"""True if the sheet now shows directly under the structure in the tree."""
node = prj.CreateStructureNodeObject()
node.SetId(node_id)
return sheet_id in node.GetSheetIds()[1]
def find_empty_area(sheet_id):
"""The biggest empty rectangle on the new, blank sheet, as (x_min, y_min, x_max, y_max).
Every company's border is different, so rather than guess where the title block is, E3 renders
the blank sheet to a bitmap and the script looks for the largest area with no ink on it.
Returns (area, True), or (working area, False) if the picture could not be made or read.
"""
sheet = prj.CreateSheetObject()
sheet.SetId(sheet_id)
ok, x_min, y_min, x_max, y_max = sheet.GetWorkingArea()
if not ok:
_, x_min, y_min, x_max, y_max = sheet.GetDrawingArea()
whole = (x_min, y_min, x_max, y_max)
width = max(1, int((x_max - x_min) * PIXELS_PER_UNIT))
height = max(1, int((y_max - y_min) * PIXELS_PER_UNIT))
image_path = os.path.join(tempfile.gettempdir(), "e3_wire_list_blank_sheet.bmp")
try:
# Arguments: format, (unused), file, area, size in pixels, 24-bit colour, not grey, dpi, compression
if not sheet.ExportImageArea("bmp", 0, image_path, x_min, y_min, x_max, y_max,
width, height, 24, 0, 96, 96, 0):
return whole, False
ink = ink_grid(read_bmp(image_path))
except (OSError, ValueError):
return whole, False
finally:
if os.path.exists(image_path):
os.remove(image_path) # only ever the temporary picture made just above
left, top, right, bottom = largest_empty_rectangle(ink)
if right <= left or bottom <= top:
return whole, False
# Pixel rows count down from the top of the area; sheet y counts up from the bottom
return (x_min + left / PIXELS_PER_UNIT, y_max - bottom / PIXELS_PER_UNIT,
x_min + right / PIXELS_PER_UNIT, y_max - top / PIXELS_PER_UNIT), True
def sheet_names_to_try():
"""WIRE_LIST first, then WIRE_LIST_2, _3... in case an earlier run's sheet is still there."""
return [SHEET_NAME] + [f"{SHEET_NAME}_{n}" for n in range(2, MAX_NAME_TRIES + 1)]
def draw_wire_table(sheet_id, area, node_name, wires):
"""Draws a title, a header row and one row per wire inside the area, until it is full.
Returns how many wires were drawn."""
x_min, y_min, x_max, y_max = area
graph = prj.CreateGraphObject()
left, right = x_min + MARGIN, x_max - MARGIN
width = right - left
y = y_max - MARGIN - TITLE_HEIGHT
add_text(graph, sheet_id, f"WIRE LIST - {node_name} ({len(wires)} wires)", left, y, TITLE_HEIGHT)
y -= ROW_HEIGHT * 2
for title, _, start in COLUMNS:
add_text(graph, sheet_id, title, left + start * width, y, TEXT_HEIGHT)
graph.CreateLine(sheet_id, left, y - 1.5, right, y - 1.5)
y -= ROW_HEIGHT
drawn = 0
for wire in wires:
if y < y_min + MARGIN:
break # area full - the free version stops at one sheet
for _, key, start in COLUMNS:
add_text(graph, sheet_id, wire[key], left + start * width, y, TEXT_HEIGHT)
y -= ROW_HEIGHT
drawn += 1
return drawn
def add_text(graph, sheet_id, text, x, y, height):
if text == "":
return # nothing to draw, and E3 rejects empty text
graph_id = graph.CreateText(sheet_id, text, x, y)
if graph_id:
graph.SetId(graph_id)
graph.SetTextHeight(height)
# ###############################################################################
# ############################ MESSAGES #########################################
# ###############################################################################
def list_wires(wires, node_name):
"""One clickable line per wire - double-click it in the Messages window to jump to the wire."""
heading(f"---- {len(wires)} {plural(len(wires), 'wire')} under {node_name} ----")
for wire in wires:
route = f"{wire['from'] or '?'} -> {wire['to'] or '?'}"
parts = [f"{wire['cable']}:{wire['wire']}", wire["colour"], wire["csa"], route]
e3.PutInfo(0, " ".join(part for part in parts if part), wire["id"])
def heading(text):
e3.PutInfoEx(0, text, 0, *HEADING_COLOUR)
def plural(count, word):
return word if count == 1 else word + "s"
def ask_yes_no(question):
"""A Windows Yes/No box using ctypes, which ships with Python - no extra package needed.
Kept on top, otherwise it can open hidden behind the E3 window."""
MB_YESNO, MB_ICONQUESTION, MB_TOPMOST, IDYES = 0x04, 0x20, 0x40000, 6
answer = ctypes.windll.user32.MessageBoxW(None, question, SCRIPT_NAME,
MB_YESNO | MB_ICONQUESTION | MB_TOPMOST)
return answer == IDYES
def end_process():
"""Ends this Python process immediately.
E3 waits for an add-on to finish before it gives back right-click and drag and drop. A normal
Python exit unloads every library first, and some (tkinter's Tcl among them) can hang while
unloading - leaving E3 waiting on a script that looks finished. TerminateProcess skips all of
that. ctypes ships with Python, so this needs nothing extra.
"""
if sys.stderr:
sys.stderr.flush()
kernel32 = ctypes.windll.kernel32
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
kernel32.TerminateProcess.argtypes = [ctypes.c_void_p, ctypes.c_uint]
kernel32.TerminateProcess(kernel32.GetCurrentProcess(), 0)
if __name__ == "__main__":
try:
main()
except Exception:
traceback.print_exc() # reaches the Messages window once main() has redirected stderr
finally:
end_process()
Replace Component ReplaceComponent.py
Changes the project. Select devices on a sheet or in the tree first, and save before running.
"""Replace Component - free demo script for Zuken E3.series.
Swaps the component (part) on the selected devices for another one from your E3 database.
1. Select one or more devices on a sheet or in the device tree.
2. Run the script. The devices are listed in the Messages window - double-click to jump to one.
3. Type the new component name and version, check the summary, and say yes.
Each device is reported as changed or not changed, with a clickable line, so you can go straight
to anything that needs a look. Save the project before running, so you can go back if needed.
Free version limits (the full version does these):
- Selected devices only - no replacing a part everywhere it is used, or from a list of swaps.
- No check that the new part's pins match the old one, and no before/after bill of materials.
Needs: Python 3.9 or newer, and the e3series package (py -m pip install e3series)
The input box uses tkinter, which comes with the standard Python installer.
Run: from the E3 Add-ons menu - see https://james-bowen.co.uk/e3-scripting/free-scripts/
Author: James Bowen - https://james-bowen.co.uk
Licence: MIT - free to use, copy and change. Provided as is, with no warranty.
Version history
1.0 13/10/26 J Bowen Initial release
1.1 13/10/26 J Bowen Process ends straight away so E3 is never left waiting
1.2 13/10/26 J Bowen Ends with TerminateProcess - os._exit could still leave E3 waiting
"""
import ctypes
import sys
import traceback
import e3series
import e3series.tools
SCRIPT_NAME = "REPLACE COMPONENT"
SCRIPT_VERSION = "1.2"
# Messages window colours (red, green, blue)
HEADING_COLOUR = (0, 84, 166)
GOOD_COLOUR = (0, 128, 0)
# Set in main(). Module level so every function can use them.
e3 = None
prj = None
def main():
global e3, prj
# With no process id, e3series connects to the E3 that launched this script from its menu
e3 = e3series.Application()
# pythonw.exe has no console, so send any crash report to the Messages window in red instead
sys.stderr = e3series.tools.E3seriesOutput(e3, sys.stderr, (200, 0, 0))
prj = e3.CreateJobObject()
e3.ClearOutputWindow()
heading(f"==== {SCRIPT_NAME} v{SCRIPT_VERSION} ====")
if prj.GetId() == 0:
e3.PutError(0, "No project is open - open a project and run the script again.")
return
device_ids = get_selected_devices()
if not device_ids:
e3.PutError(0, "Select one or more devices on a sheet or in the tree, then run the script again.")
return
device = prj.CreateDeviceObject()
list_devices(device, device_ids)
# Offer the first device's part as the starting text - usually only the end needs changing
device.SetId(device_ids[0])
new_name, new_version = ask_for_component(device.GetComponentName(), device.GetComponentVersion())
if not new_name:
e3.PutWarning(0, "Cancelled - the project was not changed.")
return
# Leave out devices that already have the new part, so the summary only counts real changes
to_change = [device_id for device_id in device_ids
if not has_component(device, device_id, new_name, new_version)]
if not to_change:
e3.PutWarning(0, f"Every selected device is already {new_name} version {new_version} - nothing to do.")
return
count = f"{len(to_change)} {plural(len(to_change), 'device')}"
if not ask_yes_no(f"Change the component on {count} to\n\n {new_name} (version {new_version})\n\n"
"Save the project first if you may want to go back."):
e3.PutWarning(0, "Cancelled - the project was not changed.")
return
replace_components(device, to_change, new_name, new_version)
# ###############################################################################
# ############################ READING E3 #######################################
# ###############################################################################
def get_selected_devices():
"""Devices picked on a sheet and in the tree, without duplicates, in the order they came back.
E3 keeps two separate selections, so both are asked for. Picking a single wire reports its
whole cable here - E3 hands back the parent of anything picked inside a device.
"""
_, on_sheets = prj.GetSelectedAllDeviceIds()
_, in_tree = prj.GetTreeSelectedAllDeviceIds()
# dict.fromkeys drops repeats but, unlike a set, keeps the order
return list(dict.fromkeys(list(on_sheets) + list(in_tree)))
def has_component(device, device_id, name, version):
device.SetId(device_id)
return (device.GetComponentName().upper() == name.upper()
and device.GetComponentVersion() == version)
# ###############################################################################
# ############################ WRITING E3 #######################################
# ###############################################################################
def replace_components(device, device_ids, new_name, new_version):
"""Sets the new part on each device and reports every one, changed or not, with a jump link."""
heading("---- Results ----")
changed = 0
for device_id in device_ids:
device.SetId(device_id)
old_part = f"{device.GetComponentName()} v{device.GetComponentVersion()}"
# Returns 1 on success, 0 if E3 could not apply it - most often the part is not in the database
if device.SetComponentName(new_name, new_version) == 1:
changed += 1
e3.PutInfo(0, f"{device.GetName()} {old_part} -> {new_name} v{new_version}", device_id)
else:
e3.PutError(0, f"{device.GetName()} NOT changed - is {new_name} version {new_version} "
"in the database, and the right type for this device?", device_id)
failed = len(device_ids) - changed
summary = f"Changed {changed} of {len(device_ids)} {plural(len(device_ids), 'device')}."
if failed:
e3.PutWarning(0, f"{summary} {failed} could not be changed - see the red lines above.")
else:
e3.PutInfoEx(0, summary + " Check connections on the changed devices before saving.",
0, *GOOD_COLOUR)
# ###############################################################################
# ############################ MESSAGES AND DIALOGS #############################
# ###############################################################################
def list_devices(device, device_ids):
"""One clickable line per device - double-click it in the Messages window to jump to it."""
heading(f"---- {len(device_ids)} selected {plural(len(device_ids), 'device')} ----")
for device_id in device_ids:
device.SetId(device_id)
e3.PutInfo(0, f"{device.GetName()} {device.GetComponentName()} "
f"v{device.GetComponentVersion()}", device_id)
def ask_for_component(current_name, current_version):
"""Asks for the new component name, then its version. Returns ('', '') if either is cancelled.
tkinter ships with Python, so no extra package is needed. It is imported here rather than at
the top, so it is only loaded when there is something to ask. The hidden root window is kept
on top so the boxes cannot open behind E3.
"""
import tkinter
from tkinter import simpledialog
root = tkinter.Tk()
root.withdraw()
root.attributes("-topmost", True)
try:
name = simpledialog.askstring(SCRIPT_NAME, "New component name:",
initialvalue=current_name, parent=root)
if not name or not name.strip():
return "", ""
version = simpledialog.askstring(SCRIPT_NAME, f"Version of {name.strip()}:",
initialvalue=current_version or "1", parent=root)
if not version or not version.strip():
return "", ""
return name.strip(), version.strip()
finally:
root.destroy()
def heading(text):
e3.PutInfoEx(0, text, 0, *HEADING_COLOUR)
def plural(count, word):
return word if count == 1 else word + "s"
def ask_yes_no(question):
"""A Windows Yes/No box using ctypes, which ships with Python - no extra package needed.
Kept on top, otherwise it can open hidden behind the E3 window."""
MB_YESNO, MB_ICONQUESTION, MB_TOPMOST, IDYES = 0x04, 0x20, 0x40000, 6
answer = ctypes.windll.user32.MessageBoxW(None, question, SCRIPT_NAME,
MB_YESNO | MB_ICONQUESTION | MB_TOPMOST)
return answer == IDYES
def end_process():
"""Ends this Python process immediately.
E3 waits for an add-on to finish before it gives back right-click and drag and drop. A normal
Python exit unloads every library first, and some (tkinter's Tcl among them) can hang while
unloading - leaving E3 waiting on a script that looks finished. TerminateProcess skips all of
that. ctypes ships with Python, so this needs nothing extra.
"""
if sys.stderr:
sys.stderr.flush()
kernel32 = ctypes.windll.kernel32
kernel32.GetCurrentProcess.restype = ctypes.c_void_p
kernel32.TerminateProcess.argtypes = [ctypes.c_void_p, ctypes.c_uint]
kernel32.TerminateProcess(kernel32.GetCurrentProcess(), 0)
if __name__ == "__main__":
try:
main()
except Exception:
traceback.print_exc() # reaches the Messages window once main() has redirected stderr
finally:
end_process()
Need more than the free version?
These scripts are deliberately simple. The versions I write for companies go further: printing only what changed since the last release, wire lists that run over as many sheets as needed and export to Excel, and component swaps that check the pins match and compare the bill of materials before and after. They also come with automated tests, a log of every run and a menu that keeps every user on the latest version.
See what's in daily use on the E3 scripting page, or tell me about a job you do by hand and I'll say how I would script it.