923 lines
35 KiB
Python
923 lines
35 KiB
Python
#!/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,
|
|
)
|