0321 from macmini

This commit is contained in:
2025-03-21 13:28:36 +08:00
commit cd1fd4bdfa
11397 changed files with 4528845 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,922 @@
#!/usr/bin/env python3
# coding: utf-8
# Assembles local KiCad component libraries from downloaded Octopart,
# Samacsys, Ultralibrarian and Snapeda zipfiles. Currently assembles just
# symbols and footptints. Tested with KiCad 7.0 for Ubuntu.
import pathlib
from enum import Enum
from zipfile import Path
from typing import Tuple, Union, Any
import re
import zipfile
from os import stat, remove
from os.path import isfile
import logging
if __name__ == "__main__":
from kicad_cli import kicad_cli
from s_expression_parse import parse_sexp, search_recursive, extract_properties
else:
from .kicad_cli import kicad_cli
from .s_expression_parse import parse_sexp, search_recursive, extract_properties
cli = kicad_cli()
class Modification(Enum):
MKDIR = 0
TOUCH_FILE = 1
MODIFIED_FILE = 2
EXTRACTED_FILE = 3
class ModifiedObject:
def __init__(self):
self.dict = {}
def append(self, obj: pathlib.Path, modification: Modification):
self.dict[obj] = modification
# keeps track of which files were modified in case an error occurs we can revert these changes before exiting
modified_objects = ModifiedObject()
def check_file(path: pathlib.Path):
"""
Check if file exists, if not create parent directories and touch file
:param path:
"""
if not path.exists():
if not path.parent.is_dir():
path.parent.mkdir(parents=True)
modified_objects.append(path.parent, Modification.MKDIR)
path.touch(mode=0o666)
modified_objects.append(path, Modification.TOUCH_FILE)
def unzip(root, suffix):
"""
return zipfile.Path starting with root ending with suffix else return None
"""
match = None
def zipper(parent):
if parent.name.endswith(suffix):
return parent
elif parent.is_dir():
for child in parent.iterdir():
match = zipper(child)
if match:
return match
match = zipper(root)
return match
class REMOTE_TYPES(Enum):
Octopart = 0
Samacsys = 1
UltraLibrarian = 2
Snapeda = 3
class import_lib:
def print(self, txt):
print("->" + txt)
def __init__(self):
self.KICAD_3RD_PARTY_LINK = "${KICAD_3RD_PARTY}"
def set_DEST_PATH(self, DEST_PATH_=pathlib.Path.home() / "KiCad"):
self.DEST_PATH = pathlib.Path(DEST_PATH_)
def cleanName(self, name):
invalid = '<>:"/\\|?* '
name = name.strip()
for char in invalid: # remove invalid characters
name = name.replace(char, "_")
return name
def get_remote_info(
self, zf: zipfile.ZipFile
) -> Tuple[Path, Path, Path, Path, REMOTE_TYPES]:
"""
:param root_path:
:type root_path: Path
:return: dcm_path, lib_path, footprint_path, model_path, remote_type
"""
self.footprint_name = None
root_path = zipfile.Path(zf)
self.dcm_path = root_path / "device.dcm"
self.lib_path = root_path / "device.lib"
self.footprint_path = root_path / "device.pretty"
self.model_path = root_path / "device.step"
# todo fill in model path for OCTOPART
if (
self.dcm_path.exists()
and self.lib_path.exists()
and self.footprint_path.exists()
):
remote_type = REMOTE_TYPES.Octopart
return (
self.dcm_path,
self.lib_path,
self.footprint_path,
self.model_path,
remote_type,
)
self.lib_path_new = unzip(root_path, ".kicad_sym")
directory = unzip(root_path, "KiCad")
if directory:
self.dcm_path = unzip(directory, ".dcm")
self.lib_path = unzip(directory, ".lib")
self.footprint_path = directory
self.model_path = unzip(root_path, ".step")
if not self.model_path:
self.model_path = unzip(root_path, ".stp")
assert self.dcm_path and (
self.lib_path or self.lib_path_new
), "Not in samacsys format"
remote_type = REMOTE_TYPES.Samacsys
return (
self.dcm_path,
self.lib_path,
self.footprint_path,
self.model_path,
remote_type,
)
directory = root_path / "KiCAD"
if directory.exists():
self.dcm_path = unzip(directory, ".dcm")
self.lib_path = unzip(directory, ".lib")
self.footprint_path = unzip(directory, ".pretty")
self.model_path = unzip(root_path, ".step")
if not self.model_path:
self.model_path = unzip(root_path, ".stp")
assert (
self.lib_path or self.lib_path_new
) and self.footprint_path, "Not in ultralibrarian format"
remote_type = REMOTE_TYPES.UltraLibrarian
return (
self.dcm_path,
self.lib_path,
self.footprint_path,
self.model_path,
remote_type,
)
footprint = unzip(root_path, ".kicad_mod")
self.lib_path = unzip(root_path, ".lib")
if self.lib_path or self.lib_path_new:
self.dcm_path = unzip(root_path, ".dcm")
# self.footprint_path = root_path
self.footprint_path = None
if footprint:
self.footprint_path = footprint.parent
self.model_path = unzip(root_path, ".step")
remote_type = REMOTE_TYPES.Snapeda
assert (
self.lib_path or self.lib_path_new
) and self.footprint_path, "Not in Snapeda format"
return (
self.dcm_path,
self.lib_path,
self.footprint_path,
self.model_path,
remote_type,
)
if footprint or self.lib_path_new:
assert False, "Unknown library zipfile"
else:
assert False, "zipfile is probably not a library to import"
def import_dcm(
self,
device: str,
remote_type: REMOTE_TYPES,
dcm_path: pathlib.Path,
overwrite_if_exists=True,
file_ending="",
) -> Tuple[Union[pathlib.Path, None], Union[pathlib.Path, None]]:
"""
# .dcm file parsing
# Note this reads in the existing dcm file for the particular remote repo, and tries to catch any duplicates
# before overwriting or creating duplicates. It reads the existing dcm file line by line and simply copy+paste
# each line if nothing will be overwritten or duplicated. If something could be overwritten or duplicated, the
# terminal will prompt whether to overwrite or to keep the existing content and ignore the new file contents.
:returns: dcm_file_read, dcm_file_write
"""
self.dcm_skipped = False
# Array of values defining all attributes of .dcm file
dcm_attributes = (
dcm_path.read_text(encoding="utf-8").splitlines()
if dcm_path
else ["#", "# " + device, "#", "$CMP " + device, "D", "F", "$ENDCMP"]
)
# Find which lines contain the component information (ignore the rest).
index_start = None
index_end = None
index_header_start = None
for attribute_idx, attribute in enumerate(dcm_attributes):
if index_start is None:
if attribute.startswith("#"):
if attribute.strip() == "#" and index_header_start is None:
index_header_start = attribute_idx # header start
elif attribute.startswith("$CMP "):
component_name = attribute[5:].strip()
if not self.cleanName(component_name) == self.cleanName(device):
raise Warning("Unexpected device in " + dcm_path.name)
dcm_attributes[attribute_idx] = attribute.replace(
component_name, device, 1
)
index_start = attribute_idx
else:
index_header_start = None
elif index_end is None:
if attribute.startswith("$CMP "):
raise Warning("Multiple devices in " + dcm_path.name)
elif attribute.startswith("$ENDCMP"):
index_end = attribute_idx + 1
elif attribute.startswith("D"):
description = attribute[2:].strip()
if description:
dcm_attributes[attribute_idx] = "D " + description
elif attribute.startswith("F"):
datasheet = attribute[2:].strip()
if datasheet:
dcm_attributes[attribute_idx] = "F " + datasheet
if index_end is None:
raise Warning(device + "not found in " + dcm_path.name)
dcm_file_read = self.DEST_PATH / (remote_type.name + file_ending + ".dcm")
dcm_file_write = self.DEST_PATH / (remote_type.name + ".dcm~")
overwrite_existing = overwrote_existing = False
check_file(dcm_file_read)
check_file(dcm_file_write)
with dcm_file_read.open("rt", encoding="utf-8") as readfile:
with dcm_file_write.open("wt", encoding="utf-8") as writefile:
if stat(dcm_file_read).st_size == 0:
# todo Handle appending to empty file
with dcm_file_read.open("wt", encoding="utf-8") as template_file:
template = ["EESchema-DOCLIB Version 2.0", "#End Doc Library"]
template_file.writelines(line + "\n" for line in template)
template_file.close()
for line in readfile:
if re.match("# *end ", line, re.IGNORECASE):
if not overwrote_existing:
writefile.write(
"\n".join(
dcm_attributes[
(
index_start
if index_header_start is None
else index_header_start
) : index_end
]
)
+ "\n"
)
writefile.write(line)
break
elif line.startswith("$CMP "):
component_name = line[5:].strip()
if component_name.startswith(device):
if overwrite_if_exists:
overwrite_existing = True
self.print("Overwrite existing dcm")
else:
overwrite_existing = False
# self.print("Import of dcm skipped")
self.dcm_skipped = True
return dcm_file_read, dcm_file_write
writefile.write(
"\n".join(dcm_attributes[index_start:index_end]) + "\n"
)
overwrote_existing = True
else:
writefile.write(line)
elif overwrite_existing:
if line.startswith("$ENDCMP"):
overwrite_existing = False
else:
writefile.write(line)
return dcm_file_read, dcm_file_write
def import_model(
self, model_path: pathlib.Path, remote_type: REMOTE_TYPES, overwrite_if_exists
) -> Union[pathlib.Path, None]:
# --------------------------------------------------------------------------------------------------------
# 3D Model file extraction
# --------------------------------------------------------------------------------------------------------
if not model_path:
return False
write_file = self.DEST_PATH / (remote_type.name + ".3dshapes") / model_path.name
self.model_skipped = False
if write_file.exists():
if overwrite_if_exists:
overwrite_existing = True
else:
self.print("Import of 3d model skipped")
self.model_skipped = True
return model_path
if model_path.is_file():
check_file(write_file)
write_file.write_bytes(model_path.read_bytes())
modified_objects.append(write_file, Modification.EXTRACTED_FILE)
if overwrite_if_exists:
self.print("Overwrite existing 3d model")
else:
self.print("Import 3d model")
return model_path
def import_footprint(
self,
remote_type: REMOTE_TYPES,
footprint_path: pathlib.Path,
found_model: pathlib.Path,
overwrite_if_exists=True,
) -> Tuple[Union[pathlib.Path, None], Union[pathlib.Path, None]]:
"""
# Footprint file parsing
:returns: footprint_file_read, footprint_file_write
"""
footprint_file_read = None
footprint_file_write = None
self.footprint_skipped = False
footprint_path_item_tmp = None
for (
footprint_path_item
) in footprint_path.iterdir(): # try to use only newer file
if footprint_path_item.name.endswith(".kicad_mod"):
footprint_path_item_tmp = footprint_path_item
break
elif footprint_path_item.name.endswith(".mod"):
footprint_path_item_tmp = footprint_path_item
footprint_path_item = footprint_path_item_tmp
if not footprint_path_item:
self.print("No footprint found")
return footprint_file_read, footprint_file_write
if footprint_path_item.name.endswith("mod"):
footprint = footprint_path_item.read_text(encoding="utf-8")
footprint_write_path = self.DEST_PATH / (remote_type.name + ".pretty")
footprint_file_read = footprint_write_path / footprint_path_item.name
footprint_file_write = footprint_write_path / (
footprint_path_item.name + "~"
)
if found_model:
footprint.splitlines()
model_path = f"{self.KICAD_3RD_PARTY_LINK}/{remote_type.name}.3dshapes/{found_model.name}"
model = [
f' (model "{model_path}"',
" (offset (xyz 0 0 0))",
" (scale (xyz 1 1 1))",
" (rotate (xyz 0 0 0))",
" )",
]
overwrite_existing = overwrote_existing = False
if footprint_file_read.exists():
if overwrite_if_exists:
overwrite_existing = True
self.print("Overwrite existing footprint")
else:
self.print("Import of footprint skipped")
self.footprint_skipped = True
return footprint_file_read, footprint_file_write
check_file(footprint_file_read)
with footprint_file_read.open("wt", encoding="utf-8") as wr:
wr.write(footprint)
overwrote_existing = True
check_file(footprint_file_write)
with footprint_file_read.open("rt", encoding="utf-8") as readfile:
with footprint_file_write.open("wt", encoding="utf-8") as writefile:
if stat(footprint_file_read).st_size == 0:
# todo Handle appending to empty file?
pass
lines = readfile.readlines()
write_3d_into_file = False
for line_idx, line in enumerate(lines):
if not write_3d_into_file and line_idx == len(lines) - 1:
writefile.writelines(
model_line + "\n" for model_line in model
)
writefile.write(line)
break
elif line.strip().startswith(r"(model"):
writefile.write(model[0] + "\n")
write_3d_into_file = True
else:
writefile.write(line)
self.print("Import footprint")
else:
check_file(footprint_file_write)
with footprint_file_write.open("wt", encoding="utf-8") as wr:
wr.write(footprint)
self.print("Import footprint")
return footprint_file_read, footprint_file_write
def import_lib(
self,
remote_type: REMOTE_TYPES,
lib_path: pathlib.Path,
overwrite_if_exists=True,
) -> Tuple[str, Union[pathlib.Path, None], Union[pathlib.Path, None]]:
"""
.lib file parsing
Note this reads in the existing lib file for the particular remote repo, and tries to catch any duplicates
before overwriting or creating duplicates. It reads the existing dcm file line by line and simply copy+paste
each line if nothing will be overwritten or duplicated. If something could be overwritten or duplicated, the
terminal will prompt whether to overwrite or to keep the existing content and ignore the new file contents.
:returns: device, lib_file_read, lib_file_write
"""
self.lib_skipped = False
device = None
lib_lines = lib_path.read_text(encoding="utf-8").splitlines()
# Find which lines contain the component information in file to be imported
index_start = None
index_end = None
index_header_start = None
for line_idx, line in enumerate(lib_lines):
if index_start is None:
if line.startswith("#"):
if line.strip() == "#" and index_header_start is None:
index_header_start = line_idx # header start
elif line.startswith("DEF "):
device = line.split()[1]
index_start = line_idx
else:
index_header_start = None
elif index_end is None:
if line.startswith("F2"):
footprint = line.split()[1]
footprint = footprint.strip('"')
self.footprint_name = self.cleanName(footprint)
lib_lines[line_idx] = line.replace(
footprint, remote_type.name + ":" + self.footprint_name, 1
)
elif line.startswith("ENDDEF"):
index_end = line_idx + 1
elif line.startswith("F1 "):
lib_lines[line_idx] = line.replace(device, device, 1)
elif line.startswith("DEF "):
raise Warning("Multiple devices in " + lib_path.name)
if index_end is None:
raise Warning(device + " not found in " + lib_path.name)
lib_file_read = self.DEST_PATH / (remote_type.name + ".lib")
lib_file_write = self.DEST_PATH / (remote_type.name + ".lib~")
overwrite_existing = overwrote_existing = overwritten = False
check_file(lib_file_read)
check_file(lib_file_write)
with lib_file_read.open("rt", encoding="utf-8") as readfile:
with lib_file_write.open("wt", encoding="utf-8") as writefile:
if stat(lib_file_read).st_size == 0:
# todo Handle appending to empty file
with lib_file_read.open("wt", encoding="utf-8") as template_file:
template = [
"EESchema-LIBRARY Version 2.4",
"#encoding utf-8",
"# End Library",
]
template_file.writelines(line + "\n" for line in template)
template_file.close()
# For each line in the existing lib file (not the file being read from the zip. The lib file you will
# add it to.)
for line in readfile:
# Is this trying to match ENDDRAW, ENDDEF, End Library or any of the above?
if re.match("# *end ", line, re.IGNORECASE):
# If you already overwrote the new info don't add it to the end
if not overwrote_existing:
writefile.write(
"\n".join(
lib_lines[
(
index_start
if index_header_start is None
else index_header_start
) : index_end
]
)
+ "\n"
)
writefile.write(line)
break
# Catch start of new component definition
elif line.startswith("DEF "):
component_name = line.split()[1]
# Catch if the currently read component matches the name of the component you are planning to
# write
if component_name.startswith(device):
# Ask if you want to overwrite existing component
if overwrite_if_exists:
overwrite_existing = True
overwritten = True
self.print("Overwrite existing lib")
else:
self.print("Import of lib skipped")
self.lib_skipped = True
return device, lib_file_read, lib_file_write
writefile.write(
"\n".join(lib_lines[index_start:index_end]) + "\n"
)
overwrote_existing = True
else:
writefile.write(line)
elif overwrite_existing:
if line.startswith("ENDDEF"):
overwrite_existing = False
else:
writefile.write(line)
if not overwritten:
self.print("Import lib")
return device, lib_file_read, lib_file_write
def import_lib_new(
self,
remote_type: REMOTE_TYPES,
lib_path: pathlib.Path,
overwrite_if_exists=True,
) -> Tuple[str, Union[pathlib.Path, None], Union[pathlib.Path, None]]:
symbol_name = None
def extract_symbol_names(input_text):
pattern = r'"(.*?)"' # Searches for text in quotes
# Searches for "(symbol" followed by text in quotes
pattern = r'\(symbol\s+"(.*?)"'
matches = re.findall(pattern, input_text)
return matches
symbol_name = None
def replace_footprint_name(string, original_name, remote_type_name, new_name):
escaped_original_name = re.escape(original_name)
pattern = rf'\(property\s+"Footprint"\s+"{escaped_original_name}"'
replacement = f'(property "Footprint" "{remote_type_name}:{new_name}"'
modified_string = re.sub(pattern, replacement, string, flags=re.MULTILINE)
return modified_string
def extract_symbol_section_new(sexpression: str, symbol_name):
pattern = re.compile(
r'\(symbol "{}"(.*?)\)\n\)'.format(re.escape(symbol_name)), re.DOTALL
)
match = pattern.search(sexpression)
if match:
return f'(symbol "{symbol_name}"{match.group(1)})'
return None
RAW_text = lib_path.read_text(encoding="utf-8") # new
sexp_list = parse_sexp(RAW_text) # new
symbol_name = search_recursive(sexp_list, "symbol") # new
symbol_properties = extract_properties(sexp_list) # new
symbol_section = extract_symbol_section_new(RAW_text, symbol_name)
Footprint_name_raw = symbol_properties["Footprint"] # new
self.footprint_name = self.cleanName(Footprint_name_raw)
symbol_section = replace_footprint_name(
symbol_section, Footprint_name_raw, remote_type.name, self.footprint_name
)
lib_file_read = self.DEST_PATH / (remote_type.name + ".kicad_sym")
lib_file_read_old = self.DEST_PATH / (remote_type.name + "_kicad_sym.kicad_sym")
lib_file_write = self.DEST_PATH / (remote_type.name + ".kicad_sym~")
if isfile(lib_file_read_old) and not isfile(lib_file_read):
lib_file_read = lib_file_read_old
if not lib_file_read.exists(): # library does not yet exist
with lib_file_write.open("wt", encoding="utf-8") as writefile:
text = RAW_text.strip().split("\n")
writefile.write(text[0] + "\n")
writefile.write(symbol_section + "\n")
writefile.write(text[-1])
check_file(lib_file_read)
self.print("Import kicad_sym")
return symbol_name, lib_file_read, lib_file_write
check_file(lib_file_read)
lib_file_txt = lib_file_read.read_text(encoding="utf-8")
existing_libs = extract_symbol_names(lib_file_txt)
if symbol_name in existing_libs:
if overwrite_if_exists:
self.print("Overwrite existing kicad_sym is not implemented") # TODO
else:
self.print("Import of kicad_sym skipped")
return symbol_name, lib_file_read, lib_file_write
closing_bracket = lib_file_txt.rfind(")")
with lib_file_write.open("wt", encoding="utf-8") as writefile:
writefile.write(lib_file_txt[:closing_bracket])
writefile.write(symbol_section + "\n")
writefile.write(lib_file_txt[closing_bracket:])
self.print("Import kicad_sym")
return symbol_name, lib_file_read, lib_file_write
def import_all(
self, zip_file: pathlib.Path, overwrite_if_exists=True, import_old_format=True
):
"""zip is a pathlib.Path to import the symbol from"""
if not zipfile.is_zipfile(zip_file):
return None
self.print(f"Import: {zip_file}")
with zipfile.ZipFile(zip_file) as zf:
(
dcm_path,
lib_path,
footprint_path,
model_path,
remote_type,
) = self.get_remote_info(zf)
self.print("Identify " + remote_type.name)
CompatibilityMode = False
if lib_path and not self.lib_path_new:
CompatibilityMode = True
temp_path = self.DEST_PATH / "temp.lib"
temp_path_new = self.DEST_PATH / "temp.kicad_sym"
if temp_path.exists():
remove(temp_path)
if temp_path_new.exists():
remove(temp_path_new)
with temp_path.open("wt", encoding="utf-8") as writefile:
text = lib_path.read_text(encoding="utf-8")
writefile.write(text)
if temp_path.exists() and cli.exists():
cli.upgrade_sym_lib(temp_path, temp_path_new)
self.print("compatibility mode: convert from .lib to .kicad_sym")
if temp_path_new.exists() and temp_path_new.is_file():
self.lib_path_new = temp_path_new
else:
self.print("error during conversion")
if self.lib_path_new:
device, lib_file_new_read, lib_file_new_write = self.import_lib_new(
remote_type, self.lib_path_new, overwrite_if_exists
)
file_ending = ""
if "_kicad_sym" in lib_file_new_read.name:
file_ending = "_kicad_sym"
dcm_file_new_read, dcm_file_new_write = self.import_dcm(
device,
remote_type,
dcm_path,
overwrite_if_exists,
file_ending=file_ending,
)
if not import_old_format:
lib_path = None
if CompatibilityMode:
if temp_path.exists():
remove(temp_path)
if temp_path_new.exists():
remove(temp_path_new)
if lib_path:
device, lib_file_read, lib_file_write = self.import_lib(
remote_type, lib_path, overwrite_if_exists
)
dcm_file_read, dcm_file_write = self.import_dcm(
device, remote_type, dcm_path, overwrite_if_exists
)
found_model = self.import_model(
model_path, remote_type, overwrite_if_exists
)
footprint_file_read, footprint_file_write = self.import_footprint(
remote_type, footprint_path, found_model, overwrite_if_exists
)
# replace read files with write files only after all operations succeeded
if self.lib_path_new:
if lib_file_new_write.exists():
lib_file_new_write.replace(lib_file_new_read)
if dcm_file_new_write.exists() and not self.dcm_skipped:
dcm_file_new_write.replace(dcm_file_new_read)
elif dcm_file_new_write.exists():
remove(dcm_file_new_write)
if lib_path:
if dcm_file_write.exists() and not self.dcm_skipped:
dcm_file_write.replace(dcm_file_read)
elif dcm_file_write.exists():
remove(dcm_file_write)
if lib_file_write.exists() and not self.lib_skipped:
lib_file_write.replace(lib_file_read)
elif lib_file_write.exists():
remove(lib_file_write)
if (
footprint_file_read
and (self.footprint_name != footprint_file_read.stem)
and not self.footprint_skipped
):
self.print(
'Warning renaming footprint file "'
+ footprint_file_read.stem
+ '" to "'
+ self.footprint_name
+ '"'
)
if footprint_file_read.exists():
remove(footprint_file_read)
footprint_file_read = footprint_file_read.parent / (
self.footprint_name + footprint_file_read.suffix
)
if (
footprint_file_write
and footprint_file_write.exists()
and not self.footprint_skipped
):
footprint_file_write.replace(footprint_file_read)
elif footprint_file_write and footprint_file_write.exists():
remove(footprint_file_write)
return ("OK",)
def main(
lib_file, lib_folder, overwrite=False, KICAD_3RD_PARTY_LINK="${KICAD_3RD_PARTY}"
):
lib_folder = pathlib.Path(lib_folder)
lib_file = pathlib.Path(lib_file)
print("overwrite", overwrite)
if not lib_folder.is_dir():
print(f"Error destination folder {lib_folder} does not exist!")
return 0
if not lib_file.is_file():
print(f"Error file {lib_folder} to be imported was not found!")
return 0
impart = import_lib()
impart.KICAD_3RD_PARTY_LINK = KICAD_3RD_PARTY_LINK
impart.set_DEST_PATH(lib_folder)
try:
(res,) = impart.import_all(lib_file, overwrite_if_exists=overwrite)
print(res)
except AssertionError as e:
print(e)
except Exception as e:
print(e)
if __name__ == "__main__":
import argparse
logging.basicConfig(level=logging.ERROR)
# Example: python plugins/KiCadImport.py --download-folder Demo/libs --lib-folder import_test
parser = argparse.ArgumentParser(
description="Import KiCad libraries from a file or folder."
)
# Create mutually exclusive arguments for file or folder
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--download-folder",
help="Path to the folder with the zip files to be imported.",
)
group.add_argument("--download-file", help="Path to the zip file to import.")
group.add_argument("--easyeda", help="Import easyeda part. example: C2040")
parser.add_argument(
"--lib-folder",
required=True,
help="Destination folder for the imported KiCad files.",
)
parser.add_argument(
"--overwrite-if-exists",
action="store_true",
help="Overwrite existing files if they already exist",
)
parser.add_argument(
"--path-variable",
help="Example: if only project-specific '${KIPRJMOD}' standard is '${KICAD_3RD_PARTY}'",
)
args = parser.parse_args()
lib_folder = pathlib.Path(args.lib_folder)
if args.path_variable:
path_variable = str(args.path_variable).strip()
else:
path_variable = "${KICAD_3RD_PARTY}"
if args.download_file:
main(
lib_file=args.download_file,
lib_folder=args.lib_folder,
overwrite=args.overwrite_if_exists,
KICAD_3RD_PARTY_LINK=path_variable,
)
elif args.download_folder:
download_folder = pathlib.Path(args.download_folder)
if not download_folder.is_dir():
print(f"Error Source folder {download_folder} does not exist!")
elif not lib_folder.is_dir():
print(f"Error destination folder {lib_folder} does not exist!")
else:
for zip_file in download_folder.glob("*.zip"):
if (
zip_file.is_file() and zip_file.stat().st_size >= 1024
): # Check if it's a file and at least 1 KB
main(
lib_file=zip_file,
lib_folder=args.lib_folder,
overwrite=args.overwrite_if_exists,
KICAD_3RD_PARTY_LINK=path_variable,
)
elif args.easyeda:
if not lib_folder.is_dir():
print(f"Error destination folder {lib_folder} does not exist!")
else:
component_id = str(args.easyeda).strip
print("Try to import EeasyEDA / LCSC Part# : ", component_id)
from impart_easyeda import easyeda2kicad_wrapper
easyeda_import = easyeda2kicad_wrapper()
easyeda_import.full_import(
component_id="C2040",
base_folder=lib_folder,
overwrite=False,
lib_var=path_variable,
)

View File

@ -0,0 +1,3 @@
from .impart_action import ActionImpartPlugin
ActionImpartPlugin().register()

View File

@ -0,0 +1,4 @@
[config]
src_path = /Users/timothy/Downloads
dest_path = /Users/timothy/Workbench/embedded/kicad_libs

View File

@ -0,0 +1,490 @@
import pcbnew
import os.path
from pathlib import Path
import wx
from time import sleep
from threading import Thread
import sys
import traceback
import subprocess
import os
import venv
try:
if __name__ == "__main__":
from impart_gui import impartGUI
from KiCadImport import import_lib
from impart_helper_func import filehandler, config_handler, KiCad_Settings
from impart_migration import find_old_lib_files, convert_lib_list
else:
# relative import is required in kicad
from .impart_gui import impartGUI
from .KiCadImport import import_lib
from .impart_helper_func import filehandler, config_handler, KiCad_Settings
from .impart_migration import find_old_lib_files, convert_lib_list
except Exception as e:
print(traceback.format_exc())
def activate_virtualenv(venv_dir):
"""Activates a virtual environment, but creates it first if it does not exist."""
venv_dir = os.path.abspath(venv_dir)
if os.name == "nt": # Windows
if not os.path.exists(venv_dir):
# venv.create(venv_dir, with_pip=True) # dont work
kicad_executable = sys.executable
kicad_bin_dir = os.path.dirname(kicad_executable)
python_executable = os.path.join(kicad_bin_dir, "python.exe")
subprocess.run(
[python_executable, "-m", "venv", venv_dir],
check=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
print(f"Virtual environment not found. Create new in {venv_dir} ...")
python_executable = os.path.join(venv_dir, "Scripts", "python.exe")
site_packages = os.path.join(venv_dir, "Lib", "site-packages")
else: # Linux / macOS
if not os.path.exists(venv_dir):
venv.create(venv_dir, with_pip=True)
print(f"Virtual environment not found. Create new in {venv_dir} ...")
python_executable = os.path.join(venv_dir, "bin", "python")
site_packages = os.path.join(
venv_dir,
"lib",
f"python{sys.version_info.major}.{sys.version_info.minor}",
"site-packages",
)
sys.path.insert(0, site_packages)
return python_executable
def ensure_package(package_name, python_executable="python"):
try:
__import__(package_name)
return True
except ModuleNotFoundError:
try:
cmd = [python_executable, "-m", "pip", "install", package_name]
print(" ".join(cmd))
subprocess.check_call(cmd)
__import__(package_name)
return True
except:
return False
EVT_UPDATE_ID = wx.NewIdRef()
def EVT_UPDATE(win, func):
win.Connect(-1, -1, EVT_UPDATE_ID, func)
class ResultEvent(wx.PyEvent):
def __init__(self, data):
wx.PyEvent.__init__(self)
self.SetEventType(EVT_UPDATE_ID)
self.data = data
class PluginThread(Thread):
def __init__(self, wxObject):
Thread.__init__(self)
self.wxObject = wxObject
self.stopThread = False
self.start()
def run(self):
lenStr = 0
global backend_h
while not self.stopThread:
if lenStr != len(backend_h.print_buffer):
self.report(backend_h.print_buffer)
lenStr = len(backend_h.print_buffer)
sleep(0.5)
def report(self, status):
wx.PostEvent(self.wxObject, ResultEvent(status))
class impart_backend:
def __init__(self):
path2config = os.path.join(os.path.dirname(__file__), "config.ini")
self.config = config_handler(path2config)
path_seting = pcbnew.SETTINGS_MANAGER().GetUserSettingsPath()
self.KiCad_Settings = KiCad_Settings(path_seting)
self.runThread = False
self.autoImport = False
self.overwriteImport = False
self.import_old_format = False
self.autoLib = False
self.folderhandler = filehandler(".")
self.print_buffer = ""
self.importer = import_lib()
self.importer.print = self.print2buffer
def version_to_tuple(version_str):
try:
return tuple(map(int, version_str.split("-")[0].split(".")))
except (ValueError, AttributeError) as e:
print(f"Version extractions error '{version_str}': {e}")
return None
minVersion = "8.0.4"
KiCadVers = version_to_tuple(pcbnew.Version())
if not KiCadVers or KiCadVers < version_to_tuple(minVersion):
self.print2buffer("KiCad Version: " + str(pcbnew.FullVersion()))
self.print2buffer("Minimum required KiCad version is " + minVersion)
self.print2buffer("This can limit the functionality of the plugin.")
if not self.config.config_is_set:
self.print2buffer(
"Warning: The path where the libraries should be saved has not been adjusted yet."
+ " Maybe you use the plugin in this version for the first time.\n"
)
additional_information = (
"If this plugin is being used for the first time, settings in KiCad are required. "
+ "The settings are checked at the end of the import process. For easy setup, "
+ "auto setting can be activated."
)
self.print2buffer(additional_information)
self.print2buffer("\n##############################\n")
def print2buffer(self, *args):
for text in args:
self.print_buffer = self.print_buffer + str(text) + "\n"
def __find_new_file__(self):
path = self.config.get_SRC_PATH()
if not os.path.isdir(path):
return 0
while True:
newfilelist = self.folderhandler.GetNewFiles(path)
for lib in newfilelist:
try:
(res,) = self.importer.import_all(
lib,
overwrite_if_exists=self.overwriteImport,
import_old_format=self.import_old_format,
)
self.print2buffer(res)
except AssertionError as e:
self.print2buffer(e)
except Exception as e:
self.print2buffer(e)
backend_h.print2buffer(f"Error: {e}")
backend_h.print2buffer("Python version " + sys.version)
print(traceback.format_exc())
self.print2buffer("")
if not self.runThread:
break
if not pcbnew.GetBoard():
# print("pcbnew close")
break
sleep(1)
backend_h = impart_backend()
def checkImport(add_if_possible=True):
libnames = ["Octopart", "Samacsys", "UltraLibrarian", "Snapeda", "EasyEDA"]
setting = backend_h.KiCad_Settings
DEST_PATH = backend_h.config.get_DEST_PATH()
msg = ""
msg += setting.check_GlobalVar(DEST_PATH, add_if_possible)
for name in libnames:
# The lines work but old libraries should not be added automatically
# libname = os.path.join(DEST_PATH, name + ".lib")
# if os.path.isfile(libname):
# msg += setting.check_symbollib(name + ".lib", add_if_possible)
libdir = os.path.join(DEST_PATH, name + ".kicad_sym")
libdir_old = os.path.join(DEST_PATH, name + "_kicad_sym.kicad_sym")
libdir_convert_lib = os.path.join(DEST_PATH, name + "_old_lib.kicad_sym")
if os.path.isfile(libdir):
libname = name + ".kicad_sym"
msg += setting.check_symbollib(libname, add_if_possible)
elif os.path.isfile(libdir_old):
libname = name + "_kicad_sym.kicad_sym"
msg += setting.check_symbollib(libname, add_if_possible)
if os.path.isfile(libdir_convert_lib):
libname = name + "_old_lib.kicad_sym"
msg += setting.check_symbollib(libname, add_if_possible)
libdir = os.path.join(DEST_PATH, name + ".pretty")
if os.path.isdir(libdir):
msg += setting.check_footprintlib(name, add_if_possible)
return msg
class impart_frontend(impartGUI):
global backend_h
def __init__(self, board, action):
super(impart_frontend, self).__init__(None)
self.board = board
self.action = action
self.m_dirPicker_sourcepath.SetPath(backend_h.config.get_SRC_PATH())
self.m_dirPicker_librarypath.SetPath(backend_h.config.get_DEST_PATH())
self.m_autoImport.SetValue(backend_h.autoImport)
self.m_overwrite.SetValue(backend_h.overwriteImport)
self.m_check_autoLib.SetValue(backend_h.autoLib)
self.m_check_import_all.SetValue(backend_h.import_old_format)
if backend_h.runThread:
self.m_button.Label = "automatic import / press to stop"
else:
self.m_button.Label = "Start"
EVT_UPDATE(self, self.updateDisplay)
self.Thread = PluginThread(self) # only for text output
self.test_migrate_possible()
def updateDisplay(self, status):
self.m_text.SetValue(status.data)
self.m_text.SetInsertionPointEnd()
# def print(self, text):
# self.m_text.AppendText(str(text)+"\n")
def on_close(self, event):
if backend_h.runThread:
dlg = wx.MessageDialog(
None,
"The automatic import process continues in the background. "
+ "If this is not desired, it must be stopped.\n"
+ "As soon as the PCB Editor window is closed, the import process also ends.",
"WARNING: impart background process",
wx.KILL_OK | wx.ICON_WARNING,
)
if dlg.ShowModal() != wx.ID_OK:
return
backend_h.autoImport = self.m_autoImport.IsChecked()
backend_h.overwriteImport = self.m_overwrite.IsChecked()
backend_h.autoLib = self.m_check_autoLib.IsChecked()
backend_h.import_old_format = self.m_check_import_all.IsChecked()
# backend_h.runThread = False
self.Thread.stopThread = True # only for text output
event.Skip()
def BottonClick(self, event):
backend_h.importer.set_DEST_PATH(backend_h.config.get_DEST_PATH())
backend_h.autoImport = self.m_autoImport.IsChecked()
tmp = self.m_overwrite.IsChecked()
if tmp and not tmp == backend_h.overwriteImport:
backend_h.folderhandler.filelist = []
backend_h.overwriteImport = self.m_overwrite.IsChecked()
backend_h.autoLib = self.m_check_autoLib.IsChecked()
backend_h.import_old_format = self.m_check_import_all.IsChecked()
if backend_h.runThread:
backend_h.runThread = False
self.m_button.Label = "Start"
return
backend_h.runThread = False
backend_h.__find_new_file__()
self.m_button.Label = "Start"
if backend_h.autoImport:
backend_h.runThread = True
self.m_button.Label = "automatic import / press to stop"
x = Thread(target=backend_h.__find_new_file__, args=[])
x.start()
add_if_possible = self.m_check_autoLib.IsChecked()
msg = checkImport(add_if_possible)
if msg:
msg += "\n\nMore information can be found in the README for the integration into KiCad.\n"
msg += "github.com/Steffen-W/Import-LIB-KiCad-Plugin"
msg += "\nSome configurations require a KiCad restart to be detected correctly."
dlg = wx.MessageDialog(None, msg, "WARNING", wx.KILL_OK | wx.ICON_WARNING)
if dlg.ShowModal() != wx.ID_OK:
return
backend_h.print2buffer("\n##############################\n")
backend_h.print2buffer(msg)
backend_h.print2buffer("\n##############################\n")
event.Skip()
def DirChange(self, event):
backend_h.config.set_SRC_PATH(self.m_dirPicker_sourcepath.GetPath())
backend_h.config.set_DEST_PATH(self.m_dirPicker_librarypath.GetPath())
backend_h.folderhandler.filelist = []
self.test_migrate_possible()
event.Skip()
def ButtomManualImport(self, event):
try:
from .impart_easyeda import easyeda2kicad_wrapper
component_id = self.m_textCtrl2.GetValue().strip() # example: "C2040"
overwrite = self.m_overwrite.IsChecked()
backend_h.print2buffer("")
backend_h.print2buffer(
"Try to import EeasyEDA / LCSC Part# : " + component_id
)
base_folder = backend_h.config.get_DEST_PATH()
easyeda_import = easyeda2kicad_wrapper()
easyeda_import.print = backend_h.print2buffer
easyeda_import.full_import(component_id, base_folder, overwrite)
event.Skip()
except Exception as e:
backend_h.print2buffer(f"Error: {e}")
backend_h.print2buffer("Python version " + sys.version)
print(traceback.format_exc())
def get_old_libfiles(self):
libpath = self.m_dirPicker_librarypath.GetPath()
libs = ["Octopart", "Samacsys", "UltraLibrarian", "Snapeda", "EasyEDA"]
return find_old_lib_files(folder_path=libpath, libs=libs)
def test_migrate_possible(self):
libs2migrate = self.get_old_libfiles()
conv = convert_lib_list(libs2migrate, drymode=True)
if len(conv):
self.m_button_migrate.Show()
else:
self.m_button_migrate.Hide()
def migrate_libs(self, event):
libs2migrate = self.get_old_libfiles()
conv = convert_lib_list(libs2migrate, drymode=True)
def print2GUI(text):
backend_h.print2buffer(text)
if len(conv) <= 0:
print2GUI("Error in migrate_libs()")
return
SymbolTable = backend_h.KiCad_Settings.get_sym_table()
SymbolLibsUri = {lib["uri"]: lib for lib in SymbolTable}
libRename = []
def lib_entry(lib):
return "${KICAD_3RD_PARTY}/" + lib
msg = ""
for line in conv:
if line[1].endswith(".blk"):
msg += "\n" + line[0] + " rename to " + line[1]
else:
msg += "\n" + line[0] + " convert to " + line[1]
if lib_entry(line[0]) in SymbolLibsUri:
entry = SymbolLibsUri[lib_entry(line[0])]
tmp = {
"oldURI": entry["uri"],
"newURI": lib_entry(line[1]),
"name": entry["name"],
}
libRename.append(tmp)
msg_lib = ""
if len(libRename):
msg_lib += "The following changes must be made to the list of imported Symbol libs:\n"
for tmp in libRename:
msg_lib += f"\n{tmp['name']} : {tmp['oldURI']} \n-> {tmp['newURI']}"
msg_lib += "\n\n"
msg_lib += "It is necessary to adjust the settings of the imported symbol libraries in KiCad."
msg += "\n\n" + msg_lib
msg += "\n\nBackup files are also created automatically. "
msg += "These are named '*.blk'.\nShould the changes be applied?"
dlg = wx.MessageDialog(
None, msg, "WARNING", wx.KILL_OK | wx.ICON_WARNING | wx.CANCEL
)
if dlg.ShowModal() == wx.ID_OK:
print2GUI("Converted libraries:")
conv = convert_lib_list(libs2migrate, drymode=False)
for line in conv:
if line[1].endswith(".blk"):
print2GUI(line[0] + " rename to " + line[1])
else:
print2GUI(line[0] + " convert to " + line[1])
else:
return
if not len(msg_lib):
return
msg_dlg = "\nShould the change be made automatically? A restart of KiCad is then necessary to apply all changes."
dlg2 = wx.MessageDialog(
None, msg_lib + msg_dlg, "WARNING", wx.KILL_OK | wx.ICON_WARNING | wx.CANCEL
)
if dlg2.ShowModal() == wx.ID_OK:
for tmp in libRename:
print2GUI(f"\n{tmp['name']} : {tmp['oldURI']} \n-> {tmp['newURI']}")
backend_h.KiCad_Settings.sym_table_change_entry(
tmp["oldURI"], tmp["newURI"]
)
print2GUI("\nA restart of KiCad is then necessary to apply all changes.")
else:
print2GUI(msg_lib)
self.test_migrate_possible() # When everything has worked, the button disappears
event.Skip()
class ActionImpartPlugin(pcbnew.ActionPlugin):
def defaults(self):
plugin_dir = Path(__file__).resolve().parent
self.resources_dir = plugin_dir.parent.parent / "resources" / plugin_dir.name
self.plugin_dir = plugin_dir
self.name = "impartGUI"
self.category = "Import library files"
self.description = "Import library files from Octopart, Samacsys, Ultralibrarian, Snapeda and EasyEDA"
self.show_toolbar_button = True
self.icon_file_name = str(self.resources_dir / "icon.png")
self.dark_icon_file_name = self.icon_file_name
def Run(self):
# Use virtual env
# TODO: Does not work completely reliably
python_executable = activate_virtualenv(venv_dir=self.plugin_dir / "venv")
if not ensure_package("pydantic", python_executable):
print("Problems with loading", "pydantic")
if not ensure_package("easyeda2kicad", python_executable):
print("Problems with loading", "easyeda2kicad")
# Start GUI
board = pcbnew.GetBoard()
Impart_h = impart_frontend(board, self)
Impart_h.ShowModal()
Impart_h.Destroy()
if __name__ == "__main__":
app = wx.App()
frame = wx.Frame(None, title="KiCad Plugin")
Impart_t = impart_frontend(None, None)
Impart_t.ShowModal()
Impart_t.Destroy()

View File

@ -0,0 +1,214 @@
# Created with strong reference to:
# https://github.com/uPesy/easyeda2kicad.py/blob/master/easyeda2kicad/__main__.py
import os
import logging
logging.basicConfig(level=logging.ERROR)
from easyeda2kicad.easyeda.easyeda_api import EasyedaApi
from easyeda2kicad.kicad.parameters_kicad_symbol import KicadVersion
from easyeda2kicad.easyeda.parameters_easyeda import EeSymbol
from easyeda2kicad.helpers import add_component_in_symbol_lib_file
from easyeda2kicad.helpers import id_already_in_symbol_lib
from easyeda2kicad.helpers import update_component_in_symbol_lib_file
from easyeda2kicad.easyeda.easyeda_importer import Easyeda3dModelImporter
from easyeda2kicad.easyeda.easyeda_importer import EasyedaFootprintImporter
from easyeda2kicad.easyeda.easyeda_importer import EasyedaSymbolImporter
from easyeda2kicad.kicad.export_kicad_3d_model import Exporter3dModelKicad
from easyeda2kicad.kicad.export_kicad_footprint import ExporterFootprintKicad
from easyeda2kicad.kicad.export_kicad_symbol import ExporterSymbolKicad
class easyeda2kicad_wrapper:
def print(self, txt):
print("easyeda:" + txt)
def import_Symbol(
self,
cad_data,
output,
overwrite=False,
kicad_version=KicadVersion.v6,
sym_lib_ext="kicad_sym",
):
importer = EasyedaSymbolImporter(easyeda_cp_cad_data=cad_data)
easyeda_symbol: EeSymbol = importer.get_symbol()
is_id_already_in_symbol_lib = id_already_in_symbol_lib(
lib_path=f"{output}.{sym_lib_ext}",
component_name=easyeda_symbol.info.name,
kicad_version=kicad_version,
)
if not overwrite and is_id_already_in_symbol_lib:
self.print("Use overwrite option to update the older symbol")
return 1
exporter = ExporterSymbolKicad(
symbol=easyeda_symbol, kicad_version=kicad_version
)
kicad_symbol_lib = exporter.export(
footprint_lib_name=output.split("/")[-1].split(".")[0],
)
if is_id_already_in_symbol_lib:
update_component_in_symbol_lib_file(
lib_path=f"{output}.{sym_lib_ext}",
component_name=easyeda_symbol.info.name,
component_content=kicad_symbol_lib,
kicad_version=kicad_version,
)
else:
add_component_in_symbol_lib_file(
lib_path=f"{output}.{sym_lib_ext}",
component_content=kicad_symbol_lib,
kicad_version=kicad_version,
)
self.print(f"Created Kicad symbol {easyeda_symbol.info.name}")
print(f"Library path : {output}.{sym_lib_ext}")
def import_Footprint(
self, cad_data, output, overwrite=False, lib_name="EASYEDA2KICAD"
):
importer = EasyedaFootprintImporter(easyeda_cp_cad_data=cad_data)
easyeda_footprint = importer.get_footprint()
is_id_already_in_footprint_lib = os.path.isfile(
f"{output}.pretty/{easyeda_footprint.info.name}.kicad_mod"
)
if not overwrite and is_id_already_in_footprint_lib:
self.print("Use overwrite option to replace the older footprint")
return 1
ki_footprint = ExporterFootprintKicad(footprint=easyeda_footprint)
footprint_filename = f"{easyeda_footprint.info.name}.kicad_mod"
footprint_path = f"{output}.pretty"
model_3d_path = f"{output}.3dshapes".replace("\\", "/").replace("./", "/")
model_3d_path = f"${{{lib_name}}}/EasyEDA.3dshapes"
ki_footprint.export(
footprint_full_path=f"{footprint_path}/{footprint_filename}",
model_3d_path=model_3d_path,
)
self.print(f"Created Kicad footprint {easyeda_footprint.info.name}")
print(f"Footprint path: {os.path.join(footprint_path, footprint_filename)}")
def import_3D_Model(self, cad_data, output, overwrite=True):
model_3d = Easyeda3dModelImporter(
easyeda_cp_cad_data=cad_data, download_raw_3d_model=True
).output
if not model_3d:
self.print(f"No 3D model available for this component.")
return
exporter = Exporter3dModelKicad(model_3d=model_3d)
if exporter.output or exporter.output_step:
filename_wrl = f"{exporter.output.name}.wrl"
filename_step = f"{exporter.output.name}.step"
lib_path = f"{output}.3dshapes"
filepath_wrl = os.path.join(lib_path, filename_wrl)
filepath_step = os.path.join(lib_path, filename_step)
formats = ""
if os.path.exists(filepath_wrl) and not overwrite:
self.print(
f"3D model (wrl) exists:Use overwrite option to replace the 3D model"
)
return
else:
formats += "wrl"
if os.path.exists(filepath_step) and not overwrite:
self.print(
f"3D model (wrl) exists:Use overwrite option to replace the 3D model"
)
return
else:
formats += ",step"
exporter.export(lib_path=output)
self.print(f"Created 3D model {exporter.output.name} ({formats})")
if filename_wrl:
print("3D model path (wrl): " + filepath_wrl)
if filename_step:
print("3D model path (step): " + filepath_step)
def full_import(
self,
component_id="C2040",
base_folder=False,
overwrite=False,
lib_var="KICAD_3RD_PARTY",
):
base_folder = os.path.expanduser(base_folder)
if not component_id.startswith("C"):
self.print("lcsc_id should start by C.... example: C2040")
return False
if not os.path.isdir(base_folder):
os.makedirs(base_folder, exist_ok=True)
lib_name = "EasyEDA"
output = f"{base_folder}/{lib_name}"
# Create new footprint folder if it does not exist
if not os.path.isdir(f"{output}.pretty"):
os.mkdir(f"{output}.pretty")
self.print(f"Create {lib_name}.pretty footprint folder in {base_folder}")
# Create new 3d model folder if don't exist
if not os.path.isdir(f"{output}.3dshapes"):
os.mkdir(f"{output}.3dshapes")
self.print(f"Create {lib_name}.3dshapes 3D model folder in {base_folder}")
lib_extension = "kicad_sym"
if not os.path.isfile(f"{output}.{lib_extension}"):
with open(
file=f"{output}.{lib_extension}", mode="w+", encoding="utf-8"
) as my_lib:
my_lib.write(
"""\
(kicad_symbol_lib
(version 20211014)
(generator https://github.com/uPesy/easyeda2kicad.py)
)"""
)
self.print(f"Create {lib_name}.{lib_extension} symbol lib in {base_folder}")
# Get CAD data of the component using easyeda API
api = EasyedaApi()
cad_data = api.get_cad_data_of_component(lcsc_id=component_id)
# API returned no data
if not cad_data:
self.print(f"Failed to fetch data from EasyEDA API for part {component_id}")
return 1
# ---------------- SYMBOL ----------------
self.import_Symbol(cad_data, output, overwrite=overwrite)
# ---------------- FOOTPRINT -------------
self.import_Footprint(cad_data, output, overwrite=overwrite, lib_name=lib_var)
# ---------------- 3D MODEL --------------
self.import_3D_Model(cad_data, output, overwrite=overwrite)
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.ERROR)
base_folder = "~/Documents/Kicad/EasyEDA"
easyeda_import = easyeda2kicad_wrapper()
easyeda_import.full_import(component_id="C2040", base_folder=base_folder)

View File

@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
###########################################################################
## Python code generated with wxFormBuilder (version 4.2.1-0-g80c4cb6)
## http://www.wxformbuilder.org/
##
## PLEASE DO *NOT* EDIT THIS FILE!
###########################################################################
import wx
import wx.xrc
import wx.adv
###########################################################################
## Class impartGUI
###########################################################################
class impartGUI ( wx.Dialog ):
def __init__( self, parent ):
wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"impartGUI", pos = wx.DefaultPosition, size = wx.Size( 650,650 ), style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.BORDER_DEFAULT )
self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
self.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )
bSizer = wx.BoxSizer( wx.VERTICAL )
self.m_button_migrate = wx.Button( self, wx.ID_ANY, u"migrate the libraries (highly recommended)", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_button_migrate.SetFont( wx.Font( 15, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )
self.m_button_migrate.Hide()
self.m_button_migrate.SetMaxSize( wx.Size( -1,150 ) )
bSizer.Add( self.m_button_migrate, 1, wx.ALL|wx.EXPAND, 5 )
self.m_button = wx.Button( self, wx.ID_ANY, u"Start", wx.DefaultPosition, wx.DefaultSize, 0 )
bSizer.Add( self.m_button, 0, wx.ALL|wx.EXPAND, 5 )
self.m_text = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_BESTWRAP|wx.TE_MULTILINE )
bSizer.Add( self.m_text, 1, wx.ALL|wx.EXPAND, 5 )
self.m_staticline11 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
self.m_staticline11.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )
self.m_staticline11.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_GRAYTEXT ) )
self.m_staticline11.Hide()
bSizer.Add( self.m_staticline11, 0, wx.EXPAND |wx.ALL, 5 )
fgSizer2 = wx.FlexGridSizer( 0, 3, 0, 0 )
fgSizer2.SetFlexibleDirection( wx.HORIZONTAL )
fgSizer2.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_ALL )
self.m_buttonImportManual = wx.Button( self, wx.ID_ANY, u"Manual Import", wx.DefaultPosition, wx.DefaultSize, 0 )
fgSizer2.Add( self.m_buttonImportManual, 0, wx.ALL, 5 )
m_choice1Choices = [ u"EeasyEDA / LCSC Part#" ]
self.m_choice1 = wx.Choice( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, m_choice1Choices, 0 )
self.m_choice1.SetSelection( 0 )
fgSizer2.Add( self.m_choice1, 0, wx.ALL|wx.EXPAND, 5 )
self.m_textCtrl2 = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.TE_PROCESS_ENTER )
self.m_textCtrl2.SetMinSize( wx.Size( 220,-1 ) )
fgSizer2.Add( self.m_textCtrl2, 0, wx.EXPAND|wx.ALL, 5 )
bSizer.Add( fgSizer2, 0, wx.ALIGN_CENTER_HORIZONTAL, 5 )
self.m_staticline12 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
self.m_staticline12.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )
self.m_staticline12.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_GRAYTEXT ) )
bSizer.Add( self.m_staticline12, 0, wx.EXPAND |wx.ALL, 5 )
fgSizer1 = wx.FlexGridSizer( 0, 4, 0, 0 )
fgSizer1.SetFlexibleDirection( wx.BOTH )
fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
fgSizer1.SetMinSize( wx.Size( -1,0 ) )
self.m_autoImport = wx.CheckBox( self, wx.ID_ANY, u"auto background import", wx.DefaultPosition, wx.DefaultSize, 0 )
fgSizer1.Add( self.m_autoImport, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.m_overwrite = wx.CheckBox( self, wx.ID_ANY, u"overwrite existing lib", wx.DefaultPosition, wx.DefaultSize, 0 )
fgSizer1.Add( self.m_overwrite, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
self.m_check_import_all = wx.CheckBox( self, wx.ID_ANY, u"import old format", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_check_import_all.Enable( False )
self.m_check_import_all.Hide()
fgSizer1.Add( self.m_check_import_all, 0, wx.ALL, 5 )
self.m_check_autoLib = wx.CheckBox( self, wx.ID_ANY, u"auto KiCad setting", wx.DefaultPosition, wx.DefaultSize, 0 )
fgSizer1.Add( self.m_check_autoLib, 0, wx.ALL, 5 )
bSizer.Add( fgSizer1, 0, wx.ALIGN_CENTER, 5 )
self.m_staticText_sourcepath = wx.StaticText( self, wx.ID_ANY, u"Folder of the library to import:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText_sourcepath.Wrap( -1 )
bSizer.Add( self.m_staticText_sourcepath, 0, wx.ALL, 5 )
self.m_dirPicker_sourcepath = wx.DirPickerCtrl( self, wx.ID_ANY, u".", u"Select a folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_DEFAULT_STYLE )
bSizer.Add( self.m_dirPicker_sourcepath, 0, wx.ALL|wx.EXPAND, 5 )
self.m_staticText_librarypath = wx.StaticText( self, wx.ID_ANY, u"Library save location:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText_librarypath.Wrap( -1 )
bSizer.Add( self.m_staticText_librarypath, 0, wx.ALL, 5 )
self.m_dirPicker_librarypath = wx.DirPickerCtrl( self, wx.ID_ANY, u".", u"Select a folder", wx.DefaultPosition, wx.DefaultSize, wx.DIRP_DEFAULT_STYLE )
bSizer.Add( self.m_dirPicker_librarypath, 0, wx.ALL|wx.EXPAND, 5 )
self.m_staticline1 = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
self.m_staticline1.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_WINDOW ) )
self.m_staticline1.SetBackgroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_GRAYTEXT ) )
self.m_staticline1.Hide()
bSizer.Add( self.m_staticline1, 0, wx.EXPAND |wx.ALL, 5 )
self.m_staticText5 = wx.StaticText( self, wx.ID_ANY, u"There is no guarantee for faultless function. Use only at your own risk. Should there be any errors please write an issue.\nNecessary settings for the integration of the libraries can be found in the README:", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_staticText5.Wrap( -1 )
self.m_staticText5.Hide()
self.m_staticText5.SetMinSize( wx.Size( -1,50 ) )
bSizer.Add( self.m_staticText5, 0, wx.EXPAND|wx.TOP|wx.RIGHT|wx.LEFT, 5 )
self.m_hyperlink = wx.adv.HyperlinkCtrl( self, wx.ID_ANY, u"github.com/Steffen-W/Import-LIB-KiCad-Plugin", u"https://github.com/Steffen-W/Import-LIB-KiCad-Plugin", wx.DefaultPosition, wx.DefaultSize, wx.adv.HL_DEFAULT_STYLE )
bSizer.Add( self.m_hyperlink, 0, wx.BOTTOM|wx.RIGHT|wx.LEFT, 5 )
self.SetSizer( bSizer )
self.Layout()
self.Centre( wx.BOTH )
# Connect Events
self.Bind( wx.EVT_CLOSE, self.on_close )
self.m_button_migrate.Bind( wx.EVT_BUTTON, self.migrate_libs )
self.m_button.Bind( wx.EVT_BUTTON, self.BottonClick )
self.m_buttonImportManual.Bind( wx.EVT_BUTTON, self.ButtomManualImport )
self.m_textCtrl2.Bind( wx.EVT_TEXT_ENTER, self.ButtomManualImport )
self.m_dirPicker_sourcepath.Bind( wx.EVT_DIRPICKER_CHANGED, self.DirChange )
self.m_dirPicker_librarypath.Bind( wx.EVT_DIRPICKER_CHANGED, self.DirChange )
def __del__( self ):
pass
# Virtual event handlers, override them in your derived class
def on_close( self, event ):
event.Skip()
def migrate_libs( self, event ):
event.Skip()
def BottonClick( self, event ):
event.Skip()
def ButtomManualImport( self, event ):
event.Skip()
def DirChange( self, event ):
event.Skip()

View File

@ -0,0 +1,311 @@
import os.path
import json
import configparser
from pathlib import Path
import re
from .s_expression_parse import readFile2var, parse_sexp, convert_list_to_dicts
class filehandler:
def __init__(self, path):
self.path = ""
self.filelist = []
self.change_path(path)
def change_path(self, newpath):
if not os.path.isdir(newpath):
newpath = "."
if newpath != self.path:
self.filelist = []
self.path = newpath
def GetNewFiles(self, path):
if path != self.path:
self.change_path(path)
filelist = os.listdir(self.path)
filelist.sort()
newFiles = []
for i in filelist:
if i not in self.filelist and i.endswith(".zip"):
pathtemp = os.path.join(self.path, i)
# the file is less than 50 MB and larger 1kB
if (os.path.getsize(pathtemp) < 1000 * 1000 * 50) and (
os.path.getsize(pathtemp) > 1000
):
newFiles.append(pathtemp)
self.filelist.append(i)
return newFiles
class config_handler:
def __init__(self, config_path):
self.config = configparser.ConfigParser()
self.config_path = config_path
self.config_is_set = False
try:
self.config.read(self.config_path)
self.config["config"]["SRC_PATH"] # only for check
self.config["config"]["DEST_PATH"] # only for check
self.config_is_set = True
except:
self.print("An exception occurred during import " + self.config_path)
self.config = configparser.ConfigParser()
self.config.add_section("config")
self.config.set("config", "SRC_PATH", "")
self.config.set("config", "DEST_PATH", "")
if self.config["config"]["SRC_PATH"] == "":
self.config["config"]["SRC_PATH"] = str(Path.home() / "Downloads")
if self.config["config"]["DEST_PATH"] == "":
self.config["config"]["DEST_PATH"] = str(Path.home() / "KiCad")
self.config_is_set = False
def get_SRC_PATH(self):
return self.config["config"]["SRC_PATH"]
def set_SRC_PATH(self, var):
self.config["config"]["SRC_PATH"] = var
self.save_config()
def get_DEST_PATH(self):
return self.config["config"]["DEST_PATH"]
def set_DEST_PATH(self, var):
self.config["config"]["DEST_PATH"] = var
self.save_config()
def save_config(self):
with open(self.config_path, "w") as configfile:
self.config.write(configfile)
def print(self, text):
print(text)
class KiCad_Settings:
def __init__(self, SettingPath):
self.SettingPath = SettingPath
def get_sym_table(self):
path = os.path.join(self.SettingPath, "sym-lib-table")
return self.__parse_table__(path)
def set_sym_table(self, libname: str, libpath: str):
path = os.path.join(self.SettingPath, "sym-lib-table")
self.__add_entry_sexp__(path, name=libname, uri=libpath)
def sym_table_change_entry(self, old_uri, new_uri):
path = os.path.join(self.SettingPath, "sym-lib-table")
self.__update_uri_in_sexp__(path, old_uri=old_uri, new_uri=new_uri)
def get_lib_table(self):
path = os.path.join(self.SettingPath, "fp-lib-table")
return self.__parse_table__(path)
def set_lib_table_entry(self, libname: str):
path = os.path.join(self.SettingPath, "fp-lib-table")
uri_lib = "${KICAD_3RD_PARTY}/" + libname + ".pretty"
self.__add_entry_sexp__(path, name=libname, uri=uri_lib)
def __parse_table__(self, path):
sexp = readFile2var(path)
parsed = parse_sexp(sexp)
return convert_list_to_dicts(parsed)
def __update_uri_in_sexp__(
self,
path,
old_uri,
new_uri,
):
with open(path, "r") as file:
data = file.readlines()
entry_pattern = re.compile(
r'\s*\(lib \(name "(.*?)"\)\(type "(.*?)"\)\(uri "(.*?)"\)\(options "(.*?)"\)\(descr "(.*?)"\)\)\s*'
)
uri_found = False
for index, line in enumerate(data):
match = entry_pattern.match(line)
if match:
name, type_, uri, options, descr = match.groups()
if uri == old_uri:
# Create a new entry with the new URI
new_entry = f' (lib (name "{name}")(type "KiCad")(uri "{new_uri}")(options "{options}")(descr "{descr}"))\n'
print("old entry:", data[index], end="")
data[index] = new_entry
print("new entry:", data[index], end="")
uri_found = True
break
if not uri_found:
raise ValueError(f"URI '{old_uri}' not found in the file.")
with open(path, "w") as file:
file.writelines(data)
def __add_entry_sexp__(
self,
path,
name="Snapeda",
uri="${KICAD_3RD_PARTY}/Snapeda.pretty",
type="KiCad",
options="",
descr="",
):
table_entry = self.__parse_table__(path)
entries = {lib["name"]: lib for lib in table_entry}
if name in entries:
raise ValueError(f"Entry with the name '{name}' already exists.")
new_entry = f' (lib (name "{name}")(type "{type}")(uri "{uri}")(options "{options}")(descr "{descr}"))\n'
with open(path, "r") as file:
data = file.readlines()
# Insert the new entry before the last bracket character
insert_index = len(data) - 1
data.insert(insert_index, new_entry)
with open(path, "w") as file:
file.writelines(data)
def get_kicad_json(self):
path = os.path.join(self.SettingPath, "kicad.json")
with open(path) as json_data:
data = json.load(json_data)
return data
def set_kicad_json(self, kicad_json):
path = os.path.join(self.SettingPath, "kicad.json")
with open(path, "w") as file:
json.dump(kicad_json, file, indent=2)
def get_kicad_common(self):
path = os.path.join(self.SettingPath, "kicad_common.json")
with open(path) as json_data:
data = json.load(json_data)
return data
def set_kicad_common(self, kicad_common):
path = os.path.join(self.SettingPath, "kicad_common.json")
with open(path, "w") as file:
json.dump(kicad_common, file, indent=2)
def get_kicad_GlobalVars(self):
KiCadjson = self.get_kicad_common()
return KiCadjson["environment"]["vars"]
def check_footprintlib(self, SearchLib, add_if_possible=True):
msg = ""
FootprintTable = self.get_lib_table()
FootprintLibs = {lib["name"]: lib for lib in FootprintTable}
temp_path = "${KICAD_3RD_PARTY}/" + SearchLib + ".pretty"
if SearchLib in FootprintLibs:
if not FootprintLibs[SearchLib]["uri"] == temp_path:
msg += "\n" + SearchLib
msg += " in the Footprint Libraries is not imported correctly."
msg += "\nYou have to import the library " + SearchLib
msg += "' with the path '" + temp_path + "' in Footprint Libraries."
if add_if_possible:
msg += "\nThe entry must either be corrected manually or deleted."
# self.set_lib_table_entry(SearchLib) # TODO
else:
msg += "\n" + SearchLib + " is not in the Footprint Libraries."
if add_if_possible:
self.set_lib_table_entry(SearchLib)
msg += "\nThe library " + SearchLib
msg += " has been successfully added."
msg += "\n##### A restart of KiCad is necessary. #####"
else:
msg += "\nYou have to import the library " + SearchLib
msg += "' with the path '" + temp_path
msg += "' in the Footprint Libraries or select the automatic option."
return msg
def check_symbollib(self, SearchLib: str, add_if_possible: bool = True):
msg = ""
SearchLib_name = SearchLib.split(".")[0]
SearchLib_name_short = SearchLib_name.split("_")[0]
SymbolTable = self.get_sym_table()
SymbolLibs = {lib["name"]: lib for lib in SymbolTable}
SymbolLibsUri = {lib["uri"]: lib for lib in SymbolTable}
temp_path = "${KICAD_3RD_PARTY}/" + SearchLib
if not temp_path in SymbolLibsUri:
msg += "\n'" + temp_path + "' is not imported into the Symbol Libraries."
if add_if_possible:
if SearchLib_name_short not in SymbolLibs:
self.set_sym_table(SearchLib_name_short, temp_path)
msg += "\nThe library " + SearchLib
msg += " has been successfully added."
msg += "\n##### A restart of KiCad is necessary. #####"
elif SearchLib_name not in SymbolLibs:
self.set_sym_table(SearchLib_name, temp_path)
msg += "\nThe library " + SearchLib
msg += " has been successfully added."
msg += "\n##### A restart of KiCad is necessary. #####"
else:
msg += "\nThe entry must either be corrected manually or deleted."
# self.set_sym_table(SearchLib_name, temp_path) # TODO
else:
msg += "\nYou must add them manually or select the automatic option."
return msg
def check_GlobalVar(self, LocalLibFolder, add_if_possible=True):
msg = ""
GlobalVars = self.get_kicad_GlobalVars()
def setup_kicad_common():
kicad_common = self.get_kicad_common()
kicad_common["environment"]["vars"]["KICAD_3RD_PARTY"] = LocalLibFolder
self.set_kicad_common(kicad_common)
if GlobalVars and "KICAD_3RD_PARTY" in GlobalVars:
if not GlobalVars["KICAD_3RD_PARTY"] == LocalLibFolder:
msg += "\nKICAD_3RD_PARTY is defined as '"
msg += GlobalVars["KICAD_3RD_PARTY"]
msg += "' and not '" + LocalLibFolder + "'."
if add_if_possible:
setup_kicad_common()
msg += "\nThe entry was changed automatically."
msg += "\n##### A restart of KiCad is necessary. #####"
else:
msg += "\nChange the entry or select the automatic option."
else:
msg += "\nKICAD_3RD_PARTY" + " is not defined in Environment Variables."
if add_if_possible:
setup_kicad_common()
msg += "\nThe entry has been added successfully."
else:
msg += "\nYou must add them manually or select the automatic option."
return msg
if __name__ == "__main__":
import pcbnew
Manager = pcbnew.SETTINGS_MANAGER()
Setting = KiCad_Settings(Manager.GetUserSettingsPath())
SearchLib = "Samacsys"
LocalLibFolder = "~/KiCad"
Setting.check_footprintlib(SearchLib)
Setting.check_symbollib(SearchLib)
Setting.check_GlobalVar(SearchLib)

View File

@ -0,0 +1,166 @@
from pathlib import Path
import logging
from .kicad_cli import kicad_cli
logger = logging.getLogger(__name__)
cli = kicad_cli()
def find_old_lib_files(
folder_path: str,
libs: list[str] = ["Octopart", "Samacsys", "UltraLibrarian", "Snapeda", "EasyEDA"],
) -> list:
folder_path = Path(folder_path).expanduser()
found_files = {}
if not folder_path.exists():
return found_files
for file in folder_path.iterdir():
if not file.is_file():
continue
if not (file.name.endswith(".lib") or file.name.endswith(".kicad_sym")):
continue
for lib in libs:
if file.name.startswith(lib):
if lib in found_files:
entry = found_files[lib]
else:
entry = {}
# Check whether the file ends with ".lib"
if file.name.endswith(".lib"):
entry["old_lib"] = file
blk_file = file.with_suffix(".lib.blk")
if blk_file.exists() and blk_file.is_file():
entry["old_lib_blk"] = blk_file # backup file
dcm_file = file.with_suffix(".dcm")
if dcm_file.exists() and dcm_file.is_file():
entry["old_lib_dcm"] = dcm_file # description file
# Check whether the file with the old kicad v6 name exists
elif file.name.endswith("_kicad_sym.kicad_sym"):
entry["oldV6"] = file
dcm_file = file.with_suffix(".dcm")
if dcm_file.exists() and dcm_file.is_file():
entry["oldV6_dcm"] = dcm_file # description file
blk_file = file.with_suffix(".kicad_sym.blk")
if blk_file.exists() and blk_file.is_file():
entry["oldV6_blk"] = blk_file # backup file
# Check whether the file with the normal ".kicad_sym" extension exists
elif file.name.endswith(".kicad_sym"):
entry["V6"] = file
dcm_file = file.with_suffix(".dcm")
if dcm_file.exists() and dcm_file.is_file():
entry["V6_dcm"] = dcm_file # description file
blk_file = file.with_suffix(".kicad_sym.blk")
if blk_file.exists() and blk_file.is_file():
entry["V6_blk"] = blk_file # backup file
kicad_sym_file = file.with_name(lib + "_old_lib.kicad_sym")
if kicad_sym_file.exists() and kicad_sym_file.is_file():
# Possible conversion name
entry["old_lib_kicad_sym"] = kicad_sym_file
if entry:
found_files[lib] = entry
return found_files
def convert_lib(SRC: Path, DES: Path, drymode=True):
BLK_file = SRC.with_suffix(SRC.suffix + ".blk") # Backup
msg = []
if drymode:
msg.append([SRC.name, DES.name])
msg.append([SRC.name, BLK_file.name])
else:
SRC_dcm = SRC.with_suffix(".dcm")
DES_dcm = DES.with_suffix(".dcm")
if DES_dcm.exists() and DES_dcm.is_file():
return []
if not cli.upgrade_sym_lib(SRC, DES) or not DES.exists():
logger.error(f"converting {SRC.name} to {DES.name} produced an error")
return []
msg.append([SRC.stem, DES.stem])
if SRC_dcm.exists() and SRC_dcm.is_file():
SRC_dcm.rename(DES_dcm)
SRC.rename(BLK_file)
msg.append([SRC.name, BLK_file.name])
return msg
def convert_lib_list(libs_dict, drymode=True):
if not cli.exists():
logger.error("kicad_cli not found! Conversion is not possible.")
drymode = True
convertlist = []
for lib, paths in libs_dict.items():
# if "V6" in paths:
# print(f"No conversion needed for {lib}.")
if "old_lib" in paths:
file = paths["old_lib"]
if "V6" in paths or "oldV6" in paths:
if "old_lib_kicad_sym" in paths:
logger.error(f"{lib} old_lib_kicad_sym already exists")
else:
kicad_sym_file = file.with_name(file.stem + "_old_lib.kicad_sym")
res = convert_lib(SRC=file, DES=kicad_sym_file, drymode=drymode)
convertlist.extend(res)
else:
name_V6 = file.with_name(lib + ".kicad_sym")
res = convert_lib(SRC=file, DES=name_V6, drymode=drymode)
convertlist.extend(res)
if "oldV6" in paths:
file = paths["oldV6"]
if "V6" in paths:
logger.error(f"{lib} V6 already exists")
else:
name_V6 = file.with_name(lib + ".kicad_sym")
res = convert_lib(SRC=file, DES=name_V6, drymode=drymode)
convertlist.extend(res)
return convertlist
if __name__ == "__main__":
from pprint import pprint
logging.basicConfig(level=logging.INFO)
path = "~/KiCad"
ret = find_old_lib_files(folder_path=path)
if ret:
print("#######################")
pprint(ret)
print("#######################")
conv = convert_lib_list(ret, drymode=True)
if conv:
print("#######################")
pprint(conv)
print("#######################")

View File

@ -0,0 +1,65 @@
import subprocess
class kicad_cli:
def run_kicad_cli(self, command):
try:
result = subprocess.run(
["kicad-cli"] + command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# print(result.stdout.strip())
return True
except subprocess.CalledProcessError as e:
print(" ".join(["kicad-cli"] + command))
print(f"Error: {e.stderr}")
return None
def exists(self):
def version_to_tuple(version_str):
try:
return tuple(map(int, version_str.split("-")[0].split(".")))
except (ValueError, AttributeError) as e:
print(f"Version extractions error '{version_str}': {e}")
return None
try:
result = subprocess.run(
["kicad-cli", "--version"],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
version = result.stdout.strip()
minVersion = "8.0.4"
KiCadVers = version_to_tuple(version)
if not KiCadVers or KiCadVers < version_to_tuple(minVersion):
print("KiCad Version", version)
print("Minimum required KiCad version is", minVersion)
return False
else:
return True
except (subprocess.CalledProcessError, FileNotFoundError):
print("kicad-cli does not exist")
return False
def upgrade_sym_lib(self, input_file, output_file):
return self.run_kicad_cli(["sym", "upgrade", input_file, "-o", output_file])
def upgrade_footprint_lib(self, pretty_folder):
return self.run_kicad_cli(["fp", "upgrade", pretty_folder])
if __name__ == "__main__":
cli = kicad_cli()
if cli.exists():
input_file = "UltraLibrarian_kicad_sym.kicad_sym"
output_file = "UltraLibrarian.kicad_sym"
cli.upgrade_sym_lib(input_file, output_file)
cli.upgrade_footprint_lib("test.pretty")

View File

@ -0,0 +1,141 @@
import re
from os.path import exists, expanduser
term_regex = r"""(?mx)
\s*(?:
(?P<brackl>\()|
(?P<brackr>\))|
(?P<num>\-?\d+\.\d+|\-?\d+)|
(?P<sq>"[^"]*")|
(?P<s>[^(^)\s]+)
)"""
# taken from https://rosettacode.org/wiki/S-Expressions#Python
def parse_sexp(sexp, dbg=False):
stack = []
out = []
if dbg:
print("%-6s %-14s %-44s %-s" % tuple("term value out stack".split()))
for termtypes in re.finditer(term_regex, sexp):
term, value = [(t, v) for t, v in termtypes.groupdict().items() if v][0]
if dbg:
print("%-7s %-14s %-44r %-r" % (term, value, out, stack))
if term == "brackl":
stack.append(out)
out = []
elif term == "brackr":
assert stack, "Trouble with nesting of brackets"
tmpout, out = out, stack.pop(-1)
out.append(tmpout)
elif term == "num":
v = float(value)
if v.is_integer():
v = int(v)
out.append(v)
elif term == "sq":
out.append(value[1:-1])
elif term == "s":
out.append(value)
else:
raise NotImplementedError("Error: %r" % (term, value))
assert not stack, "Trouble with nesting of brackets"
return out[0]
def print_sexp(exp):
out = ""
if type(exp) == type([]):
out += "(" + " ".join(print_sexp(x) for x in exp) + ")"
elif type(exp) == type("") and re.search(r"[\s()]", exp):
out += '"%s"' % repr(exp)[1:-1].replace('"', '"')
else:
out += "%s" % exp
return out
def readFile2var(path):
path = expanduser(path)
if not exists(path):
return None
with open(path, "r") as file:
data = file.read()
return data
def convert_list_to_dicts(data):
dict_list = []
for entry in data:
if not type(entry) == list:
continue
entry_dict = {}
for item in entry:
if type(item) == list and len(item) == 2:
key, value = item
entry_dict[key] = value
if len(entry_dict):
dict_list.append(entry_dict)
return dict_list
def search_recursive(line: list, entry: str, all=False):
if type(line[0]) == str and line[0] == entry:
if all:
return line
else:
return line[1]
for e in line:
if type(e) == list:
res = search_recursive(line=e, entry=entry, all=all)
if not res == None:
return res
return None
def extract_properties(sexp_list: list):
"""
Extracts all “property” entries from a nested list and saves them in a dictionary.
:param sexp_list: The nested list
:return: A dictionary with “property” keys and their values
"""
properties = {}
def traverse_list(lst):
"""Recursive function to run through the list and extract 'property' entries."""
if isinstance(lst, list):
if len(lst) > 1 and lst[0] == "property":
key = lst[1]
value = lst[2] if len(lst) > 2 else None
properties[key] = value
for item in lst:
traverse_list(item)
traverse_list(sexp_list)
return properties
if __name__ == "__main__":
from pprint import pprint
sexp = """(sym_lib_table
(version 7)
(lib (name "4xxx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/4xxx.kicad_sym")(options "")(descr "4xxx series symbols"))
(lib (name "4xxx_IEEE")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/4xxx_IEEE.kicad_sym")(options "")(descr "4xxx series IEEE symbols"))
(lib (name "74xGxx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xGxx.kicad_sym")(options "")(descr "74xGxx symbols"))
(lib (name "74xx")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xx.kicad_sym")(options "")(descr "74xx symbols"))
(lib (name "74xx_IEEE")(type "KiCad")(uri "${KICAD7_SYMBOL_DIR}/74xx_IEEE.kicad_sym")(options "")(descr "74xx series IEEE symbols"))
)"""
dir_ = "~/.config/kicad/8.0/sym-lib-table"
dir_ = "~/.config/kicad/8.0/fp-lib-table"
sexp = readFile2var(dir_)
parsed = parse_sexp(sexp)
# pprint(parsed)
parsed_dict = convert_list_to_dicts(parsed[0:10])
pprint(parsed_dict)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
home = /Applications/KiCad/KiCad.app/Contents/MacOS
include-system-site-packages = false
version = 3.9.13