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 moved below the Worldsemi logo and reworded for the dashed boxes; hidden 300 um solder-ball layer
3 files changed
+125−19
provenance/tools/hero_post_callout.py+22−19@@ -4,8 +4,8 @@ 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,+ - four small amber dashed boxes (outline only) around those off-copper terminal strips,+ - a text panel on the underside, below the Worldsemi logo so the logo stays visible, with leader lines to the 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)"""@@ -31,7 +31,7 @@ if any(n.get("name") == NODE for n in js["nodes"]): sys.exit(json.dumps(dict(glb 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. Worldsemi's PCB Solder Pad drawing (WS2812B-2020 datasheet p.2) puts 0.7 x 0.7 mm "+TIP = ("No copper inside this amber dashed box. 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 is off the pad. The datasheet gives no reason; a 0.40 mm gap is the usual guard against solder " "bridges between neighbouring pins.")@@ -106,37 +106,40 @@ for sx in (-1, 1): 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.70 # mm-img = Image.new("RGBA", (2000, 1400), (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, 1393], outline=(255, 150, 20, 255), width=10)-lines = [("No copper under the amber strips", fb, (255, 170, 40)),+# text panel on the underside, between the pad columns (x -0.50..0.50) and below the logo (ring 0.875 mm across at+# (0.04, 0.09), so it ends at y = -0.35): y -0.96..-0.40, drawn to read correctly from BELOW+PW, PH = 1.0, 0.56 # mm+PCX, PCY = 0.0, -0.68 # panel centre, mm+img = Image.new("RGBA", (2000, 1120), (18, 22, 28, 215)); dr = ImageDraw.Draw(img)+fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 88)+fr = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 72)+dr.rectangle([6, 6, 1993, 1113], outline=(255, 150, 20, 255), width=10)+lines = [("No copper inside the dashed boxes", 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, while the", fr, (235, 238, 242)),+ ("0.7 x 0.7 mm pads 0.40 mm apart, but 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 is", fr, (235, 238, 242)),- ("off the pad. The datasheet gives no reason;", fr, (235, 238, 242)),- ("a 0.40 mm gap is the usual guard against", fr, (235, 238, 242)),- ("solder bridges between neighbouring pins.", fr, (235, 238, 242)),+ ("pitch, so their inner 0.09 mm is off the pad.", fr, (235, 238, 242)),+ ("No reason given; a 0.40 mm gap is the usual", fr, (235, 238, 242)),+ ("guard against solder bridges between pins.", fr, (235, 238, 242)), ("WS2812B-2020 datasheet p.2", fr, (150, 200, 210))]-y = 60+y = 50 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+ tw = dr.textlength(t, font=f); assert tw < 1880, (t, tw) # fits inside the frame+ dr.text((60, y), t, font=f, fill=c); y += 122 if f is fb else 124+assert y < 1100, y 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+x0, x1, y0, y1 = PCX - PW / 2, PCX + PW / 2, PCY - PH / 2, PCY + PH / 2 tri = [((x0, y0), (x1, y0), (x1, y1)), ((x0, y0), (x1, y1), (x0, y1))] uvx = lambda x: (x - x0) / PW; uvy = lambda yv: (yv - y0) / PH # reads upright in the Adom viewer's Bottom view 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))+ px, py = math.copysign(PW / 2, cx), y1 if cy > 0 else PCY # top corners reach up past the logo's side 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
provenance/tools/hero_post_solderballs.pyadded+103@@ -0,0 +1,103 @@+#!/usr/bin/env python3+"""Hero GLB: a hidden 'Solder balls' layer showing where the 300 um jet-paste balls land on the footprint's pads.++Follows the Adom footprint standard (john/adom-solder-ball-layer-standard): balls 0.30 mm across at 0.25 mm pitch+(adjacent balls overlap slightly, by design), each drawn as a hemisphere with its flat face on the pad, dull leaded+solder grey (63Sn/37Pb), the layer hidden by default. Ball positions use the same rule as adom-footprint's paste-dot+overlay (1.0.26): per SMD pad, a grid at 0.25 mm pitch centred on the pad, kept 0.06 mm inside the pad edge.+For this footprint that is 2 x 2 balls per 0.7 x 0.7 mm pad, 16 in all, identical to `adom-footprint embed`.++usage: hero_post_solderballs.py HERO.glb [FOOTPRINT.kicad_mod] (in place; refuses to run twice)+The footprint defaults to the single .kicad_mod beside the GLB."""+import glob, json, math, os, re, struct, sys++D, PITCH, MARGIN = 0.30, 0.25, 0.06 # mm+MM = 0.001+Z0 = -0.016 # flat face just above the copper fill (-0.018 mm), under the chip seat+SEG, RINGS = 16, 6+NODE = "Solder balls - 300 um jet paste"+TIP = ("Jet-paste solder ball, 0.30 mm across, 0.25 mm pitch (Adom 300 um solder-ball layer standard).\n"+ "63Sn/37Pb leaded paste; 2 x 2 per pad on this footprint, 16 in all.")+++def read_glb(p):+ b = open(p, "rb").read(); n = struct.unpack("<I", b[12:16])[0]; js = json.loads(b[20:20 + n]); o = 20 + n+ return js, bytearray(b[o + 8:o + 8 + struct.unpack("<I", b[o:o + 4])[0]] if o < len(b) else b"")+++def write_glb(p, js, bin_):+ j = json.dumps(js, separators=(",", ":")).encode(); j += b" " * (-len(j) % 4); bin_ += b"\0" * (-len(bin_) % 4)+ open(p, "wb").write(struct.pack("<III", 0x46546C67, 2, 12 + 8 + len(j) + 8 + len(bin_)) + struct.pack("<II", len(j), 0x4E4F534A) + j+ + struct.pack("<II", len(bin_), 0x004E4942) + bytes(bin_))+++def pads(mod):+ t = open(mod).read(); out = []+ for m in re.finditer(r'\(pad\s+"?([^"\s]*)"?\s+smd\s+\w+\s+\(at\s+([-\d.]+)\s+([-\d.]+)(?:\s+[-\d.]+)?\)\s+\(size\s+([-\d.]+)\s+([-\d.]+)\)', t):+ out.append(dict(num=m.group(1), x=float(m.group(2)), y=-float(m.group(3)), w=float(m.group(4)), h=float(m.group(5)))) # KiCad y down -> y up+ return out+++def grid(c, span):+ n = max(1, int(math.floor((span - 2 * MARGIN - D) / PITCH + 1e-9)) + 1)+ return [c + (i - (n - 1) / 2) * PITCH for i in range(n)]+++glb = sys.argv[1]+mod = sys.argv[2] if len(sys.argv) > 2 else (glob.glob(os.path.join(os.path.dirname(os.path.abspath(glb)), "*.kicad_mod")) or [None])[0]+if not mod: sys.exit("no .kicad_mod beside the GLB; pass one")+js, bin_ = read_glb(glb)+if any(n.get("name") == NODE for n in js["nodes"]): sys.exit(json.dumps(dict(glb=glb, skip="solder balls already present")))+balls = [(x, y, p["num"]) for p in pads(mod) for y in grid(p["y"], p["h"]) for x in grid(p["x"], p["w"])]+++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+++# one hemisphere mesh at the origin (mm), dome up (+Z), flat disc down; every ball is a node translated onto its pad+r = D / 2; P, N, I = [], [], []+for k in range(RINGS + 1): # dome: rings from the equator (k=0) to the pole+ th = (math.pi / 2) * k / RINGS+ for s in range(SEG + 1):+ ph = 2 * math.pi * s / SEG; nx, ny, nz = math.cos(th) * math.cos(ph), math.cos(th) * math.sin(ph), math.sin(th)+ P.append((r * nx, r * ny, r * nz)); N.append((nx, ny, nz))+for k in range(RINGS):+ for s in range(SEG):+ a = k * (SEG + 1) + s; b = a + SEG + 1+ I += [a, a + 1, b, a + 1, b + 1, b]+c0 = len(P); P.append((0, 0, 0)); N.append((0, 0, -1))+for s in range(SEG + 1): # flat face on the pad+ ph = 2 * math.pi * s / SEG; P.append((r * math.cos(ph), r * math.sin(ph), 0)); N.append((0, 0, -1))+for s in range(SEG):+ I += [c0, c0 + 2 + s, c0 + 1 + s]+pos = b"".join(struct.pack("<3f", x * MM, y * MM, z * MM) for x, y, z in P)+nrm = b"".join(struct.pack("<3f", *n) for n in N)+idx = b"".join(struct.pack("<H", i) for i in I)+acc = js.setdefault("accessors", [])+acc.append(dict(bufferView=buf(pos, 34962), componentType=5126, count=len(P), type="VEC3",+ min=[-r * MM, -r * MM, 0], max=[r * MM, r * MM, r * MM]))+acc.append(dict(bufferView=buf(nrm, 34962), componentType=5126, count=len(N), type="VEC3"))+acc.append(dict(bufferView=buf(idx, 34963), componentType=5123, count=len(I), type="SCALAR"))+js.setdefault("materials", []).append(dict(name="Solder ball - 63Sn/37Pb, dull", doubleSided=True,+ pbrMetallicRoughness=dict(baseColorFactor=[0.60, 0.61, 0.63, 1.0], metallicFactor=0.55, roughnessFactor=0.6)))+js.setdefault("meshes", []).append(dict(name="solder_ball_300um", primitives=[dict(+ attributes=dict(POSITION=len(acc) - 3, NORMAL=len(acc) - 2), indices=len(acc) - 1, material=len(js["materials"]) - 1)]))+mesh_i = len(js["meshes"]) - 1+kids = []+for x, y, num in balls:+ js["nodes"].append(dict(name="solder_ball_pad%s_%+.3f_%+.3f" % (num, x, y), mesh=mesh_i, translation=[x * MM, y * MM, Z0 * MM],+ extras=dict(tooltip="Pad %s solder ball\n%s" % (num, TIP))))+ kids.append(len(js["nodes"]) - 1)+js["nodes"].append(dict(name=NODE, children=kids, extras=dict(+ adomLayer=dict(key="solder_balls", label="Solder balls (300 um jet paste)", group="Footprint", hidden=True), 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", {})["solder_balls"] = dict(+ what="300 um jet-paste solder balls on the footprint pads, hidden by default", source="john/adom-solder-ball-layer-standard; " + os.path.basename(mod))+write_glb(glb, js, bin_)+print(json.dumps(dict(glb=glb, balls=len(balls), per_pad=len(balls) // max(1, len(pads(mod))), footprint=os.path.basename(mod))))
ws2812b-2020-hero.glb⋯ 1 unchanged line ⋯