All 4 scripts now accept CLI arguments via argparse instead of hardcoded paths, making them reusable across hardware versions and ready for MCP server integration: - extract_easyeda_db.py: --db-path, --output-dir, --verbose - extract_bom.py: --extract-root or --epro-dir, --output-dir, --versions - generate_jlcpcb_bom.py: --bom-dir, --version, --output-file - convert_to_kicad.py: --bom-dir, --kicad-lib-dir, --versions Core logic in each script is refactored into callable functions that return results, enabling programmatic use from MCP tools.
203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract schematic data from EasyEDA Pro project databases (.eprj).
|
|
|
|
Parses the SQLite-backed .eprj file, decompresses schematic documents,
|
|
and outputs raw JSON schematics, a basic netlist graph, and BOM data
|
|
for each hardware version found in the project.
|
|
|
|
Usage:
|
|
python extract_easyeda_db.py --db-path /path/to/project.eprj --output-dir /path/to/output
|
|
python extract_easyeda_db.py --help
|
|
"""
|
|
import argparse
|
|
import sqlite3
|
|
import base64
|
|
import zlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
def decompress_dataStr(b64_str):
|
|
if not b64_str.startswith("base64"):
|
|
return b64_str
|
|
|
|
b64_data = b64_str[6:]
|
|
compressed = base64.b64decode(b64_data)
|
|
|
|
decompressed = None
|
|
for wbits in [31, 15, -15]:
|
|
try:
|
|
decompressed = zlib.decompress(compressed, wbits)
|
|
break
|
|
except zlib.error:
|
|
continue
|
|
|
|
if decompressed is None:
|
|
raise ValueError("Could not decompress data")
|
|
|
|
return decompressed.decode('utf-8', errors='ignore')
|
|
|
|
|
|
def parse_json_lines(decompressed_str):
|
|
lines = decompressed_str.strip().split('\n')
|
|
parsed = []
|
|
for line in lines:
|
|
if not line.strip(): continue
|
|
try:
|
|
parsed.append(json.loads(line))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return parsed
|
|
|
|
|
|
def extract_db(db_path, output_dir_base, verbose=False):
|
|
"""Extract schematic data from an EasyEDA Pro project database.
|
|
|
|
Args:
|
|
db_path: Path to the .eprj SQLite database.
|
|
output_dir_base: Base directory for output files.
|
|
verbose: If True, print extra details during extraction.
|
|
|
|
Returns:
|
|
dict: Mapping of version names to their output directories.
|
|
"""
|
|
conn = sqlite3.connect(db_path)
|
|
c = conn.cursor()
|
|
|
|
# 1. Get projects/versions mapping
|
|
c.execute("SELECT boards FROM projects LIMIT 1")
|
|
row = c.fetchone()
|
|
if not row:
|
|
print("No project found.")
|
|
return {}
|
|
|
|
boards = json.loads(row[0])
|
|
|
|
# 2. Get components from components table
|
|
c.execute("SELECT uuid, title, dataStr FROM components")
|
|
components_db = {}
|
|
for cuuid, title, dataStr in c.fetchall():
|
|
try:
|
|
comp_data_str = decompress_dataStr(dataStr)
|
|
comp_data = parse_json_lines(comp_data_str)
|
|
components_db[cuuid] = {
|
|
"title": title,
|
|
"data": comp_data
|
|
}
|
|
except Exception as e:
|
|
if verbose:
|
|
print(f"Failed to parse component {cuuid}: {e}")
|
|
|
|
results = {}
|
|
for board in boards:
|
|
version_name = board.get("name")
|
|
sch_uuid = board.get("sch")
|
|
if not version_name or not sch_uuid:
|
|
continue
|
|
|
|
print(f"Processing version: {version_name}")
|
|
|
|
# Determine the target directory format (e.g., Savearth_v2_8 -> v2.8)
|
|
v_folder = version_name.lower().replace("savearth_", "").replace("savearth ", "").replace("_", ".")
|
|
if not v_folder.startswith("v"):
|
|
v_folder = "v" + v_folder
|
|
|
|
out_dir = os.path.join(output_dir_base, v_folder, "parsed_data")
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
results[version_name] = out_dir
|
|
|
|
# 3. Get all schematic documents for this sch_uuid
|
|
c.execute("SELECT title, docType, dataStr FROM documents WHERE schematic_uuid=?", (sch_uuid,))
|
|
docs = c.fetchall()
|
|
|
|
schematic_data = {}
|
|
netlist_graph = {"nets": {}, "components": []}
|
|
|
|
for doc_title, doc_type, dataStr in docs:
|
|
try:
|
|
decomp = decompress_dataStr(dataStr)
|
|
parsed = parse_json_lines(decomp)
|
|
schematic_data[doc_title] = parsed
|
|
|
|
# Very basic Netlist graph extraction
|
|
for item in parsed:
|
|
if not isinstance(item, list) or len(item) == 0:
|
|
continue
|
|
item_type = item[0]
|
|
if item_type == "COMPONENT":
|
|
netlist_graph["components"].append(item)
|
|
elif item_type == "WIRE":
|
|
# item[1] is usually the UUID, subsequent elements are coords/nets
|
|
net_id = item[1]
|
|
netlist_graph["nets"][net_id] = item
|
|
|
|
except Exception as e:
|
|
if verbose:
|
|
print(f"Failed to parse document {doc_title}: {e}")
|
|
|
|
# 4. Write Schematic Raw JSON
|
|
with open(os.path.join(out_dir, "schematic_raw.json"), "w") as f:
|
|
json.dump(schematic_data, f, indent=2)
|
|
|
|
# 5. Write basic Netlist JSON
|
|
with open(os.path.join(out_dir, "netlist.json"), "w") as f:
|
|
json.dump(netlist_graph, f, indent=2)
|
|
|
|
# 6. Build BOM from the components used in the schematic
|
|
bom_extended = []
|
|
for doc_title, parsed in schematic_data.items():
|
|
for item in parsed:
|
|
if isinstance(item, list) and len(item) > 0 and item[0] == "COMPONENT":
|
|
# The component instance in the schematic
|
|
# Usually item[1] is instance UUID, item[2] is component DB UUID
|
|
if len(item) > 2:
|
|
db_uuid = item[2]
|
|
if db_uuid in components_db:
|
|
bom_extended.append({
|
|
"instance_id": item[1],
|
|
"component_id": db_uuid,
|
|
"details": components_db[db_uuid]["title"]
|
|
})
|
|
|
|
with open(os.path.join(out_dir, "bom_extended.json"), "w") as f:
|
|
json.dump(bom_extended, f, indent=2)
|
|
|
|
if verbose:
|
|
print(f" → {out_dir}: {len(schematic_data)} docs, {len(bom_extended)} components")
|
|
|
|
conn.close()
|
|
print("Extraction complete.")
|
|
return results
|
|
|
|
|
|
def build_parser():
|
|
"""Build and return the argument parser."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Extract schematic data from EasyEDA Pro project databases (.eprj).",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""\
|
|
Examples:
|
|
%(prog)s --db-path hardware/v2.8/easyeda/project.eprj --output-dir hardware/
|
|
%(prog)s --db-path project.eprj --output-dir ./output --verbose
|
|
"""
|
|
)
|
|
parser.add_argument(
|
|
"--db-path", required=True,
|
|
help="Path to the EasyEDA Pro project database (.eprj SQLite file)."
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir", required=True,
|
|
help="Base output directory for extracted data (version subdirs will be created)."
|
|
)
|
|
parser.add_argument(
|
|
"--verbose", action="store_true",
|
|
help="Print extra details during extraction."
|
|
)
|
|
return parser
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = build_parser()
|
|
args = parser.parse_args()
|
|
extract_db(args.db_path, args.output_dir, verbose=args.verbose)
|