Commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
output/
|
||||||
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run nexarxclaude",
|
||||||
|
"type": "debugpy",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/nexarxclaude.py",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"envFile": "${workspaceFolder}/.env"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
207
nexarxclaude.py
207
nexarxclaude.py
@@ -97,6 +97,11 @@ query GetSheetComponents($projectId: ID!, $cursor: String) {
|
|||||||
}
|
}
|
||||||
nodes {
|
nodes {
|
||||||
designator
|
designator
|
||||||
|
description
|
||||||
|
parameters {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
component {
|
component {
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
@@ -254,26 +259,44 @@ def normalise(s: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
_MFR_ALIASES = {
|
_MFR_ALIASES = {
|
||||||
"ti": "texasinstruments",
|
"ti": "texasinstruments",
|
||||||
"texas": "texasinstruments",
|
"texas": "texasinstruments",
|
||||||
"texasinstruments": "texasinstruments",
|
"texasinstruments": "texasinstruments",
|
||||||
"onsemi": "onsemi",
|
"onsemi": "onsemi",
|
||||||
"onsemiconductor": "onsemi",
|
"onsemiconductor": "onsemi",
|
||||||
"onsemin": "onsemi",
|
"onsemin": "onsemi",
|
||||||
"nxp": "nxp",
|
"fairchild": "onsemi", # acquired by ON Semi 2016
|
||||||
"nxpsemiconductors": "nxp",
|
"fairchildsemiconductor": "onsemi",
|
||||||
"st": "stmicro",
|
"nxp": "nxp",
|
||||||
"stmicro": "stmicro",
|
"nxpsemiconductors": "nxp",
|
||||||
"stmicroelectronics": "stmicro",
|
"nexperia": "nxp", # spun off from NXP
|
||||||
"abracon": "abracon",
|
"st": "stmicro",
|
||||||
"abraconcorporation": "abracon",
|
"stmicro": "stmicro",
|
||||||
"ck": "ck",
|
"stmicroelectronics": "stmicro",
|
||||||
"ckcomponents": "ck",
|
"abracon": "abracon",
|
||||||
"murata": "murata",
|
"abraconcorporation": "abracon",
|
||||||
"muratamfg": "murata",
|
"ck": "ck",
|
||||||
"muratamanufacturing": "murata",
|
"ckcomponents": "ck",
|
||||||
"kingbright": "kingbright",
|
"murata": "murata",
|
||||||
"kingbight": "kingbright", # known Nexar typo
|
"muratamfg": "murata",
|
||||||
|
"muratamanufacturing": "murata",
|
||||||
|
"kingbright": "kingbright",
|
||||||
|
"kingbight": "kingbright", # known Nexar typo
|
||||||
|
"tdk": "tdk",
|
||||||
|
"epcos": "tdk", # acquired by TDK
|
||||||
|
"tdkepcos": "tdk",
|
||||||
|
"analogdevices": "analogdevices",
|
||||||
|
"analog": "analogdevices",
|
||||||
|
"maxim": "analogdevices", # acquired by Analog Devices 2021
|
||||||
|
"maximintegrated": "analogdevices",
|
||||||
|
"teconnectivity": "teconnectivity",
|
||||||
|
"tyco": "teconnectivity",
|
||||||
|
"raychem": "teconnectivity", # Raychem is a TE Connectivity brand
|
||||||
|
"fujitsu": "fujitsu",
|
||||||
|
"ramxeed": "fujitsu", # RAMXEED distributes Fujitsu memory
|
||||||
|
"ramxeedfujitsu": "fujitsu",
|
||||||
|
"taiyoyuden": "taiyoyuden",
|
||||||
|
"tayoyuden": "taiyoyuden", # common BOM spelling variant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -482,10 +505,71 @@ def _find_passive_bom_match(bom_data: list, designator: str, nexar_params: dict,
|
|||||||
|
|
||||||
MAX_MFR_PARTS = 5
|
MAX_MFR_PARTS = 5
|
||||||
|
|
||||||
def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_data: dict | None) -> str:
|
def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_data: dict | None, raw_mode: bool = False) -> str:
|
||||||
wb = openpyxl.Workbook()
|
wb = openpyxl.Workbook()
|
||||||
wb.remove(wb.active)
|
wb.remove(wb.active)
|
||||||
|
|
||||||
|
if raw_mode:
|
||||||
|
def _get_params(node):
|
||||||
|
"""Merge component-level and instance-level parameters. Instance wins on conflict."""
|
||||||
|
comp = node.get("component") or {}
|
||||||
|
comp_params = {p["name"]: (p.get("value") or "").strip()
|
||||||
|
for p in (comp.get("details") or {}).get("parameters") or []
|
||||||
|
if p.get("name")}
|
||||||
|
inst_params = {p["name"]: (p.get("value") or "").strip()
|
||||||
|
for p in (node.get("parameters") or [])
|
||||||
|
if p.get("name")}
|
||||||
|
return {**comp_params, **inst_params}
|
||||||
|
|
||||||
|
def _find_param(params: dict, name: str) -> str:
|
||||||
|
"""Case-insensitive parameter lookup."""
|
||||||
|
name_lower = name.lower()
|
||||||
|
for k, v in params.items():
|
||||||
|
if k.lower() == name_lower:
|
||||||
|
return v
|
||||||
|
return ""
|
||||||
|
|
||||||
|
headers = ["Designator", "Reference", "Description", "Manufacturer 1", "Manufacturer part number 1"]
|
||||||
|
|
||||||
|
col_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
for sheet in schematics:
|
||||||
|
sheet_name = sheet["documentName"] or sheet["documentId"]
|
||||||
|
safe_name = re.sub(r"[\\/*?:\[\]]", "_", sheet_name)[:31]
|
||||||
|
ws = wb.create_sheet(title=safe_name)
|
||||||
|
ws.append(headers)
|
||||||
|
for i in range(len(headers)):
|
||||||
|
col = col_letters[i] if i < 26 else col_letters[i // 26 - 1] + col_letters[i % 26]
|
||||||
|
ws.column_dimensions[col].width = 30 if i >= 3 else 16
|
||||||
|
|
||||||
|
nodes = (sheet.get("designItems") or {}).get("nodes") or []
|
||||||
|
for node in sorted(nodes, key=lambda n: n.get("designator") or ""):
|
||||||
|
comp = node.get("component") or {}
|
||||||
|
params = _get_params(node)
|
||||||
|
part_number = _find_param(params, "PartNumber")
|
||||||
|
if not part_number:
|
||||||
|
continue # non-BOM item — skip
|
||||||
|
description = (
|
||||||
|
node.get("description") or
|
||||||
|
comp.get("description") or
|
||||||
|
_find_param(params, "Description") or
|
||||||
|
""
|
||||||
|
)
|
||||||
|
row = [
|
||||||
|
node.get("designator") or "",
|
||||||
|
part_number,
|
||||||
|
description,
|
||||||
|
_find_param(params, "Manufacturer 1"),
|
||||||
|
_find_param(params, "Manufacturer part number 1"),
|
||||||
|
]
|
||||||
|
ws.append(row)
|
||||||
|
|
||||||
|
output_dir = os.path.join(os.path.dirname(__file__), "output")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
safe_project = re.sub(r'[\\/:*?"<>|]', "_", project_name).strip()
|
||||||
|
filename = os.path.join(output_dir, f"{safe_project} - {variant_name} (raw).xlsx")
|
||||||
|
wb.save(filename)
|
||||||
|
return filename
|
||||||
|
|
||||||
# Build header row
|
# Build header row
|
||||||
mfr_headers = []
|
mfr_headers = []
|
||||||
for n in range(1, MAX_MFR_PARTS + 1):
|
for n in range(1, MAX_MFR_PARTS + 1):
|
||||||
@@ -543,9 +627,19 @@ def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_d
|
|||||||
if not match:
|
if not match:
|
||||||
match = _find_passive_bom_match(bom_data, designator, params, nexar_desc)
|
match = _find_passive_bom_match(bom_data, designator, params, nexar_desc)
|
||||||
|
|
||||||
|
# --- PartNumber fallback (local Altium library) ---
|
||||||
|
if not match and not valid_pairs:
|
||||||
|
part_number = params.get("PartNumber", "").strip()
|
||||||
|
if part_number and part_number != "-":
|
||||||
|
candidates = [e for e in bom_data if _mpn_match(e["mpn"], part_number)]
|
||||||
|
match = max(candidates, key=lambda e: e["unit_cost"]) if candidates else None
|
||||||
|
|
||||||
# --- Diagnostics when no match ---
|
# --- Diagnostics when no match ---
|
||||||
if not match:
|
if not match:
|
||||||
if not valid_pairs:
|
if not valid_pairs:
|
||||||
|
if not hasattr(export_to_xlsx, '_local_lib_sample_printed') and params:
|
||||||
|
export_to_xlsx._local_lib_sample_printed = True
|
||||||
|
print(f" [Sample local library params for {designator}]: {dict(list(params.items())[:15])}")
|
||||||
print(f" NO MATCH [{designator}] {ref}: no manufacturer data in Nexar")
|
print(f" NO MATCH [{designator}] {ref}: no manufacturer data in Nexar")
|
||||||
else:
|
else:
|
||||||
tried_mpns = [p for _, p in valid_pairs]
|
tried_mpns = [p for _, p in valid_pairs]
|
||||||
@@ -574,6 +668,7 @@ def export_to_xlsx(project_name: str, variant_name: str, schematics: list, bom_d
|
|||||||
ws.append(row)
|
ws.append(row)
|
||||||
|
|
||||||
output_dir = os.path.join(os.path.dirname(__file__), "output")
|
output_dir = os.path.join(os.path.dirname(__file__), "output")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
safe_project = re.sub(r'[\\/:*?"<>|]', "_", project_name).strip()
|
safe_project = re.sub(r'[\\/:*?"<>|]', "_", project_name).strip()
|
||||||
filename = os.path.join(output_dir, f"{safe_project} - {variant_name}.xlsx")
|
filename = os.path.join(output_dir, f"{safe_project} - {variant_name}.xlsx")
|
||||||
wb.save(filename)
|
wb.save(filename)
|
||||||
@@ -639,19 +734,29 @@ def main():
|
|||||||
bom_dir = os.path.join(os.path.dirname(__file__), "BOM")
|
bom_dir = os.path.join(os.path.dirname(__file__), "BOM")
|
||||||
bom_files = sorted(f for f in os.listdir(bom_dir) if f.lower().endswith(".xlsx"))
|
bom_files = sorted(f for f in os.listdir(bom_dir) if f.lower().endswith(".xlsx"))
|
||||||
|
|
||||||
if not bom_files:
|
|
||||||
print(f"\nNo xlsx files found in {bom_dir}.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"\nBOM files available:")
|
print(f"\nBOM files available:")
|
||||||
print("-" * 60)
|
print("-" * 60)
|
||||||
|
print(f" [0] Skip BOM matching — raw parameter export")
|
||||||
for i, name in enumerate(bom_files, start=1):
|
for i, name in enumerate(bom_files, start=1):
|
||||||
print(f" [{i}] {name}")
|
print(f" [{i}] {name}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
choice = prompt_choice(f"Select a BOM file (1-{len(bom_files)}): ", len(bom_files))
|
while True:
|
||||||
selected_bom = os.path.join(bom_dir, bom_files[choice - 1])
|
try:
|
||||||
print(f"\nSelected BOM: {bom_files[choice - 1]}")
|
choice = int(input(f"Select a BOM file (0-{len(bom_files)}): "))
|
||||||
|
if 0 <= choice <= len(bom_files):
|
||||||
|
break
|
||||||
|
print(f" Please enter a number between 0 and {len(bom_files)}.")
|
||||||
|
except ValueError:
|
||||||
|
print(" Please enter a valid number.")
|
||||||
|
|
||||||
|
raw_mode = (choice == 0)
|
||||||
|
if raw_mode:
|
||||||
|
selected_bom = None
|
||||||
|
print("\nRaw parameter export selected — BOM matching skipped.")
|
||||||
|
else:
|
||||||
|
selected_bom = os.path.join(bom_dir, bom_files[choice - 1])
|
||||||
|
print(f"\nSelected BOM: {bom_files[choice - 1]}")
|
||||||
|
|
||||||
# --- Fetch schematic sheet list ---
|
# --- Fetch schematic sheet list ---
|
||||||
print("\nFetching schematic sheets...")
|
print("\nFetching schematic sheets...")
|
||||||
@@ -717,32 +822,50 @@ def main():
|
|||||||
bom_variants = data["desProjectById"]["design"]["workInProgress"].get("variants") or []
|
bom_variants = data["desProjectById"]["design"]["workInProgress"].get("variants") or []
|
||||||
bom_variant = next((v for v in bom_variants if v["name"] == selected_variant["name"]), None)
|
bom_variant = next((v for v in bom_variants if v["name"] == selected_variant["name"]), None)
|
||||||
|
|
||||||
|
if not bom_variant:
|
||||||
|
available = [v["name"] for v in bom_variants]
|
||||||
|
print(f" Warning: variant '{selected_variant['name']}' not found in BOM query.")
|
||||||
|
print(f" Available variant names: {available}")
|
||||||
|
|
||||||
fitted_designators = set()
|
fitted_designators = set()
|
||||||
if bom_variant:
|
if bom_variant:
|
||||||
|
total_instances = 0
|
||||||
for bom_item in (bom_variant.get("bom") or {}).get("bomItems") or []:
|
for bom_item in (bom_variant.get("bom") or {}).get("bomItems") or []:
|
||||||
for instance in bom_item.get("bomItemInstances") or []:
|
for instance in bom_item.get("bomItemInstances") or []:
|
||||||
|
total_instances += 1
|
||||||
if instance.get("isFitted"):
|
if instance.get("isFitted"):
|
||||||
fitted_designators.add(instance["designator"])
|
fitted_designators.add(instance["designator"])
|
||||||
|
if not total_instances:
|
||||||
|
print(f" Warning: variant found but no BOM instances returned — BOM may not be generated in Altium 365 for this variant.")
|
||||||
|
elif not fitted_designators:
|
||||||
|
print(f" Warning: {total_instances} BOM instance(s) found but none have isFitted=True.")
|
||||||
|
|
||||||
# Filter each sheet's nodes to fitted components only
|
# Filter each sheet's nodes to fitted components only.
|
||||||
for s in schematics:
|
# If no fitted designator data is available, export all components unfiltered.
|
||||||
s["designItems"]["nodes"] = [
|
if fitted_designators:
|
||||||
n for n in s["designItems"]["nodes"]
|
for s in schematics:
|
||||||
if n.get("designator") in fitted_designators
|
s["designItems"]["nodes"] = [
|
||||||
]
|
n for n in s["designItems"]["nodes"]
|
||||||
|
if n.get("designator") in fitted_designators
|
||||||
total_components = sum(len(s["designItems"]["nodes"]) for s in schematics)
|
]
|
||||||
print(f"Total: {total_components} fitted component(s) across {len(schematics)} sheet(s).")
|
total_components = sum(len(s["designItems"]["nodes"]) for s in schematics)
|
||||||
|
print(f"Total: {total_components} fitted component(s) across {len(schematics)} sheet(s).")
|
||||||
|
else:
|
||||||
|
total_components = sum(len(s["designItems"]["nodes"]) for s in schematics)
|
||||||
|
print(f"No fitted/unfitted variant data found — exporting all {total_components} component(s) across {len(schematics)} sheet(s).")
|
||||||
|
|
||||||
# --- Load BOM cross-reference data ---
|
# --- Load BOM cross-reference data ---
|
||||||
bom_data = load_bom_data(selected_bom)
|
if raw_mode:
|
||||||
if bom_data is None:
|
bom_data = None
|
||||||
print("No 'bom' tab found in the selected BOM file — exporting without BOM data.")
|
|
||||||
else:
|
else:
|
||||||
print(f"Loaded {len(bom_data)} entries from BOM.")
|
bom_data = load_bom_data(selected_bom)
|
||||||
|
if bom_data is None:
|
||||||
|
print("No 'bom' tab found in the selected BOM file — exporting without BOM data.")
|
||||||
|
else:
|
||||||
|
print(f"Loaded {len(bom_data)} entries from BOM.")
|
||||||
|
|
||||||
# --- Export ---
|
# --- Export ---
|
||||||
filename = export_to_xlsx(selected_project["name"], selected_variant["name"], schematics, bom_data)
|
filename = export_to_xlsx(selected_project["name"], selected_variant["name"], schematics, bom_data, raw_mode=raw_mode)
|
||||||
print(f"\nExported to: {filename}")
|
print(f"\nExported to: {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user