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 {
|
||||
designator
|
||||
description
|
||||
parameters {
|
||||
name
|
||||
value
|
||||
}
|
||||
component {
|
||||
name
|
||||
description
|
||||
@@ -254,26 +259,44 @@ def normalise(s: str) -> str:
|
||||
|
||||
|
||||
_MFR_ALIASES = {
|
||||
"ti": "texasinstruments",
|
||||
"texas": "texasinstruments",
|
||||
"texasinstruments": "texasinstruments",
|
||||
"onsemi": "onsemi",
|
||||
"onsemiconductor": "onsemi",
|
||||
"onsemin": "onsemi",
|
||||
"nxp": "nxp",
|
||||
"nxpsemiconductors": "nxp",
|
||||
"st": "stmicro",
|
||||
"stmicro": "stmicro",
|
||||
"stmicroelectronics": "stmicro",
|
||||
"abracon": "abracon",
|
||||
"abraconcorporation": "abracon",
|
||||
"ck": "ck",
|
||||
"ckcomponents": "ck",
|
||||
"murata": "murata",
|
||||
"muratamfg": "murata",
|
||||
"muratamanufacturing": "murata",
|
||||
"kingbright": "kingbright",
|
||||
"kingbight": "kingbright", # known Nexar typo
|
||||
"ti": "texasinstruments",
|
||||
"texas": "texasinstruments",
|
||||
"texasinstruments": "texasinstruments",
|
||||
"onsemi": "onsemi",
|
||||
"onsemiconductor": "onsemi",
|
||||
"onsemin": "onsemi",
|
||||
"fairchild": "onsemi", # acquired by ON Semi 2016
|
||||
"fairchildsemiconductor": "onsemi",
|
||||
"nxp": "nxp",
|
||||
"nxpsemiconductors": "nxp",
|
||||
"nexperia": "nxp", # spun off from NXP
|
||||
"st": "stmicro",
|
||||
"stmicro": "stmicro",
|
||||
"stmicroelectronics": "stmicro",
|
||||
"abracon": "abracon",
|
||||
"abraconcorporation": "abracon",
|
||||
"ck": "ck",
|
||||
"ckcomponents": "ck",
|
||||
"murata": "murata",
|
||||
"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
|
||||
|
||||
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.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
|
||||
mfr_headers = []
|
||||
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:
|
||||
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 ---
|
||||
if not match:
|
||||
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")
|
||||
else:
|
||||
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)
|
||||
|
||||
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}.xlsx")
|
||||
wb.save(filename)
|
||||
@@ -639,19 +734,29 @@ def main():
|
||||
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"))
|
||||
|
||||
if not bom_files:
|
||||
print(f"\nNo xlsx files found in {bom_dir}.")
|
||||
return
|
||||
|
||||
print(f"\nBOM files available:")
|
||||
print("-" * 60)
|
||||
print(f" [0] Skip BOM matching — raw parameter export")
|
||||
for i, name in enumerate(bom_files, start=1):
|
||||
print(f" [{i}] {name}")
|
||||
|
||||
print()
|
||||
choice = prompt_choice(f"Select a BOM file (1-{len(bom_files)}): ", len(bom_files))
|
||||
selected_bom = os.path.join(bom_dir, bom_files[choice - 1])
|
||||
print(f"\nSelected BOM: {bom_files[choice - 1]}")
|
||||
while True:
|
||||
try:
|
||||
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 ---
|
||||
print("\nFetching schematic sheets...")
|
||||
@@ -717,32 +822,50 @@ def main():
|
||||
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)
|
||||
|
||||
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()
|
||||
if bom_variant:
|
||||
total_instances = 0
|
||||
for bom_item in (bom_variant.get("bom") or {}).get("bomItems") or []:
|
||||
for instance in bom_item.get("bomItemInstances") or []:
|
||||
total_instances += 1
|
||||
if instance.get("isFitted"):
|
||||
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
|
||||
for s in schematics:
|
||||
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).")
|
||||
# Filter each sheet's nodes to fitted components only.
|
||||
# If no fitted designator data is available, export all components unfiltered.
|
||||
if fitted_designators:
|
||||
for s in schematics:
|
||||
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).")
|
||||
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 ---
|
||||
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.")
|
||||
if raw_mode:
|
||||
bom_data = None
|
||||
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 ---
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user