sistemas-cibernetico-fisicos/hardware/common/scripts/extract_easyeda_db.py
Fábio Cunha 30b85f346a refactor(scripts): make hardware utility scripts version-agnostic with argparse
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.
2026-05-25 18:05:29 +01:00

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)