by adom
2020 Addressable RGB LED · 800 Kbps single-line cascade · 16 mA per channel (V1.0 sheet), 12 mA from V1.2
master
John Lauer
Hero: datasheet note moved below the Worldsemi logo and reworded for the dashed boxes; hidden 300 um solder-ball layer
982b906
16d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
#!/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 (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)"""
import io, json, math, struct, sys
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_))
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 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.")
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_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.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, but the", fr, (235, 238, 242)),
("terminals are 0.78 mm long on a 1.00 mm", 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 = 50
for t, f, c in lines:
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 = 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), 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
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 = [((x - lx0) / (lx1 - lx0), (yv - ly0) / (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])))