by adom
0603 SMD Green LED · 3.2 V drop · 25 mA max
master
John Lauer
LED colour pass: coloured STEP is the default (lens colour from the datasheet dominant wavelength, softened), plain STE…
c7192f0
20d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
#!/usr/bin/env python3
"""Lens colours for the Adom basic-part LEDs (second pass, after John's review on 2026-09-30:
"a bit less saturated so they feel a bit more real", "match the nanometer wavelength", "amber looks super red").
Method (OKLCh, sRGB D65):
1. hue = OKLab hue of the in-gamut sRGB colour whose dominant wavelength is EXACTLY the part's typical
lambda_d from the datasheet table (page recorded below): the D65 white -> spectral-locus line
(CIE 1931 2-deg) clipped at the sRGB gamut edge. (The OKLab hue of the raw spectral colour is not
used: OKLab is unreliable that far outside sRGB and turns 470 nm cyan.) Reds whose lambda_d lies
beyond the sRGB red primary (lambda_d ~611 nm) take the red primary's hue, 29.2 deg: sRGB cannot
show a redder hue, and clipping along the white line would drift them toward rose.
2. chroma = SAT x the gamut-cusp chroma at that hue (the most saturated sRGB colour of that hue), SAT = 0.65,
then reduced further only if needed to stay inside sRGB at the chosen lightness.
3. lightness L: red 0.60 (631 nm 0.56, darker, since sRGB cannot separate 622 and 631 by hue), amber 0.72, green 0.74, blue 0.55, so the lens reads as tinted epoxy, not paint.
Amber exception: the OKLab hue of 605 nm (35.6 deg) sits only ~10 deg from the red parts (24-26 deg) and
renders as coral-red. John asked for amber to read clearly amber-orange, between red and yellow, so the
amber DISPLAY hue is 60 deg at L 0.76: midway between the red parts (29.2 deg) and how a yellow LED reads on
screen (~90 deg; note the lambda_d-exact colour of the series' 591 nm yellow sibling is itself only 52.7 deg,
i.e. sRGB orange, which is the whole problem with long-wave colours on sRGB). First try (47 deg, L 0.72)
still rendered terracotta next to the red part. lambda_d 605 nm is still what is recorded; the hue shift is a display choice, stated.
White (phosphor-converted InGaN): the lens is the YAG phosphor, drawn as a pale warm yellow, low chroma
(OKLCh L 0.90, C 0.075, h 95); the emission chromaticity is recorded, not drawn.
Writes colours.tsv (slug, MPN, family, lambda_d, sRGB) and colours.json (full provenance)."""
import json, math
import wl2srgb as w
w.LOC.update({570: (.4441, .5547), 575: (.4788, .5202), 580: (.5125, .4866), 585: (.5448, .4544), 590: (.5752, .4242)})
SAT = 0.65
PARTS = [ # slug, MPN, family, lambda_d (typ) or CIE xy, datasheet page, table
('in-s42ata', 'IN-S42ATA', 'kicad-chip', 605, 'IN-S42AT series V1.1 p.3 Electrical Characteristics, lambda_D typ'),
('in-s42atb', 'IN-S42ATB', 'kicad-chip', 470, 'IN-S42AT series V1.1 p.3, lambda_D typ'),
('in-s42atg', 'IN-S42ATG', 'kicad-chip', 525, 'IN-S42AT series V1.1 p.3, lambda_D typ'),
('in-s42atr', 'IN-S42ATR', 'template', 631, 'IN-S42AT series V1.1 p.3, lambda_D typ'),
('in-s42atuw', 'IN-S42ATUW', 'kicad-chip', (0.295, 0.30), 'IN-S42AT series V1.1 p.3, CIE x/y (white, no lambda_D)'),
('in-s63bta', 'IN-S63BTA', 'kicad-chip', 605, 'IN-S63BT series (2022-03-10) p.3 Electrical Characteristics, lambda_D typ'),
('in-s63bt5b', 'IN-S63BT5B', 'kicad-chip', 468, 'IN-S63BT series p.3, lambda_D typ (5 mA test)'),
('in-s63btg', 'IN-S63BTG', 'kicad-chip', 527, 'IN-S63BT series p.3, lambda_D typ'),
('in-s63btr', 'IN-S63BTR', 'kicad-chip', 622, 'IN-S63BT series p.3, lambda_D typ'),
('in-s63bt5uw', 'IN-S63BT5UW', 'kicad-chip', (0.27, 0.255), 'IN-S63BT series p.3, CIE x/y (white, 5 mA, no lambda_D)'),
]
def ok2srgb(L, C, h):
a, b = C * math.cos(math.radians(h)), C * math.sin(math.radians(h)); return w.ok2lin([L, a, b])
def inside(lin): return all(-1e-7 <= c <= 1 + 1e-7 for c in lin)
def max_c(L, h):
lo, hi = 0.0, 0.5
for _ in range(50):
m = (lo + hi) / 2
if inside(ok2srgb(L, m, h)): lo = m
else: hi = m
return lo
def cusp(h): return max((max_c(0.2 + 0.78 * i / 399, h), 0.2 + 0.78 * i / 399) for i in range(400))
out = []
for slug, mpn, fam, lam, src in PARTS:
if isinstance(lam, tuple):
L, C, h = 0.90, 0.075, 95.0; note = 'white: pale warm YAG-phosphor yellow, low chroma (emission CIE x/y recorded, not drawn)'
spectral_h = None
else:
s_edge = w.along_white(w.xy2lin(*w.locus(lam)))
dec = lambda c: c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
Lk, a, b = w.lin2ok([dec(c) for c in s_edge]); spectral_h = math.degrees(math.atan2(b, a)) % 360; h = spectral_h
L = {'B': 0.55, 'G': 0.74, 'A': 0.76, 'R': 0.60}[mpn[-1]]
note = f'hue = OKLab hue ({h:.1f} deg) of the lambda_d-exact gamut-edge colour for {lam} nm'
if mpn.endswith('R') and lam > 625: L = 0.56 # 631 nm: same displayable hue as 622 nm, drawn darker (lower luminous efficacy)
if mpn.endswith('R') and lam > 611: h = 29.2; note = f'red: lambda_d {lam} nm is beyond the sRGB red primary; hue = red primary 29.2 deg (lambda_d-exact edge colour would be {spectral_h:.1f} deg, rose)'
if mpn.endswith('A'): h = 60.0; note = f'amber: display hue 60 deg, midway between red (29.2) and on-screen yellow (~90) (lambda_d-exact hue for 605 nm is {spectral_h:.1f} deg, reads red); see docstring'
Cc, Lc = cusp(h); C = min(SAT * Cc, max_c(L, h))
Cc, Lc = cusp(h); C = min(SAT * Cc, max_c(L, h))
lin = [min(1, max(0, c)) for c in ok2srgb(L, C, h)]; srgb = [round(w.enc(c), 4) for c in lin]
hexs = '#%02X%02X%02X' % tuple(round(c * 255) for c in srgb)
out.append(dict(slug=slug, mpn=mpn, family=fam, dominant_wavelength_nm=None if isinstance(lam, tuple) else lam,
cie_xy=list(lam) if isinstance(lam, tuple) else None, datasheet=src,
spectral_oklab_hue_deg=None if spectral_h is None else round(spectral_h, 1),
oklch=[round(L, 3), round(C, 4), round(h, 1)], srgb=srgb, hex=hexs, note=note))
print(f'{mpn:12s} {str(lam):14s} oklch({L:.2f} {C:.3f} {h:5.1f}) {hexs} {note}')
json.dump(dict(method=__doc__, saturation_of_cusp=SAT, parts=out), open('colours.json', 'w'), indent=1, ensure_ascii=False)
with open('colours.tsv', 'w') as f:
for o in out:
wl = o['dominant_wavelength_nm'] or ','.join(str(v) for v in o['cie_xy'])
f.write('\t'.join([o['slug'], o['mpn'], o['family'], str(wl), ','.join(str(v) for v in o['srgb'])]) + '\n')
f.write('ws2812b-2020\tWS2812B-2020\tws5050\tRGB\t0.82,0.84,0.86\n')