main
adonis fix polarity: cathode is the +X pad; adopt the official ams OSRAM geometry STEP for the 3D model and body outline b657465 1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
#!/usr/bin/env python3
"""
Emit every non-3D artefact for the LW A6SG-V2BA-FK0PN0 component page from one
source of truth: KiCad symbol + footprint, the EAGLE library, pads.json,
page.json, package.json, README.md and the two interactive viewers.

Datasheet: "LW A6SG binning FK0PN0", ams OSRAM, version 1.1, 2020-06-01.
Page numbers in comments refer to that document.
"""
import json, os, re, textwrap

HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SLUG = "lw-a6sg-v2ba-fk0pn0"
MPN = "LW A6SG-V2BA-FK0PN0"
SYM = "LW_A6SG-V2BA-FK0PN0"
FP = "LED_SIDELED_LW-A6SG"
MFR = "ams OSRAM"
PKG = "SIDELED®"
OWNER = "adonis"

# --------------------------------------------------------------- land pattern
# p.15 "Recommended Solder Pad": two 1.2 x 3.0 pads, 3.7 outer-to-outer.
PAD_W, PAD_H, PAD_SPAN = 1.20, 3.00, 3.70
PAD_X = (PAD_SPAN - PAD_W) / 2.0          # 1.25 -> pitch 2.5
LENS_DIA = 2.40                           # p.14 front view; r=1.2 circle in the vendor CAD

# Body envelope, taken from the vendor STEP (LW_A6SG_20091210_geometry.STEP)
# after the rigid placement in tools/place_step.py. The land is NOT centred
# under the body: the lens face stands 2.225 mm in front of the pad centreline
# and the back of the body 1.540 mm behind it.
BODY_W = 4.00                             # x, +-2.0
BODY_FRONT = -2.225                       # y, the lens plane (emission side)
BODY_BACK = 1.540                         # y, back face
BODY_TOP = 4.025                          # z above the board
OPTIC_Z = 2.575                           # z of the optical axis
SILK_X, SILK_F, SILK_B = BODY_W / 2 + 0.20, BODY_FRONT - 0.20, BODY_BACK + 0.20
CRTY_X, CRTY_F, CRTY_B = BODY_W / 2 + 0.40, BODY_FRONT - 0.40, BODY_BACK + 0.40

PINS = [
    dict(number="1", name="K", type="passive", pad_x=+PAD_X,
         description=(
             "Cathode. On this footprint - a view looking DOWN at the board - "
             "the cathode is the RIGHT-hand pad when the lens faces up the "
             "page. The datasheet's front view (p.14) labels the LEFT lead "
             "'Cathode', but a front view and a top view see the lead axis "
             "from opposite sides. The vendor CAD settles it: the cathode "
             "marking on the top face sits over this pad. Return LED current "
             "here.")),
    dict(number="2", name="A", type="passive", pad_x=-PAD_X,
         description=(
             "Anode. Drive at IF = 20 mA typical, 5 mA min to 50 mA max (p.3). "
             "VF is 2.90 V min / 3.00 V typ / 3.50 V max at 20 mA (p.4), so "
             "size the series resistor from VF max, not typ. Never connect to "
             "a fixed supply without current limiting.")),
]

SPECS = [
    ("Manufacturer", MFR),
    ("Package", "SIDELED®, 2-lead gull-wing, side-emitting"),
    ("Category", "LED"),
    ("Pins", "2"),
    ("Colour", "White, Cx = 0.33 / Cy = 0.33 (CIE 1931)"),
    ("Luminous intensity", "900 … 2240 mcd at IF = 20 mA"),
    ("Forward voltage", "3.00 V typ, 3.50 V max at IF = 20 mA"),
    ("Forward current", "20 mA typ, 50 mA max (5 mA min)"),
    ("Viewing angle", "120° (2φ at 50 % IV)"),
    ("Body size", "4.00 × 3.765 × 4.025 mm overall"),
    ("Optical axis", "2.575 mm above the board"),
]

ELECTRICAL = [
    ("Forward voltage", "VF", "2.90", "3.00", "3.50", "V", "IF = 20 mA (p.4)"),
    ("Forward current", "IF", "5", "20", "50", "mA", "TS = 25 °C (p.3)"),
    ("Surge current", "IFS", "—", "—", "300", "mA",
     "t ≤ 10 µs, D = 0.005 (p.3)"),
    ("Reverse voltage", "VR", "—", "—", "5", "V", "TS = 25 °C (p.3)"),
    ("Reverse current", "IR", "—", "0.01", "10", "µA", "VR = 5 V (p.4)"),
    ("Luminous intensity", "IV", "900", "—", "2240", "mcd",
     "IF = 20 mA, bins V2…BA (p.2, p.5)"),
    ("Chromaticity", "Cx / Cy", "—", "0.33 / 0.33", "—", "—",
     "CIE 1931, bin group FK0PN0 (p.4)"),
    ("Thermal resistance", "RthJS", "—", "—", "220", "K/W",
     "junction to solder point, real (p.4)"),
    ("Thermal resistance", "RthJA", "—", "—", "380", "K/W",
     "junction to ambient, real (p.4)"),
    ("Junction temperature", "Tj", "—", "—", "125", "°C", "p.3"),
    ("Operating temperature", "Top", "-40", "—", "110", "°C", "p.3"),
]


def w(rel, text):
    path = os.path.join(HERE, rel)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "w") as fh:
        fh.write(text)
    print("  wrote %-42s %6d B" % (rel, len(text.encode())))


# ------------------------------------------------------------------- symbol
def kicad_sym():
    def pin(p, x, ang, name):
        return (
            '    (pin passive line (at %.2f 0.00 %d) (length 2.54)\n'
            '      (name "%s" (effects (font (size 1.27 1.27))))\n'
            '      (number "%s" (effects (font (size 1.27 1.27))))\n'
            '    )' % (x, ang, name, p))

    stroke = '(stroke (width 0.254) (type default)) (fill (type %s))'
    body = [
        # cathode bar, then the anode triangle pointing into it
        '    (polyline (pts (xy -1.27 -1.27) (xy -1.27 1.27))\n      %s)'
        % (stroke % 'none'),
        '    (polyline (pts (xy 1.27 1.27) (xy 1.27 -1.27) (xy -1.27 0.00)'
        ' (xy 1.27 1.27))\n      %s)' % (stroke % 'none'),
    ]
    # two emission arrows, the LED convention
    for dx in (-0.55, 0.75):
        body.append(
            '    (polyline (pts (xy %.2f 1.60) (xy %.2f 2.70))\n      %s)'
            % (dx, dx + 1.10, stroke % 'none'))
        body.append(
            '    (polyline (pts (xy %.2f 2.70) (xy %.2f 2.24) (xy %.2f 2.34)'
            ' (xy %.2f 2.70))\n      %s)'
            % (dx + 1.10, dx + 1.02, dx + 0.64, dx + 1.10, stroke % 'outline'))
    return (
        '(kicad_symbol_lib (version 20211014) (generator adom_sideled)\n'
        '  (symbol "%s" (pin_names (offset 0.762) hide) (in_bom yes) (on_board yes)\n'
        '    (property "Reference" "D" (at 0 5.08 0) (effects (font (size 1.27 1.27))))\n'
        '    (property "Value" "%s" (at 0 -3.81 0) (effects (font (size 1.27 1.27))))\n'
        '    (property "Footprint" "%s:%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
        '    (property "Datasheet" "docs/%s-datasheet.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
        '    (property "Manufacturer" "%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
        '    (property "Description" "White SIDELED, side-emitting, 900-2240 mcd at 20 mA" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'
        '    (symbol "%s_0_1"\n%s\n    )\n'
        '    (symbol "%s_1_1"\n%s\n    )\n'
        '  )\n)\n'
        % (SYM, MPN, SLUG, FP, SLUG, MFR, SYM, "\n".join(body), SYM,
           "\n".join([pin("1", -3.81, 0, "K"), pin("2", 3.81, 180, "A")])))


# ---------------------------------------------------------------- footprint
def kicad_mod():
    def pad(n, x, name):
        return (
            '\t(pad "%s" smd roundrect\n'
            '\t\t(at %.3f 0)\n\t\t(size %.2f %.2f)\n'
            '\t\t(layers "F.Cu" "F.Paste" "F.Mask")\n'
            '\t\t(roundrect_rratio 0.25)\n'
            '\t\t(uuid "0000pad-%s")\n\t)' % (n, x, PAD_W, PAD_H, n))

    def line(layer, x1, y1, x2, y2, width):
        return ('\t(fp_line\n\t\t(start %.3f %.3f)\n\t\t(end %.3f %.3f)\n'
                '\t\t(stroke\n\t\t\t(width %.3f)\n\t\t\t(type solid)\n\t\t)\n'
                '\t\t(layer "%s")\n\t)' % (x1, y1, x2, y2, width, layer))

    def rect(layer, hx, y0, y1, width):
        return "\n".join([
            line(layer, -hx, y0, hx, y0, width),
            line(layer, hx, y0, hx, y1, width),
            line(layer, hx, y1, -hx, y1, width),
            line(layer, -hx, y1, -hx, y0, width)])

    hx, f, b = BODY_W / 2, BODY_FRONT, BODY_BACK
    fab = "\n".join([
        rect("F.Fab", hx, f, b, 0.10),
        # lens window on the emitting (-Y) face, and the emission arrow
        '\t(fp_circle\n\t\t(center 0 %.3f)\n\t\t(end %.3f %.3f)\n'
        '\t\t(stroke\n\t\t\t(width 0.100)\n\t\t\t(type solid)\n\t\t)\n'
        '\t\t(fill none)\n\t\t(layer "F.Fab")\n\t)' % (f, LENS_DIA / 2, f),
        line("F.Fab", 0, f - 0.35, 0, f - 1.15, 0.10),
        line("F.Fab", 0, f - 1.15, -0.30, f - 0.85, 0.10),
        line("F.Fab", 0, f - 1.15, 0.30, f - 0.85, 0.10),
    ])
    silk = rect("F.SilkS", SILK_X, SILK_F, SILK_B, 0.12) + "\n" + (
        # cathode / pin-1 triangle, behind the body on the cathode (+X) side
        '\t(fp_poly\n\t\t(pts\n\t\t\t(xy %.3f %.3f) (xy %.3f %.3f) (xy %.3f %.3f)\n\t\t)\n'
        '\t\t(stroke\n\t\t\t(width 0.050)\n\t\t\t(type solid)\n\t\t)\n'
        '\t\t(fill solid)\n\t\t(layer "F.SilkS")\n\t)'
        % (PAD_X, SILK_B + 0.10, PAD_X - 0.24, SILK_B + 0.53,
           PAD_X + 0.24, SILK_B + 0.53))
    crtyd = rect("F.CrtYd", CRTY_X, CRTY_F, CRTY_B, 0.05)

    def prop(name, value, layer, y, hide=""):
        return ('\t(property "%s" "%s"\n\t\t(at 0 %.3f 0)\n\t\t(layer "%s")\n%s'
                '\t\t(uuid "0000prop-%s")\n\t\t(effects\n\t\t\t(font\n'
                '\t\t\t\t(size 1 1)\n\t\t\t\t(thickness 0.15)\n\t\t\t)\n\t\t)\n\t)'
                % (name, value, y, layer, hide, name.lower()))

    return (
        '(footprint "%s"\n'
        '\t(version 20240108)\n'
        '\t(generator "adom_sideled")\n'
        '\t(generator_version "9.0")\n'
        '\t(layer "F.Cu")\n'
        '\t(descr "ams OSRAM SIDELED, 2-lead side-emitting LED, 4.0 x 3.63 x 3.97 mm. '
        'Land pattern per the Recommended Solder Pad drawing, LW A6SG binning FK0PN0 v1.1 p.15: '
        '2 x (1.2 x 3.0) mm pads, 3.7 mm outer span. Emission is toward -Y; pin 1 (cathode) is the +X pad. Body envelope and the 3D model come from the ams OSRAM geometry STEP.")\n'
        '\t(tags "LED SIDELED OSRAM side-emitting white")\n'
        '\t(attr smd)\n'
        '%s\n%s\n%s\n%s\n%s\n%s\n%s\n'
        '\t(embedded_fonts no)\n'
        '\t(model "${KIPRJMOD}/%s.step"\n'
        '\t\t(offset\n\t\t\t(xyz 0 0 0)\n\t\t)\n'
        '\t\t(scale\n\t\t\t(xyz 1 1 1)\n\t\t)\n'
        '\t\t(rotate\n\t\t\t(xyz 0 0 0)\n\t\t)\n\t)\n)\n'
        % (FP,
           prop("Reference", "D**", "F.SilkS", SILK_F - 0.9),
           prop("Value", MPN, "F.Fab", SILK_B + 1.5),
           prop("Datasheet", "docs/%s-datasheet.pdf" % SLUG, "F.Fab", 0,
                "\t\t(hide yes)\n"),
           silk, fab, crtyd,
           "\n".join([pad(p["number"], p["pad_x"], p["name"]) for p in PINS]),
           SLUG))


# ------------------------------------------------------------------ EAGLE
def lbr():
    layers = "".join(
        '<layer number="%d" name="%s" color="%d" fill="%d" visible="%s" active="yes"/>\n'
        % t for t in [
            (1, "Top", 4, 1, "yes"), (16, "Bottom", 1, 1, "yes"),
            (17, "Pads", 2, 1, "yes"), (18, "Vias", 2, 1, "yes"),
            (20, "Dimension", 15, 1, "yes"), (21, "tPlace", 7, 1, "yes"),
            (22, "bPlace", 7, 1, "yes"), (23, "tOrigins", 15, 1, "yes"),
            (25, "tNames", 7, 1, "yes"), (27, "tValues", 7, 1, "yes"),
            (29, "tStop", 7, 3, "no"), (31, "tCream", 7, 4, "no"),
            (39, "tKeepout", 4, 11, "no"), (51, "tDocu", 7, 1, "yes"),
            (94, "Symbols", 4, 1, "yes"), (95, "Names", 7, 1, "yes"),
            (96, "Values", 7, 1, "yes"), (97, "Info", 7, 1, "yes")])
    # EAGLE's Y axis points the opposite way to KiCad's, hence the sign flips.
    smds = "".join(
        '  <smd name="%s" x="%.4f" y="0.0000" dx="%.4f" dy="%.4f" layer="1" roundness="25"/>\n'
        % (p["number"], p["pad_x"], PAD_W, PAD_H) for p in PINS)
    # EAGLE's Y axis points the opposite way to KiCad's, so every y is negated.
    hx, f, b = BODY_W / 2, -BODY_FRONT, -BODY_BACK
    sf, sb = -SILK_F, -SILK_B
    wires = "".join(
        '  <wire x1="%.4f" y1="%.4f" x2="%.4f" y2="%.4f" width="%.4f" layer="%d"/>\n' % t
        for t in [
            (-SILK_X, sf, SILK_X, sf, 0.12, 21),
            (SILK_X, sf, SILK_X, sb, 0.12, 21),
            (SILK_X, sb, -SILK_X, sb, 0.12, 21),
            (-SILK_X, sb, -SILK_X, sf, 0.12, 21),
            (-hx, f, hx, f, 0.10, 51),
            (hx, f, hx, b, 0.10, 51),
            (hx, b, -hx, b, 0.10, 51),
            (-hx, b, -hx, f, 0.10, 51)])
    return (
        '<?xml version="1.0" encoding="utf-8"?>\n'
        '<!DOCTYPE eagle SYSTEM "eagle.dtd">\n<eagle version="9.6.2">\n<drawing>\n'
        '<settings><setting alwaysvectorfont="no"/></settings>\n'
        '<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1"'
        ' display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>\n'
        '<layers>\n%s</layers>\n<library>\n'
        '<description>%s &#8212; %s %s</description>\n'
        '<packages>\n<package name="%s">\n'
        '  <description>%s SIDELED, 2-lead side-emitting LED</description>\n'
        '%s%s'
        '  <circle x="0" y="%.4f" radius="%.4f" width="0.1" layer="51"/>\n'
        '  <text x="0" y="%.3f" size="1.27" layer="25" align="bottom-center">&gt;NAME</text>\n'
        '  <text x="0" y="%.3f" size="1.27" layer="27" align="top-center">&gt;VALUE</text>\n'
        '<!--adom-pin1--><polygon width="0.0500" layer="21">'
        '<vertex x="%.4f" y="%.4f"/><vertex x="%.4f" y="%.4f"/>'
        '<vertex x="%.4f" y="%.4f"/></polygon>\n'
        '</package>\n</packages>\n'
        '<symbols>\n<symbol name="%s">\n'
        '  <wire x1="-1.27" y1="-1.27" x2="-1.27" y2="1.27" width="0.254" layer="94"/>\n'
        '  <wire x1="1.27" y1="1.27" x2="1.27" y2="-1.27" width="0.254" layer="94"/>\n'
        '  <wire x1="1.27" y1="-1.27" x2="-1.27" y2="0" width="0.254" layer="94"/>\n'
        '  <wire x1="-1.27" y1="0" x2="1.27" y2="1.27" width="0.254" layer="94"/>\n'
        '  <wire x1="-0.55" y1="1.6" x2="0.55" y2="2.7" width="0.2" layer="94"/>\n'
        '  <wire x1="0.75" y1="1.6" x2="1.85" y2="2.7" width="0.2" layer="94"/>\n'
        '  <text x="0" y="5.08" size="1.27" layer="95" align="bottom-center">&gt;NAME</text>\n'
        '  <text x="0" y="-3.81" size="1.27" layer="96" align="top-center">&gt;VALUE</text>\n'
        '  <pin name="K" x="-3.81" y="0" length="short" direction="pas"/>\n'
        '  <pin name="A" x="3.81" y="0" length="short" direction="pas" rot="R180"/>\n'
        '</symbol>\n</symbols>\n'
        '<devicesets>\n<deviceset name="%s" prefix="D">\n'
        '  <description>%s white SIDELED, 900-2240 mcd at IF = 20 mA</description>\n'
        '  <gates><gate name="G$1" symbol="%s" x="0" y="0"/></gates>\n'
        '  <devices>\n    <device name="" package="%s">\n      <connects>\n'
        '        <connect gate="G$1" pin="K" pad="1"/>\n'
        '        <connect gate="G$1" pin="A" pad="2"/>\n'
        '      </connects>\n'
        '      <technologies><technology name=""/></technologies>\n'
        '    </device>\n  </devices>\n</deviceset>\n</devicesets>\n'
        '</library>\n</drawing>\n</eagle>\n'
        % (layers, MPN, MFR, PKG, FP, MFR, smds, wires,
           f, LENS_DIA / 2, sf + 0.7, sb - 0.7,
           PAD_X, sb - 0.10, PAD_X - 0.24, sb - 0.53,
           PAD_X + 0.24, sb - 0.53,
           SYM, SYM, MFR, SYM, FP))


# ------------------------------------------------------------------- viewers
TPL = open(os.path.join(HERE, "tools", "viewer.tpl.html")).read()


def viewer(kind, bar, svg, title):
    pins = [dict(number=p["number"], name=p["name"], type=p["type"],
                 desc=p["description"]) for p in PINS]
    return (TPL
            .replace("@@TITLE@@", title)
            .replace("@@BAR@@", bar)
            .replace("@@SVG@@", svg)
            .replace("@@KIND@@", kind)
            .replace("@@MPN@@", "%s/%s" % (OWNER.upper(), SYM))
            .replace("@@PINS@@", json.dumps(pins, sort_keys=True)))


def symbol_svg():
    S = 100.0   # px per mm
    def X(v): return v * S
    def Y(v): return -v * S     # SVG y grows downward, KiCad symbol y grows up
    out = []
    out.append("<polyline points='%.1f,%.1f %.1f,%.1f' fill='none' stroke='#e35d5d' stroke-width='4'/>"
               % (X(-1.27), Y(-1.27), X(-1.27), Y(1.27)))
    out.append("<polygon points='%.1f,%.1f %.1f,%.1f %.1f,%.1f' fill='#e35d5d22' stroke='#e35d5d' stroke-width='4'/>"
               % (X(1.27), Y(1.27), X(1.27), Y(-1.27), X(-1.27), Y(0)))
    for dx in (-0.55, 0.75):
        out.append("<polyline points='%.1f,%.1f %.1f,%.1f' fill='none' stroke='#dfe4ec' stroke-width='4'/>"
                   % (X(dx), Y(1.6), X(dx + 1.1), Y(2.7)))
        out.append("<polygon points='%.1f,%.1f %.1f,%.1f %.1f,%.1f' fill='#dfe4ec'/>"
                   % (X(dx + 1.10), Y(2.70), X(dx + 1.02), Y(2.24), X(dx + 0.64), Y(2.34)))
    for p, x0, x1 in ((PINS[0], -3.81, -1.27), (PINS[1], 3.81, 1.27)):
        out.append("<g class='pingroup' data-num='%s' data-name='%s'>"
                   "<line class='pinvis' x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' stroke='#dfe4ec' stroke-width='4'/>"
                   "<line x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' stroke='transparent' stroke-width='26' pointer-events='stroke'/></g>"
                   % (p["number"], p["name"], X(x0), Y(0), X(x1), Y(0),
                      X(x0), Y(0), X(x1), Y(0)))
        out.append("<text x='%.1f' y='%.1f' font-size='60' fill='#9fb0c8' text-anchor='middle' dominant-baseline='central'>%s</text>"
                   % (X((x0 + x1) / 2), Y(0.45), p["number"]))
        out.append("<text x='%.1f' y='%.1f' font-size='60' fill='#e6e9ef' text-anchor='middle' dominant-baseline='central'>%s</text>"
                   % (X((x0 + x1) / 2), Y(-0.55), p["name"]))
    return ("<svg xmlns='http://www.w3.org/2000/svg' viewBox='-560 -340 1120 640'"
            " style='width:100%%;height:100%%'>%s</svg>" % "".join(out))


def footprint_svg():
    out = []
    def line(cls, colour, x1, y1, x2, y2, wid):
        out.append("<line class='%s' x1='%.3f' y1='%.3f' x2='%.3f' y2='%.3f'"
                   " stroke='%s' stroke-width='%.3f'/>"
                   % (cls, x1, y1, x2, y2, colour, wid))
    hx, f, b = BODY_W / 2, BODY_FRONT, BODY_BACK

    def box(cls, colour, x, y0, y1, wid):
        for a, bb, c, d in ((-x, y0, x, y0), (x, y0, x, y1),
                            (x, y1, -x, y1), (-x, y1, -x, y0)):
            line(cls, colour, a, bb, c, d, wid)

    box("ly-silk", "#d8b4fe", SILK_X, SILK_F, SILK_B, 0.12)
    box("ly-courtyard", "#4ade80", CRTY_X, CRTY_F, CRTY_B, 0.05)
    box("ly-fab", "#f472b6", hx, f, b, 0.10)
    out.append("<circle class='ly-fab' cx='0' cy='%.3f' r='%.3f' fill='none'"
               " stroke='#f472b6' stroke-width='0.10'/>" % (f, LENS_DIA / 2))
    for a, bb, c, d in ((0, f - 0.35, 0, f - 1.15),
                        (0, f - 1.15, -0.30, f - 0.85),
                        (0, f - 1.15, 0.30, f - 0.85)):
        line("ly-fab", "#f472b6", a, bb, c, d, 0.10)
    for p in PINS:
        out.append("<rect class='ly-copper pad' data-num='%s' data-name='%s'"
                   " x='%.3f' y='%.3f' width='%.3f' height='%.3f' rx='%.3f'"
                   " fill='#f59e0b' stroke='#00000033' stroke-width='0.03' opacity='0.94'/>"
                   % (p["number"], p["name"], p["pad_x"] - PAD_W / 2, -PAD_H / 2,
                      PAD_W, PAD_H, PAD_W * 0.25))
        out.append("<text class='padnum' x='%.3f' y='0' font-size='0.540' fill='#1c1206'"
                   " text-anchor='middle' dominant-baseline='central' pointer-events='none'"
                   " font-family='ui-monospace,monospace' font-weight='700'>%s</text>"
                   % (p["pad_x"], p["number"]))
    out.append("<polygon class='ly-silk adom-pin1' points='%.3f,%.3f %.3f,%.3f %.3f,%.3f'"
               " fill='#d8b4fe' stroke='none'><title>pin 1 (cathode)</title></polygon>"
               % (PAD_X, SILK_B + 0.10, PAD_X - 0.24, SILK_B + 0.53,
                  PAD_X + 0.24, SILK_B + 0.53))
    m = 0.55
    return ("<svg xmlns='http://www.w3.org/2000/svg' viewBox='%.3f %.3f %.3f %.3f'"
            " style='width:100%%;height:100%%'>%s</svg>"
            % (-CRTY_X - m, CRTY_F - m, 2 * CRTY_X + 2 * m,
               (CRTY_B - CRTY_F) + 2 * m, "".join(out)))


# --------------------------------------------------------------------- JSON
def pads_json():
    return json.dumps({
        "schema": "adom.pads/1",
        "slug": SLUG,
        "footprint": FP,
        "generated_from": {
            "footprint": "datasheet:[email protected]",
            "pins": "page.json:component.pins",
            "overrides": None,
        },
        "pin1": {"pad": "1", "at": [PAD_X, 0.0], "corner": "right",
                 "source": "vendor-cad-cathode-marking",
                 "derived_from": "LW_A6SG_20091210_geometry.STEP"},
        "pad_count": 2,
        "symbol_pins": [p["number"] for p in PINS],
        "paste_apertures": 0,
        "issues": [],
        "pads": [{
            "pad": p["number"], "kind": "smd", "shape": "roundrect",
            "at": [p["pad_x"], 0.0], "rot": 0.0, "size": [PAD_W, PAD_H],
            "layers": ["F.Cu", "F.Mask", "F.Paste"], "thermal": False,
            "pin": p["number"], "name": p["name"], "type": p["type"],
            "description": p["description"], "is_pin1": p["number"] == "1",
            "source": {
                "geometry": "LW A6SG binning FK0PN0 v1.1, p.15 Recommended Solder Pad; "
                            "polarity and body outline from LW_A6SG_20091210_geometry.STEP",
                "meaning": "page.json:component.pins",
                "correspondence": "kicad: symbol pin number == pad name"},
        } for p in PINS],
    }, indent=1) + "\n"


FILES = ["page.json", "README.md",
         "%s.kicad_sym" % SLUG, "%s.kicad_mod" % SLUG,
         "%s.glb" % SLUG, "%s.step" % SLUG, "%s.lbr" % SLUG,
         "docs/hero.png", "docs/%s-datasheet.pdf" % SLUG,
         "viewers/symbol.html", "viewers/footprint.html", "pads.json",
         "skills/sideled-lw-a6sg/SKILL.md"]

# The untouched vendor STEP is the provenance for the 3D model. It belongs on
# the page, but shipping it as well as the placed copy would double the install
# payload for no gain, so it stays out of the tarball.
REPO_ONLY = ["LW_A6SG_20091210_geometry.STEP"]


def page_json(sha):
    return json.dumps({
        "schema_version": 1,
        "type": "component",
        "kind": "component",
        "slug": SLUG,
        "title": MPN,
        "brief": "ams OSRAM Standard LEDs - SMD, White SIDELED®, side-emitting. "
                 "Datasheet-verified pinout, KiCad symbol/footprint/3D.",
        "version": "0.2.0",
        "license": "CC-BY-SA-4.0",
        "tags": ["led", "sideled", "side-emitting", "kicad", "automotive", SLUG],
        "component": {
            "mpn": MPN,
            "manufacturer": MFR,
            "package": PKG,
            "pin_count": 2,
            "category": "LED",
            "subcategory": "Standard LEDs - SMD, White SIDELED®",
            "datasheet": "docs/%s-datasheet.pdf" % SLUG,
            "parts": {
                "symbol": "%s.kicad_sym" % SLUG,
                "footprint": "%s.kicad_mod" % SLUG,
                "model_3d": "%s.glb" % SLUG,
                "model_3d_step": "%s.step" % SLUG,
                "lbr": "%s.lbr" % SLUG,
            },
            "distributor_links": {
                "mouser": "https://www.mouser.com/en/ProductDetail/ams-OSRAM/LW-A6SG-V2BA-FK0PN0?qs=vLWxofP3U2xiZuGh4CU2Qw%3D%3D",
                "digikey": "https://www.digikey.com/en/products/detail/ams-osram-ag/LW-A6SG-V2BA-FK0PN0-0-20-R33/13624925",
            },
            "pins": [{"number": p["number"], "name": p["name"],
                      "type": p["type"], "description": p["description"]}
                     for p in PINS],
            "provenance": {
                "extractor": "adom component pipeline (Claude)",
                "datasheet": {
                    "url": "docs/%s-datasheet.pdf" % SLUG,
                    "title": "LW A6SG binning FK0PN0",
                    "version": "1.1",
                    "dated": "2020-06-01",
                    "pages": 25,
                    "sha256": sha[:16],
                    "source_tier": "manufacturer",
                    "retrieved": "2026-08-27",
                },
                "cad": "Generated from the manufacturer datasheet "
                       "(land pattern p.15, package outline p.14)",
                "pins": {"total": 2, "described": 2, "cites_pages": True,
                         "refuses_to_fabricate": True},
            },
            "viewers": {"symbol": "viewers/symbol.html",
                        "footprint": "viewers/footprint.html"},
            "pin1": {"pad": "1", "at": [PAD_X, 0.0], "corner": "right",
                     "source": "vendor-cad-cathode-marking",
                     "derived_from": "LW_A6SG_20091210_geometry.STEP",
                     "mark": "drawn"},
        },
        "eda_assets": {
            "kicad": {
                "symbol": "%s.kicad_sym" % SLUG,
                "footprint": "%s.kicad_mod" % SLUG,
                "model": "%s.step" % SLUG,
                "provenance": "authored",
            },
            "eagle": {"library": "%s.lbr" % SLUG, "provenance": "authored"},
        },
        "metadata": {"specs": dict(SPECS)},
        "sections": ["hero", "viewers", "readme", "breadcrumbs", "info"],
        "discovery_triggers": TRIGGERS,
        "discovery_pitch": PITCH,
        "author": {"name": "adonis", "email": "[email protected]"},
        "dependencies": {},
        "hero": {"type": "image", "path": "docs/hero.png"},
        "files": FILES + REPO_ONLY,
    }, indent=2) + "\n"


TRIGGERS = [SLUG, "lw a6sg", "a6sg", "sideled", "osram sideled",
            "white sideled", "side emitting led", "side firing led",
            "q65113a1198", "light pipe led"]
PITCH = ("LW A6SG-V2BA-FK0PN0 (ams OSRAM) white SIDELED - side-emitting LED "
         "with datasheet-verified pinout, land pattern, KiCad symbol/footprint "
         "and 3D model.")


def package_json():
    return json.dumps({
        "slug": SLUG,
        "version": "0.2.0",
        "type": "component",
        "description": "ams OSRAM Standard LEDs - SMD, White SIDELED®, "
                       "side-emitting, 900-2240 mcd at IF = 20 mA. "
                       "Datasheet-verified pinout, KiCad symbol/footprint/3D.",
        "tags": ["led", "sideled", "side-emitting", "kicad", "automotive", SLUG],
        "kind": "component",
        "discovery_triggers": TRIGGERS,
        "discovery_pitch": PITCH,
        "dependencies": {},
        "hero": {"type": "image", "path": "docs/hero.png"},
        # The hero lives in the page repo after the first publish materialised
        # it; keeping it out of the tarball stops every installer re-downloading
        # 1 MB of page art on each update.
        "files": [f for f in FILES if f != "docs/hero.png"],
    }, indent=2) + "\n"


def readme(sha):
    pin_rows = "\n".join(
        "| %s | %s | %s | %s |" % (p["number"], p["name"], p["type"],
                                   p["description"]) for p in PINS)
    spec_rows = "\n".join("| %s | %s |" % kv for kv in SPECS)
    el_rows = "\n".join("| %s | %s | %s | %s | %s | %s | %s |" % r
                        for r in ELECTRICAL)
    return """# %(mpn)s

**%(mfr)s** · Standard LEDs - SMD, White SIDELED® · %(pkg)s

A white, side-emitting SIDELED® from %(mfr)s: light leaves through the domed
lens on one *vertical* face, parallel to the board, rather than upward. ThinGaN
chip, diffused silicone resin, 120° Lambertian, binned to Cx = Cy = 0.33 and
900–2240 mcd at 20 mA. Ordering code Q65113A1198.

Pinout, land pattern and body dimensions are read off the manufacturer
datasheet; the symbol, footprint and 3D model are generated from those numbers.

## Pinout

| Pin | Name | Type | Function |
|---|---|---|---|
%(pins)s

**Pin 1 is the cathode, and on this footprint it is the RIGHT-hand pad when the
lens points up the page.** The datasheet's front view (p.14) labels the *left*
lead "Cathode", but a front view looks at the part from the lens side while a
footprint looks down at the board — the two see the lead axis from opposite
sides, so the cathode swaps ends between them. The vendor CAD is unambiguous:
the cathode marking on the top face has no mirror partner and sits over the +X
pad. The footprint repeats it as a filled silkscreen triangle behind that pad.

## Key specifications

| Parameter | Value |
|---|---|
%(specs)s

## Electrical and optical

| Parameter | Symbol | Min | Typ | Max | Unit | Conditions |
|---|---|---|---|---|---|---|
%(elec)s

Brightness bins for this ordering code: V2 (900–1120 mcd), AA (1120–1400),
AB (1400–1800), BA (1800–2240), all at IF = 20 mA (p.5). Chromaticity bin
group FK0…PN0 (p.6–8). MSL 2 per JEDEC J-STD-020E, reflow peak 245 °C
(p.16). ESD 2 kV HBM Class 2. Corrosion robustness class 1B. Approximate
weight 67.0 mg.

## Land pattern

From the datasheet's *Recommended Solder Pad* drawing (p.15):

| | Value |
|---|---|
| Pads | 2 × SMD roundrect, 1.20 × 3.00 mm |
| Outer span | 3.70 mm (pads centred at x = ±1.25 mm) |
| Pin 1 (cathode) | +X pad, i.e. right-hand when the lens points up the page |
| Copper area | > 16 mm² per pad recommended for heat dissipation |
| Body vs land | lens face 2.225 mm in front of the pad centreline, back face 1.540 mm behind |

Lead spacing on the part is 2.54 mm (p.14) while the recommended land is drawn
on a 2.50 mm pitch — the 0.04 mm difference sits well inside the 1.2 mm pad for
a 1.0 mm lead, and the footprint follows the manufacturer's land drawing.

The datasheet asks for a nitrogen reflow atmosphere for best joint quality, and
the package is **not** suitable for ultrasonic cleaning (p.15).

## CAD

- **Symbol** `%(sym)s` — standard 2-pin LED, pin 1 K / pin 2 A.
- **Footprint** `%(fp)s` — emission direction is **−Y**, marked with an arrow
  and the Ø2.4 mm lens window on F.Fab; pin 1 is the +X pad.
- **3D** `%(slug)s.step` (+ GLB) — the official ams OSRAM geometry, 4.00 ×
  3.765 × 4.025 mm, origin on the land pattern, part sitting on Z = 0.
- **EAGLE** `%(slug)s.lbr` — same package, symbol and device set.

## Provenance

Everything here is traceable to the manufacturer datasheet rather than a
distributor listing:

- **Datasheet:** [%(mfr)s, *LW A6SG binning FK0PN0*, v1.1, 2020-06-01](docs/%(slug)s-datasheet.pdf)
  (25 pp, sha256 `%(sha)s`, retrieved 2026-08-27)
- **Pins and electricals:** read from pp. 2–6 and 14–16; every row above cites
  its page.
- **Land pattern:** p.15 *Recommended Solder Pad*, used as drawn.
- **3D model and body outline:** the official ams OSRAM geometry file
  `LW_A6SG_20091210_geometry.STEP`, kept in this repo unmodified. The shipped
  `%(slug)s.step` is that file moved into KiCad coordinates by a **rigid
  rotation and translation only** — no geometry is regenerated:

  ```
  x = -Xo          y = Zo + 2.225          z = Yo + 2.575
  ```

  det = +1, so no mirroring. `tools/place_step.py` performs it and pins the
  representation's own datum axis to identity so every reader agrees on where
  the part sits. The offsets come from the vendor model itself: the foot's flat
  contact runs Zo = −3.475 … −0.975 (centre −2.225) and its underside is at
  Yo = −2.575.

The vendor CAD also corrects two things a reading of the drawings alone gets
wrong:

1. **Polarity.** See the pinout note above — the cathode is the +X pad on the
   footprint, not the −X pad that the front view suggests.
2. **There is no 0.80 mm standoff.** The (0.8) in the side view is the
   gull-wing rise, not a gap under the body: the body underside sits 0.055 mm
   off the board, the optical axis is 2.575 mm up, and the package is 4.025 mm
   tall overall.

## Sourcing

| Distributor | Part number | Note |
|---|---|---|
| Mouser | 720-LWA6SGA1366 | %(mpn)s |
| Mouser | 720-LWA6SGA1198 | LW A6SG-V2BA-FK0PN0-0-20-R33 (taped) |
| DigiKey | 475-LWA6SG-V2BA-FK0PN0-0-20-R33TR-ND | taped variant, in stock |

The bare `%(mpn)s` ordering code is marked **obsolete / not recommended for new
designs**; the `-0-20-R33` tape-and-reel variant is the one still stocked. Check
availability before committing a design.

[Datasheet PDF](docs/%(slug)s-datasheet.pdf)
""" % dict(mpn=MPN, mfr=MFR, pkg=PKG, pins=pin_rows, specs=spec_rows,
           elec=el_rows, sym=SYM, fp=FP, slug=SLUG, sha=sha[:16])


if __name__ == "__main__":
    import hashlib
    sha = hashlib.sha256(
        open(os.path.join(HERE, "docs", "%s-datasheet.pdf" % SLUG), "rb").read()
    ).hexdigest()
    print("datasheet sha256 %s" % sha)
    w("%s.kicad_sym" % SLUG, kicad_sym())
    w("%s.kicad_mod" % SLUG, kicad_mod())
    w("%s.lbr" % SLUG, lbr())
    w("pads.json", pads_json())
    w("page.json", page_json(sha))
    w("package.json", package_json())
    w("README.md", readme(sha))
    w("viewers/symbol.html",
      viewer("symbol", "<span class=h>Schematic symbol</span><span class=sp></span>",
             symbol_svg(), "%s/%s symbol" % (OWNER.upper(), SYM)))
    w("viewers/footprint.html",
      viewer("footprint",
             '<span class=h>PCB footprint</span><span class=sp></span>'
             '<button class="lyrtog on" data-lyr="copper"><span class="d" style="background:#e35d5d"></span>Copper</button>'
             '<button class="lyrtog on" data-lyr="silk"><span class="d" style="background:#e6edf3"></span>Silkscreen</button>'
             '<button class="lyrtog" data-lyr="fab"><span class="d" style="background:#8b949e"></span>Fab</button>'
             '<button class="lyrtog" data-lyr="courtyard"><span class="d" style="background:#bc8cff"></span>Courtyard</button>',
             footprint_svg(), "%s/%s footprint" % (OWNER.upper(), SYM)))