by adom
2020 Addressable RGB LED · 800 Kbps single-line cascade · 16 mA per channel (V1.0 sheet), 12 mA from V1.2
← Commit history
Hero: datasheet note under the chip (off-copper terminal strips, 0.40 mm pad gap, explanation readable from below)
2 files changed
+168
provenance/tools/hero_post_callout.pyadded+168@@ -0,0 +1,168 @@+#!/usr/bin/env python3+"""WS2812B-2020 hero: a 'Datasheet note' layer that explains the one thing that looks wrong from underneath.++Worldsemi's terminals are 0.78 mm long on a 1.00 mm pitch, but its PCB Solder Pad drawing puts 0.7 x 0.7 mm pads+0.40 mm apart (1.10 mm pitch), so the inner 0.09 mm of every bottom terminal sits off the copper. This adds, under the+chip and readable from below:+ - four small amber dashed boxes (lightly filled) on those off-copper terminal strips,+ - a text panel on the underside with leader lines to the four boxes,+ - hover tooltips on all of it (extras.tooltip),+all in one declared layer (extras.adomLayer key 'datasheet_note'), so a reader can switch it off.+usage: ws_callout.py HERO.glb (in place; refuses to run twice)"""+import io, json, math, struct, sys+sys.path.insert(0, "/home/adom/project/basic-parts-overhaul")+from hero_copper import read_glb, write_glb+from PIL import Image, ImageDraw, ImageFont++glb = sys.argv[1]+js, bin_ = read_glb(glb)+NODE = "Datasheet note - why part of each terminal has no copper"+if any(n.get("name") == NODE for n in js["nodes"]): sys.exit(json.dumps(dict(glb=glb, skip="note already present")))++MM = 0.001+Z = -0.006 * MM # just under the chip's seat (z = 0), above the copper fill (-0.018 mm)+AMBER = [1.0, 0.55, 0.05]+TIP = ("No copper under this strip, by design. Worldsemi's PCB Solder Pad drawing (WS2812B-2020 datasheet p.2) "+ "puts 0.7 x 0.7 mm pads 0.40 mm apart, while the terminals are 0.78 mm long on a 1.00 mm pitch, so the "+ "inner 0.09 mm of each terminal sits off the pad. The 0.40 mm gap keeps neighbouring pins from bridging.")+++def buf(data, target=None):+ global bin_+ bin_ += b"\0" * (-len(bin_) % 4)+ bv = dict(buffer=0, byteOffset=len(bin_), byteLength=len(data)); bv.update({"target": target} if target else {})+ js.setdefault("bufferViews", []).append(bv); bin_ += data+ return len(js["bufferViews"]) - 1+++def mesh(name, tris, mat, uv=None, tooltip=None):+ """tris: list of (x, y) mm triangles corners in the board frame (y up), all at Z."""+ pts = [p for t in tris for p in t]+ pos = b"".join(struct.pack("<3f", x * MM, y * MM, Z) for x, y in pts)+ nrm = b"".join(struct.pack("<3f", 0, 0, -1) for _ in pts)+ acc = js.setdefault("accessors", [])+ xs = [x * MM for x, y in pts]; ys = [y * MM for x, y in pts]+ acc.append(dict(bufferView=buf(pos, 34962), componentType=5126, count=len(pts), type="VEC3", min=[min(xs), min(ys), Z], max=[max(xs), max(ys), Z]))+ acc.append(dict(bufferView=buf(nrm, 34962), componentType=5126, count=len(pts), type="VEC3"))+ attrs = dict(POSITION=len(acc) - 2, NORMAL=len(acc) - 1)+ if uv:+ acc.append(dict(bufferView=buf(b"".join(struct.pack("<2f", u, v) for u, v in uv), 34962), componentType=5126, count=len(uv), type="VEC2"))+ attrs["TEXCOORD_0"] = len(acc) - 1+ js["meshes"].append(dict(name=name, primitives=[dict(attributes=attrs, material=mat)]))+ nd = dict(name=name, mesh=len(js["meshes"]) - 1)+ if tooltip: nd["extras"] = dict(tooltip=tooltip)+ js["nodes"].append(nd); return len(js["nodes"]) - 1+++def rect(x0, y0, x1, y1):+ return [((x0, y0), (x1, y0), (x1, y1)), ((x0, y0), (x1, y1), (x0, y1))]+++def seg(a, b, w):+ """a thin quad (two triangles) from a to b, w mm wide."""+ dx, dy = b[0] - a[0], b[1] - a[1]; L = math.hypot(dx, dy) or 1; nx, ny = -dy / L * w / 2, dx / L * w / 2+ p = [(a[0] + nx, a[1] + ny), (b[0] + nx, b[1] + ny), (b[0] - nx, b[1] - ny), (a[0] - nx, a[1] - ny)]+ return [(p[0], p[1], p[2]), (p[0], p[2], p[3])]+++def dashed_rect(x0, y0, x1, y1, w=0.012, dash=0.04, gap=0.025):+ tris = []; corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]+ for a, b in zip(corners, corners[1:]):+ L = math.hypot(b[0] - a[0], b[1] - a[1]); t = 0.0+ while t < L:+ t1 = min(t + dash, L); f0, f1 = t / L, t1 / L+ tris += seg((a[0] + (b[0] - a[0]) * f0, a[1] + (b[1] - a[1]) * f0), (a[0] + (b[0] - a[0]) * f1, a[1] + (b[1] - a[1]) * f1), w)+ t += dash + gap+ return tris+++ex = js.setdefault("extensionsUsed", [])+if "KHR_materials_unlit" not in ex: ex.append("KHR_materials_unlit")+mats = js.setdefault("materials", [])+def mat(name, rgba, tex=None):+ m = dict(name=name, alphaMode="BLEND", doubleSided=True, extensions={"KHR_materials_unlit": {}},+ pbrMetallicRoughness=dict(baseColorFactor=rgba, metallicFactor=0, roughnessFactor=1))+ if tex is not None: m["pbrMetallicRoughness"]["baseColorTexture"] = dict(index=tex)+ mats.append(m); return len(mats) - 1+m_line = mat("Datasheet note - amber dashes", AMBER + [0.95])+m_fill = mat("Datasheet note - amber fill", AMBER + [0.30])++# the four off-copper strips: bottom terminal x 0.565..1.10, inner end y 0.11..0.20 from the centre (pad starts at 0.20)+kids = []+centres = []+for sx in (-1, 1):+ for sy in (-1, 1):+ xa, xb = sorted((sx * 0.565, sx * 1.10)); ya, yb = sorted((sy * 0.11, sy * 0.20))+ kids.append(mesh("note_strip_fill_%+d%+d" % (sx, sy), rect(xa, ya, xb, yb), m_fill, tooltip=TIP))+ kids.append(mesh("note_strip_dash_%+d%+d" % (sx, sy), dashed_rect(xa - 0.008, ya - 0.008, xb + 0.008, yb + 0.008), m_line, tooltip=TIP))+ centres.append((sx * 0.83, sy * 0.155))++# text panel on the underside, between the pad columns (x -0.53..0.53), drawn to read correctly from BELOW+PW, PH = 1.0, 0.62 # mm+img = Image.new("RGBA", (2000, 1240), (18, 22, 28, 215)); dr = ImageDraw.Draw(img)+fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 92)+fr = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 74)+dr.rectangle([6, 6, 1993, 1233], outline=(255, 150, 20, 255), width=10)+lines = [("No copper under the amber strips", fb, (255, 170, 40)),+ ("Worldsemi's PCB Solder Pad drawing puts", fr, (235, 238, 242)),+ ("0.7 x 0.7 mm pads 0.40 mm apart. The", fr, (235, 238, 242)),+ ("terminals are 0.78 mm long on a 1.00 mm", fr, (235, 238, 242)),+ ("pitch, so the inner 0.09 mm of each one", fr, (235, 238, 242)),+ ("sits off the pad. The 0.40 mm gap keeps", fr, (235, 238, 242)),+ ("neighbouring pins from bridging.", fr, (235, 238, 242)),+ ("WS2812B-2020 datasheet p.2", fr, (150, 200, 210))]+y = 60+for t, f, c in lines:+ dr.text((70, y), t, font=f, fill=c); y += (120 if f is fb else 140) if t == lines[0][0] else 132+png = io.BytesIO(); img.save(png, "PNG")+js.setdefault("images", []).append(dict(bufferView=buf(png.getvalue()), mimeType="image/png", name="datasheet-note"))+js.setdefault("samplers", []).append(dict(magFilter=9729, minFilter=9987, wrapS=33071, wrapT=33071))+js.setdefault("textures", []).append(dict(source=len(js["images"]) - 1, sampler=len(js["samplers"]) - 1))+m_text = mat("Datasheet note - text", [1, 1, 1, 1], tex=len(js["textures"]) - 1)+x0, x1, y0, y1 = -PW / 2, PW / 2, -PH / 2, PH / 2+tri = [((x0, y0), (x1, y0), (x1, y1)), ((x0, y0), (x1, y1), (x0, y1))]+# seen from below, +X appears on the LEFT: the image's left edge (u = 0) goes at +X so the text reads correctly+uvx = lambda x: (x1 - x) / PW; uvy = lambda yv: (y1 - yv) / PH+uv = [(uvx(x), uvy(yv)) for t in tri for x, yv in t]+kids.append(mesh("note_text_panel", tri, m_text, uv=uv, tooltip=TIP))+# leader lines from the panel's corners to the strips+for (cx, cy) in centres:+ px, py = (math.copysign(PW / 2, cx), math.copysign(PH / 2 * 0.6, cy))+ kids.append(mesh("note_leader_%+.0f%+.0f" % (cx, cy), seg((px, py), (cx - math.copysign(0.27, cx), cy), 0.008), m_line))++# 0.40 mm dimension between the two pads of each column: ticks on the pad edges, a shaft with arrowheads, a label+def label_tex(text, w_px=900, h_px=260):+ im = Image.new("RGBA", (w_px, h_px), (18, 22, 28, 200)); d = ImageDraw.Draw(im)+ f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 150)+ tw = d.textlength(text, font=f); d.text(((w_px - tw) / 2, 40), text, font=f, fill=(255, 170, 40))+ b = io.BytesIO(); im.save(b, "PNG")+ js["images"].append(dict(bufferView=buf(b.getvalue()), mimeType="image/png", name="dim-" + text))+ js["textures"].append(dict(source=len(js["images"]) - 1, sampler=len(js["samplers"]) - 1))+ return mat("Datasheet note - dimension label", [1, 1, 1, 1], tex=len(js["textures"]) - 1)+m_dim = label_tex("0.40 mm")+GAP = 0.20 # pad inner edges at y = +/-0.20 (pads 0.7 tall on +/-0.55)+for sx in (-1, 1):+ xa = sx * 0.70 # arrow inside the pad column, clear of the castellation+ tris = []+ for sy in (-1, 1): # extension ticks along each pad edge+ tris += seg((sx * 0.60, sy * GAP), (sx * 1.00, sy * GAP), 0.008)+ tris += seg((xa, -GAP + 0.045), (xa, GAP - 0.045), 0.010) # shaft+ for sy in (-1, 1): # arrowheads touching the pad edges+ tip = (xa, sy * GAP); base = sy * (GAP - 0.05)+ tris.append(((xa - 0.025, base), (xa + 0.025, base), tip))+ kids.append(mesh("note_dim_040_%+d" % sx, tris, m_line, tooltip="0.40 mm between the pads (Worldsemi PCB Solder Pad drawing)"))+ # label beside the arrow, horizontal, readable from below+ lx0, lx1 = sorted((sx * 0.74, sx * 1.18)); ly0, ly1 = -0.065, 0.065+ tri2 = [((lx0, ly0), (lx1, ly0), (lx1, ly1)), ((lx0, ly0), (lx1, ly1), (lx0, ly1))]+ uv2 = [((lx1 - x) / (lx1 - lx0), (ly1 - yv) / (ly1 - ly0)) for t in tri2 for x, yv in t]+ kids.append(mesh("note_dim_label_%+d" % sx, tri2, m_dim, uv=uv2, tooltip="0.40 mm between the pads (Worldsemi PCB Solder Pad drawing)"))++js["nodes"].append(dict(name=NODE, children=kids,+ extras=dict(adomLayer=dict(key="datasheet_note", label="Datasheet note", group="Footprint"),+ tooltip=TIP)))+js["scenes"][js.get("scene", 0)]["nodes"].append(len(js["nodes"]) - 1)+js["buffers"][0]["byteLength"] = len(bin_) + (-len(bin_) % 4)+js["asset"].setdefault("extras", {}).setdefault("adomHero", {}).setdefault("layers", {})["datasheet_note"] = dict(+ what="underside callout: off-copper terminal strips + explanation, readable from below", source="WS2812B-2020 datasheet p.1-2")+write_glb(glb, js, bin_)+print(json.dumps(dict(glb=glb, nodes=len(kids), text_panel_mm=[PW, PH])))
ws2812b-2020-hero.glb⋯ 1 unchanged line ⋯