元驭机器人元驭机器人Hall 传感器教学下载原文件
1#!/usr/bin/env python32"""Render the checked hardware illustrations from the archived source PDFs.34Run from any directory with Python, PyMuPDF and Pillow installed:5    python scripts/render_hardware_assets.py67PDF page numbers in the manifest are one-based. Crop rectangles are in the8unrotated PDF page's point coordinates, with the origin at the top left.9The PDFs are read only. No drawing is redrawn, mirrored or recolored.10"""1112from __future__ import annotations1314import argparse15import hashlib16import json17from pathlib import Path1819import fitz20from PIL import Image212223ROOT = Path(__file__).resolve().parents[1]24DEFAULT_SOURCE = ROOT / "hardware"25DEFAULT_OUTPUT = ROOT / "docs" / "assets" / "hardware"2627# Bounds were checked against the full HallPos schematic. Each region includes28# complete symbols, local wires, power/ground ends, and the connecting net names.29SCHEMATIC_REGIONS = [30    ("linear-power.png", [45, 265, 402, 440], "U5 input, U7 regulator, C7/C8/C9, D1 and NC1"),31    ("linear-hall.png", [354, 48, 558, 216], "U1/U2/U4 Hall sensors, C3/C4/C5, ADC1/ADC2/ADC3"),32    ("linear-mcu.png", [45, 15, 314, 260], "U3 MCU, C1/C2, BOOT0/NRST, all used net labels"),33    ("linear-rs485.png", [454, 249, 638, 392], "U6 transceiver, R4/R5, C6, TP2/TP3/TP4, all net labels"),34    ("linear-swd.png", [675, 270, 828, 386], "CN1 SWD connector including power and grounded fixing pads"),35    ("linear-bus-connector.png", [45, 265, 205, 440], "U5 power/RS485 connector, NC1 termination and D1 input protection"),36    ("linear-led.png", [636, 63, 716, 184], "LED1/R1, VCC_5V and LED_STA"),37]383940def graphic_bounds(page: fitz.Page) -> fitz.Rect:41    """Find the board drawing, excluding the almost-white PDF export frame.4243    Inspect an 8x RGB raster; ink below luminance 220 forms the board bounds.44    Keep two PDF points of padding so the outer board stroke is not cropped.45    """46    probe_scale = 8.047    pix = page.get_pixmap(matrix=fitz.Matrix(probe_scale, probe_scale), alpha=False)48    image = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)49    mask = image.convert("L").point(lambda value: 255 if value < 220 else 0)50    bbox = mask.getbbox()51    if bbox is None:52        raise ValueError("No board drawing found")53    x0, y0, x1, y1 = bbox54    return fitz.Rect(55        x0 / probe_scale - 2, y0 / probe_scale - 2,56        x1 / probe_scale + 2, y1 / probe_scale + 2,57    ) & page.rect585960def render(source: Path, output: Path) -> dict:61    output.mkdir(parents=True, exist_ok=True)62    documents: dict[str, fitz.Document] = {}63    entries: list[dict] = []6465    def export(filename: str, pdf: str, page_number: int, *, crop=None,66               width: int = 1600, kind: str, description: str,67               automatic_crop: bool = False) -> None:68        if pdf not in documents:69            documents[pdf] = fitz.open(source / ("schematics" if pdf.startswith("SCH_") else "pcb") / pdf)70        page = documents[pdf][page_number - 1]71        bounds = graphic_bounds(page) if automatic_crop else fitz.Rect(crop or page.rect)72        if not page.rect.contains(bounds):73            raise ValueError(f"Crop outside {pdf} page {page_number}: {bounds}")74        scale = width / bounds.width75        pixmap = page.get_pixmap(matrix=fitz.Matrix(scale, scale), clip=bounds, alpha=False)76        target = output / filename77        pixmap.save(target)78        entries.append({79            "file": filename,80            "kind": kind,81            "description": description,82            "source_pdf": "hardware/" + ("schematics/" if pdf.startswith("SCH_") else "pcb/") + pdf,83            "source_sha256": hashlib.sha256((source / ("schematics" if pdf.startswith("SCH_") else "pcb") / pdf).read_bytes()).hexdigest(),84            "page_number": page_number,85            "page_rect_points": [round(n, 4) for n in page.rect],86            "crop_rect_points": [round(n, 4) for n in bounds],87            "crop_method": "8x raster luminance < 220, enclosing bounds + 2 pt padding"88                           if automatic_crop else "reviewed fixed region" if crop else "full page",89            "render_scale": round(scale, 8),90            "pixel_size": [pixmap.width, pixmap.height],91            "orientation": "as exported in source PDF; no mirror or rotation",92        })9394    for prefix, pdf in [("linear", "PCB_HallPos.pdf"), ("encoder", "PCB_Encoder.pdf")]:95        export(f"{prefix}-pcb-top.png", pdf, 4, kind="pcb-component-side",96               description="Component-side composite: pads, traces, copper and component outlines; PDF page 4",97               automatic_crop=True)98        export(f"{prefix}-pcb-bottom.png", pdf, 5, kind="pcb-reverse-side",99               description="Reverse-side composite: traces, copper, vias and original markings; PDF page 5",100               automatic_crop=True)101        # Page 1 remains a supplementary locator, never the main PCB illustration.102        export(f"{prefix}-placement.png", pdf, 1, kind="reference-designator-locator",103               description="Supplementary component locator from PDF page 1; not a copper-layer drawing",104               automatic_crop=True, width=1300)105106    for prefix, pdf in [("linear", "SCH_HallPos.pdf"), ("encoder", "SCH_Encoder.pdf")]:107        export(f"{prefix}-schematic.png", pdf, 1, width=2400,108               kind="full-schematic", description="Complete schematic, including source title block")109110    for filename, crop, description in SCHEMATIC_REGIONS:111        export(filename, "SCH_HallPos.pdf", 1, crop=crop, width=1800 if filename == "linear-power.png" else 1400,112               kind="schematic-detail", description=description)113114    manifest = {115        "schema_version": 1,116        "generator": "scripts/render_hardware_assets.py",117        "renderer": f"PyMuPDF {fitz.VersionBind}",118        "coordinate_system": "PDF points, origin top-left, page_number is one-based",119        "board_side_convention": {120            "top": "PDF page 4, identified as the populated/component side from pad and component outlines",121            "bottom": "PDF page 5, opposite side; preserve the source PDF's view",122            "qualification": "PDF pages do not print layer names. These are composite views, not individual Gerber layers.",123        },124        "assets": entries,125    }126    (output / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")127    for document in documents.values():128        document.close()129    return manifest130131132def main() -> None:133    parser = argparse.ArgumentParser(description=__doc__)134    parser.add_argument("--source-dir", type=Path, default=DEFAULT_SOURCE)135    parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT)136    args = parser.parse_args()137    manifest = render(args.source_dir.resolve(), args.output_dir.resolve())138    for asset in manifest["assets"]:139        print(f"{asset['file']}: {asset['pixel_size'][0]} x {asset['pixel_size'][1]}, "140              f"{Path(asset['source_pdf']).name} p{asset['page_number']}")141142143if __name__ == "__main__":144    main()