This commit is contained in:
Bane Stojkovic
2026-10-04 22:40:26 +02:00
parent da5184a226
commit ad3a3fe46d
16 changed files with 1069 additions and 169 deletions
@@ -525,6 +525,10 @@
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Jewel (WS2812B) - 7 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Panel (WS2812B) - 8x8, 64 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Ring (WS2812B) - 12, 16 and 24 LEDs</description>
@@ -627,18 +627,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
* as the inch measures they work out to exactly, so a board that wants smaller pads overrides
* these rather than a second pad-drawing method being written somewhere else.
*/
protected Size getSolderPadWidth() {
return new Size(0.11d, SizeUnit.in);
}
protected Size getSolderPadLength() {
return new Size(0.08d, SizeUnit.in);
}
protected Size getSolderPadHoleSize() {
return new Size(0.035d, SizeUnit.in);
}
/**
* Spreads {@link #RGB_LED_GRADIENT} evenly over {@code count} LEDs, interpolating between
* neighbouring anchors and treating them as a loop so the last LED runs back into the first.
@@ -661,31 +649,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
return colors;
}
protected void drawSolderPads(Graphics2D g2d, int startIndex, int count, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
double padW = getSolderPadWidth().convertToPixels();
double padH = getSolderPadLength().convertToPixels();
double holeD = getSolderPadHoleSize().convertToPixels();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
RoundRectangle2D pad = new RoundRectangle2D.Double(p.getX() - padW / 2.0, p.getY() - padH / 2.0, padW, padH, 2, 2);
g2d.setColor(LIGHT_METAL_COLOR);
g2d.fill(pad);
g2d.setColor(LIGHT_METAL_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// Central through-hole / drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Helper to draw standard PCB through-hole solder pads (gold/copper pads with drill holes, square for Pin 1).
*
@@ -729,6 +692,47 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
drawingObserver.stopTrackingContinuityArea();
}
/**
* Helper to draw rectangular surface solder pads: bare copper on the face of the board with no
* drill through it, which is what a part meant to be soldered to directly carries rather than
* the plated holes {@link #drawPcbSolderPads} draws.
*
* <p>Unlike a round pad a rectangular one has an orientation, so each is turned with the board.
*
* @param padWidth Pad size across the board, which for an edge pad is its reach inward
* @param padLength Pad size along the row of pads
*/
protected void drawSurfacePads(Graphics2D g2d, int startIndex, int count, Size padWidth,
Size padLength, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) {
return;
}
double w = padWidth.convertToPixels();
double h = padLength.convertToPixels();
double theta = orientation.toRadians();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (theta != 0) {
g2d.rotate(theta, p.getX(), p.getY());
}
Rectangle2D pad = new Rectangle2D.Double(p.getX() - w / 2.0, p.getY() - h / 2.0, w, h);
g2d.setColor(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// turned back rather than saving the transform, as the LED painter does
if (theta != 0) {
g2d.rotate(-theta, p.getX(), p.getY());
}
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Pad, drill-hole and edge-notch diameters used by {@link #drawCastellatedPads} and
* {@link #subtractCastellationNotches}. A board whose castellations differ from the package
@@ -47,7 +47,7 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "LED Matrix (MAX7219)", category = "Displays & Outputs",
@ComponentDescriptor(name = "LED Matrix", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
@@ -71,11 +71,6 @@ public class WS2812BJewel extends AbstractMakerBoard {
public static Size MOUNTING_HOLE_SPACING = new Size(19.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
// The same 5050-family pads the rings carry.
public static Size PAD_WIDTH = new Size(1.6d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.3d, SizeUnit.mm);
public static Size PAD_HOLE = new Size(0.7d, SizeUnit.mm);
/** The LEDs on the circle. The seventh sits in the middle and is not one of them. */
public static final int RING_LED_COUNT = 6;
@@ -104,21 +99,6 @@ public class WS2812BJewel extends AbstractMakerBoard {
return Integer.toString(index + 1);
}
@Override
protected Size getSolderPadWidth() {
return PAD_WIDTH;
}
@Override
protected Size getSolderPadLength() {
return PAD_LENGTH;
}
@Override
protected Size getSolderPadHoleSize() {
return PAD_HOLE;
}
private double getOuterRadius() {
return OUTER_DIAMETER.convertToPixels() / 2.0;
}
@@ -236,7 +216,7 @@ public class WS2812BJewel extends AbstractMakerBoard {
g2d.setTransform(oldTx);
drawSolderPads(g2d, 0, PAD_NAMES.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -0,0 +1,276 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// Every figure here is read off Adafruit's published EagleCAD board file for the part rather than
// from a product listing, which gives only the outline. The pitch, the hole pattern, the port
// positions and the order the pixels are chained in are all things the drawing needs and no
// vendor states.
//
// Three relationships hold on this board, and they are recorded as the measurements they are
// rather than derived in code, because a panel from another maker need not share any of them: the
// board is exactly eight pitches across, the pixel grid is centred so that half a pitch of margin
// is left outside the outer pixels, and the port row lands in the gap between the first and second
// rows of pixels.
@ComponentDescriptor(name = "NeoPixel Panel", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B NeoPixel Matrix Panel (8x8, 64 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BPanel extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
public static Size BOARD_SIZE = new Size(71.12d, SizeUnit.mm);
public static Size PIXEL_PITCH = new Size(8.89d, SizeUnit.mm);
/** How far the outer pad of a port sits in from the side edge its port stands against. */
public static Size PAD_EDGE_INSET = new Size(1.905d, SizeUnit.mm);
/** How far a port row sits in from the end of the board it runs beside. */
public static Size PAD_ROW_INSET = new Size(8.89d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.8d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.0d, SizeUnit.mm);
/** Pixels along one edge of the square matrix. */
public static final int MATRIX_ORDER = 8;
/**
* Hole centres in millimetres from the middle of the board. Six stand in from the side edges and
* six in the gaps between pixel columns; the pattern is symmetric about both axes, so which way
* the vertical axis points does not change the drawing.
*/
private static final double[][] MOUNTING_HOLES = new double[][] {
{-33.02d, 17.78d}, {-33.02d, 0d}, {-33.02d, -17.78d},
{33.02d, 17.78d}, {33.02d, 0d}, {33.02d, -17.78d},
{-8.89d, 26.67d}, {8.89d, 26.67d},
{-8.89d, 0d}, {8.89d, 0d},
{-8.89d, -26.67d}, {8.89d, -26.67d}};
/**
* Input first, then output. Both ports carry the same two supply rails, so the rails are
* numbered to keep the node names distinct the way the Stick's four grounds are; the board
* prints them simply +5V and GND, and on its back rather than this face.
*/
public static final String[] PAD_NAMES =
new String[] {"DIN", "+5V_1", "GND_1", "DOUT", "+5V_2", "GND_2"};
// Drawn lit, as every addressable part in this package is. The colour is handed out along the
// data chain rather than across the board, so the sweep shows the order the pixels are addressed
// in: this panel is wired as a progressive raster, left to right along each row and then back to
// the left of the next, not as the serpentine most cheap panels use.
public static Color[] LED_COLORS = buildLedGradient(MATRIX_ORDER * MATRIX_ORDER);
public WS2812BPanel() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
/**
* There is only one of these panels, so unlike a board with a variant property this is not
* needed to keep the BOM's rows apart. It is still the better answer than an empty value column.
*/
@Override
protected String getVariantLabel() {
return "8x8, 64 LEDs";
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** Left edge of the board; the input port's first pad sits one inset in from it. */
private double getBoardX(double x) {
return x - PAD_EDGE_INSET.convertToPixels();
}
/** Top edge of the board; the input port's row sits its own inset down from it. */
private double getBoardY(double y) {
return y - PAD_ROW_INSET.convertToPixels();
}
/**
* The two ports are each other turned about the centre of the board, which is what lets panels
* be chained: the input runs inward from the left edge near the top, the output inward from the
* right edge near the bottom.
*/
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double board = BOARD_SIZE.convertToPixels();
double acrossBoard = board - 2 * PAD_EDGE_INSET.convertToPixels();
double downBoard = board - 2 * PAD_ROW_INSET.convertToPixels();
double[][] relativeOffsets = new double[PAD_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
relativeOffsets[3 + i][0] = acrossBoard - i * spacing;
relativeOffsets[3 + i][1] = downBoard;
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double board = BOARD_SIZE.convertToPixels();
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), board, board,
radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double board = BOARD_SIZE.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
double centreX = boardX + board / 2.0;
double centreY = boardY + board / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (double[] hole : MOUNTING_HOLES) {
MakerBoardPainter.drawMountingHole(g2d, centreX + px(hole[0]), centreY + px(hole[1]),
holeSize);
}
double pitch = PIXEL_PITCH.convertToPixels();
double margin = (board - (MATRIX_ORDER - 1) * pitch) / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
for (int i = 0; i < LED_COLORS.length; i++) {
MakerBoardPainter.drawAddressableLed(g2d, boardX + margin + (i % MATRIX_ORDER) * pitch,
boardY + margin + (i / MATRIX_ORDER) * pitch, ledSize, LED_COLORS[i]);
}
// The port names are printed on the back of the real board, so this face carries none.
}
g2d.setTransform(oldTx);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
double boardSize = Math.min(width, height) - 4;
double boardX = (width - boardSize) / 2.0;
double boardY = (height - boardSize) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
// A four by four grid rather than the part's eight by eight: at toolbox size eight rows of
// dots fill in to a solid block and the panel stops reading as a panel.
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
int order = 4;
double pitch = boardSize / order;
double dotSize = Math.max(2.0, pitch * 0.55);
for (int i = 0; i < order * order; i++) {
double dotX = boardX + (i % order + 0.5) * pitch - dotSize / 2.0;
double dotY = boardY + (i / order + 0.5) * pitch - dotSize / 2.0;
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize));
}
}
}
@@ -59,9 +59,6 @@ public class WS2812BRing extends AbstractMakerBoard {
// Pads are small and sit near the rim; these are what the ring overrides the inherited solder
// pad footprint with, the default being sized for a breakout board rather than a ring.
public static Size PAD_WIDTH = new Size(1.6d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.3d, SizeUnit.mm);
public static Size PAD_HOLE = new Size(0.7d, SizeUnit.mm);
public static Size PAD_EDGE_INSET = new Size(1.4d, SizeUnit.mm);
// The ring is drawn lit: unlit it is a black disc with white specks on it, which reads as no
@@ -104,21 +101,6 @@ public class WS2812BRing extends AbstractMakerBoard {
return Integer.toString(index + 1);
}
@Override
protected Size getSolderPadWidth() {
return PAD_WIDTH;
}
@Override
protected Size getSolderPadLength() {
return PAD_LENGTH;
}
@Override
protected Size getSolderPadHoleSize() {
return PAD_HOLE;
}
/** The circle the LED centres sit on: the midpoint between the inner and outer rims. */
private double getMidRadius() {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
@@ -254,7 +236,7 @@ public class WS2812BRing extends AbstractMakerBoard {
g2d.setTransform(oldTx);
drawSolderPads(g2d, 0, ringSize.getPadNames().length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, ringSize.getPadNames().length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -59,7 +59,21 @@ public class WS2812BStick extends AbstractMakerBoard {
public static Size BOARD_WIDTH = new Size(51.1d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(10.22d, SizeUnit.mm);
public static Size PAD_INSET = new Size(2.0d, SizeUnit.mm);
// Surface pads rather than plated holes, and flush with the cut end rather than inset from it,
// so how far in their centres sit is half their width and not a figure of its own. The width is
// the reach inward from the edge; the length is what has to fit the 0.1 inch pad pitch.
public static Size PAD_WIDTH = new Size(3.0d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
// Air between a pad and the nearest package, as the strip keeps at its cut ends. Spreading the
// row to the pads' inner edge instead leaves the two exactly touching, which draws as copper
// against plastic with no board between them and puts one over the other on any rounding.
public static Size PAD_CLEARANCE = new Size(0.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(1.0d, SizeUnit.in);
public static Size MOUNTING_HOLE_TOP_OFFSET = new Size(2.0d, SizeUnit.mm);
// Drawn lit, off the same colour wheel as the ring, so the two read as the same family of part.
public static Color[] LED_COLORS_8 = buildLedGradient(8);
@@ -90,8 +104,9 @@ public class WS2812BStick extends AbstractMakerBoard {
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// the two pad columns sit the same distance in from each end of the board
double columnSpacing = BOARD_WIDTH.convertToPixels() - 2 * PAD_INSET.convertToPixels();
// the two pad columns stand flush against their own ends, so what separates them is the
// board less one whole pad
double columnSpacing = BOARD_WIDTH.convertToPixels() - PAD_WIDTH.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 4; i++) {
@@ -104,9 +119,10 @@ public class WS2812BStick extends AbstractMakerBoard {
rotatePoints(firstPoint, relativeOffsets);
}
/** Left edge of the board; the first pad column sits {@code PAD_INSET} in from it. */
/** Left edge of the board; the first pad column is flush with it, so its centres sit half a
* pad inboard. */
private double getBoardX(double x) {
return x - PAD_INSET.convertToPixels();
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Top edge of the board, derived so that the four-pad column is centred across its width. */
@@ -115,6 +131,36 @@ public class WS2812BStick extends AbstractMakerBoard {
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
/**
* Where the package of the given LED sits. The row is spread evenly between the inner edges of
* the two pad columns rather than packed edge to edge, and it sits low on the board: the
* mounting holes take the top of it, so the packages are centred in what is left below them
* rather than on the board's own middle.
*/
Point2D getLedCentre(int index) {
Point2D p0 = controlPoints[0];
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double ledSize = RGB_LED_SIZE.convertToPixels();
double inset = PAD_WIDTH.convertToPixels() + PAD_CLEARANCE.convertToPixels();
double fieldStart = boardX + inset;
double fieldEnd = boardX + BOARD_WIDTH.convertToPixels() - inset;
double pitch = (fieldEnd - fieldStart - ledSize) / (LED_COLORS_8.length - 1);
return new Point2D.Double(fieldStart + ledSize / 2.0 + index * pitch,
(getHoleCentre(false).getY() + MOUNTING_HOLE_SIZE.convertToPixels() / 2.0 + boardY
+ BOARD_HEIGHT.convertToPixels()) / 2.0);
}
/** One of the two mounting holes, which sit a fixed span apart astride the board's centre. */
Point2D getHoleCentre(boolean right) {
Point2D p0 = controlPoints[0];
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(
getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0 + (right ? offset : -offset),
getBoardY(p0.getY()) + MOUNTING_HOLE_TOP_OFFSET.convertToPixels());
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
@@ -138,11 +184,6 @@ public class WS2812BStick extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
@@ -157,29 +198,29 @@ public class WS2812BStick extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// Eight 5mm packages spread evenly between the two pad columns rather than packed edge to
// edge. The field has to clear the header block drawPinHeader paints around each pin, which
// reaches one pin width plus a pixel either side of the control point and is drawn after the
// LEDs -- starting the field at the pad inset alone put that block over the first and last
// package. The pad names are printed on the back of the real board, so this face carries no
// silkscreen.
double ledSize = RGB_LED_SIZE.convertToPixels();
double padInset = PAD_INSET.convertToPixels();
double headerHalf = PIN_SIZE.convertToPixels() + 1;
double fieldStart = boardX + padInset + headerHalf;
double fieldEnd = boardX + boardW - padInset - headerHalf;
double pitch = (fieldEnd - fieldStart - ledSize) / 7.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean right : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(right);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
for (int i = 0; i < 8; i++) {
MakerBoardPainter.drawAddressableLed(g2d, fieldStart + ledSize / 2.0 + i * pitch,
boardY + boardH / 2.0, ledSize, LED_COLORS_8[i]);
// The packages go down before the pads do, so the row starts at the pads' inner edge rather
// than under them. The pad names are printed on the back of the real board, so this face
// carries no silkscreen.
double ledSize = RGB_LED_SIZE.convertToPixels();
for (int i = 0; i < LED_COLORS_8.length; i++) {
Point2D led = getLedCentre(i);
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(), ledSize, LED_COLORS_8[i]);
}
}
g2d.setTransform(oldTx);
// Draw input and output header pins
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
// The real stick ships bare and is wired by soldering to its pads, so it carries pads rather
// than a fitted header. They are surface copper at the cut ends rather than the plated holes
// the ring and the jewel carry.
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -45,7 +45,7 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "NeoPixel Strip (WS2812B)", category = "Displays & Outputs",
@ComponentDescriptor(name = "NeoPixel Strip", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B LED Strip (30, 60 or 144 LEDs per metre)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
@@ -57,23 +57,32 @@ public class WS2812BStrip extends AbstractMakerBoard {
public static Color TAPE_WHITE = Color.decode("#F2F2F2");
// How far in from the cut end the pads sit, and the air left between a pad and the nearest LED.
public static Size PAD_END_INSET = new Size(2.5d, SizeUnit.mm);
// Surface copper flush with the cut rather than plated holes set in from it: tape is cut
// through the middle of a pad pair, so each cut end carries half a pad on its very edge. The
// 3 x 1.5 mm footprint is the stick's, taken from that part rather than measured on tape, so it
// is the figure to correct first if the drawing looks wrong.
public static Size PAD_WIDTH = new Size(3.0d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
// The air left between a pad and the nearest package.
public static Size PAD_CLEARANCE = new Size(0.5d, SizeUnit.mm);
/**
* A cut length of tape, not a board, so the count is a free number rather than an enum: the part
* is whatever the builder cuts. The ceiling is not about the control-point array, which stays at
* four whatever the count, but about the shape -- 144 LEDs of 30/m tape is already most of five
* six whatever the count, but about the shape -- 144 LEDs of 30/m tape is already most of five
* metres, and past that the drawing stops being useful before the maths does.
*/
public static final int MIN_LED_COUNT = 1;
public static final int MAX_LED_COUNT = 144;
public static final int DEFAULT_LED_COUNT = 8;
// The tape carries power and data in at one end and passes data out at the other. Only the data
// line is brought out at the far end, as the plan has it: a strip is chained by its own pads.
public static final String[] PIN_NAMES = new String[] {"+5V", "DIN", "GND", "DOUT"};
// Both cuts carry all three lines, which is what makes tape chainable: the supply rails run the
// whole length as continuous copper and only the data pad changes sense from one end to the
// other. The rails are numbered to keep the node names distinct, as the stick's grounds are;
// the tape prints them +5V and GND at both ends.
public static final String[] PIN_NAMES =
new String[] {"+5V_1", "DIN", "GND_1", "+5V_2", "DOUT", "GND_2"};
private Density density = Density._60;
private int ledCount = DEFAULT_LED_COUNT;
@@ -154,28 +163,28 @@ public class WS2812BStrip extends AbstractMakerBoard {
return ledColors;
}
/** The span the three input pads occupy across the tape, at the library's standard pad pitch. */
/** The span a column of three pads occupies across the tape, at the standard pad pitch. */
private static double getPadRunLength() {
return 2 * PIN_SPACING.convertToPixels();
}
/**
* How far the LED field starts in from each cut end. The pads come first and the packages must
* clear them, so this is derived from the pad footprint rather than chosen: half a pad plus a
* half package, with a little air between. Deriving it is what stops a dense tape drawing its
* first LED on top of its own input pad.
* How far the first package's centre sits in from a cut end. The pads come first and the
* packages must clear them, so this is derived from the pad footprint rather than chosen: the
* whole pad, a little air, then a half package. Deriving it is what stops a dense tape drawing
* its first LED on top of its own input pad.
*/
private double getLeadIn() {
return PAD_END_INSET.convertToPixels() + getSolderPadWidth().convertToPixels() / 2.0
+ RGB_LED_SIZE.convertToPixels() / 2.0 + PAD_CLEARANCE.convertToPixels();
return PAD_WIDTH.convertToPixels() + PAD_CLEARANCE.convertToPixels()
+ RGB_LED_SIZE.convertToPixels() / 2.0;
}
/** Left end of the tape; control point 0 is the first pad, one inset in from the cut. */
/** Left end of the tape; the pads are flush with it, so their centres sit half a pad inboard. */
private double getBoardX(double x) {
return x - PAD_END_INSET.convertToPixels();
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Top edge. The input pads are centred across the width, so the first sits above centre. */
/** Top edge. Each column of pads is centred across the width, so the first sits above centre. */
private double getBoardY(double y) {
return y - (getTapeWidth() - getPadRunLength()) / 2.0;
}
@@ -184,22 +193,26 @@ public class WS2812BStrip extends AbstractMakerBoard {
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double inset = PAD_END_INSET.convertToPixels();
// +5V, DIN and GND run across the input end at the standard pad pitch, centred on the width;
// DOUT sits alone at the far end on the centre line, because that end carries only the data
// pad. Spacing them by pitch rather than by a fraction of the width keeps them from touching:
// a pad is 0.08in tall, so fractions of a 10mm tape would leave them almost edge to edge.
// Each cut end carries +5V, data and GND in the same three rows, because those rows are
// continuous copper running the length of the tape. Spacing them by the pad pitch rather than
// by a fraction of the width keeps them from touching: fractions of a 10 mm tape would leave
// them almost edge to edge.
//
// Offsets are measured from control point 0, which is itself one inset inboard of the left
// cut -- so reaching a point one inset inboard of the *right* cut costs two insets, not one.
// Getting that wrong puts the output pad centre exactly on the tape edge, half of it hanging
// off the end, which is easy to miss because it still clears the last LED.
// Both columns stand flush against their own cut, so what separates them is the tape less one
// whole pad. The offsets are measured from control point 0, which is itself half a pad inboard
// of the left cut; an earlier version inset the pads instead, and reaching a point one inset
// inboard of the *right* cut then cost two insets rather than one, which put the far pad's
// centre on the tape edge with half of it hanging off.
double columnSpacing = getTapeLength() - PAD_WIDTH.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
relativeOffsets[1][1] = spacing;
relativeOffsets[2][1] = 2 * spacing;
relativeOffsets[3][0] = getTapeLength() - 2 * inset;
relativeOffsets[3][1] = spacing;
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[3 + i][0] = columnSpacing;
relativeOffsets[3 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
@@ -262,7 +275,8 @@ public class WS2812BStrip extends AbstractMakerBoard {
g2d.setTransform(oldTx);
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -216,7 +216,7 @@ public class MT3608BoostConverter extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw square/rounded rectangular tinned solder pads using LIGHT_METAL_COLOR
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -188,7 +188,7 @@ public class TP4056Charger extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw tinned solder pads
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -83,11 +83,10 @@ public class WS2812BJewelTest {
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, size, size);
}
/** Axis-aligned, as {@code drawSolderPads} draws it -- pads are not turned to face the centre. */
/** The shared round pad's bounding box; pads are not turned to face the centre. */
private static Rectangle2D padRect(Point2D pad) {
double w = WS2812BJewel.PAD_WIDTH.convertToPixels();
double h = WS2812BJewel.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
double size = WS2812BJewel.PAD_SIZE.convertToPixels();
return new Rectangle2D.Double(pad.getX() - size / 2.0, pad.getY() - size / 2.0, size, size);
}
@Test
@@ -0,0 +1,236 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the NeoPixel panel. Dimensions can be checked against the board file they
* came from, so what is worth pinning down here is the arrangement, which cannot: the two ports
* are each other turned about the centre of the board, and both the ports and the mounting holes
* sit in the gaps of the pixel grid rather than on bare margin. Every one of those passes whatever
* the dimensions are, and fails silently in the drawing if it is wrong.
*/
public class WS2812BPanelTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double px(double millimetres) {
return millimetres * PX_PER_MM;
}
private static final int ORDER = WS2812BPanel.MATRIX_ORDER;
/** The board file's hole pattern, in millimetres from the middle of the board. */
private static final double[][] HOLES_MM = new double[][] {
{-33.02d, 17.78d}, {-33.02d, 0d}, {-33.02d, -17.78d},
{33.02d, 17.78d}, {33.02d, 0d}, {33.02d, -17.78d},
{-8.89d, 26.67d}, {8.89d, 26.67d},
{-8.89d, 0d}, {8.89d, 0d},
{-8.89d, -26.67d}, {8.89d, -26.67d}};
/** Where a pixel's centre sits, counted along the data chain, in component coordinates. */
private static Point2D ledCentre(WS2812BPanel panel, int index) {
Rectangle2D board = panel.getBodyShape().getBounds2D();
double pitch = WS2812BPanel.PIXEL_PITCH.convertToPixels();
double margin = (board.getWidth() - (ORDER - 1) * pitch) / 2.0;
return new Point2D.Double(board.getX() + margin + (index % ORDER) * pitch,
board.getY() + margin + (index / ORDER) * pitch);
}
@Test
public void theBoardIsTheSquareItWasMeasuredAs() {
Rectangle2D board = new WS2812BPanel().getBodyShape().getBounds2D();
Assert.assertEquals(px(71.12d), board.getWidth(), 0.01d);
Assert.assertEquals("the board is not square", board.getWidth(), board.getHeight(), 0.01d);
}
/**
* On the 8x8 the board is exactly eight pitches across, which puts a half-pitch of margin
* outside the outer pixels and lands the port row in the first row gap. That is a property of
* this particular board rather than of panels in general, so it is asserted rather than relied
* on: a later panel that does not share it should fail here and be given its own figures.
*/
@Test
public void theBoardIsAWholeNumberOfPitchesAcross() {
Assert.assertEquals(ORDER * WS2812BPanel.PIXEL_PITCH.convertToPixels(),
WS2812BPanel.BOARD_SIZE.convertToPixels(), 0.01d);
}
@Test
public void thePixelGridIsCentredOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
Point2D first = ledCentre(panel, 0);
Point2D last = ledCentre(panel, ORDER * ORDER - 1);
Assert.assertEquals("left margin", first.getX() - board.getX(),
board.getMaxX() - last.getX(), 0.01d);
Assert.assertEquals("top margin", first.getY() - board.getY(),
board.getMaxY() - last.getY(), 0.01d);
}
/**
* The chain runs left to right along each row and then back to the left of the next, which is
* how Adafruit wires this panel and not how the serpentine panels are wired. The colours are
* handed out along the chain, so getting it wrong would show as a gradient that folds back on
* itself every row.
*/
@Test
public void theChainIsAProgressiveRaster() {
WS2812BPanel panel = new WS2812BPanel();
Assert.assertEquals("the row does not advance along the chain", ledCentre(panel, 0).getY(),
ledCentre(panel, ORDER - 1).getY(), 0.01d);
Assert.assertEquals("the chain does not return to the left edge",
ledCentre(panel, 0).getX(), ledCentre(panel, ORDER).getX(), 0.01d);
Assert.assertEquals("the chain does not step down a row",
WS2812BPanel.PIXEL_PITCH.convertToPixels(),
ledCentre(panel, ORDER).getY() - ledCentre(panel, 0).getY(), 0.01d);
}
@Test
public void controlPointsAreTheTwoPortsInOrder() {
WS2812BPanel panel = new WS2812BPanel();
Assert.assertEquals(6, panel.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < panel.getControlPointCount(); i++) {
String name = panel.getControlPointNodeName(i);
Assert.assertNotNull("pin " + i, name);
Assert.assertTrue("duplicate node name " + name, names.add(name));
}
Assert.assertEquals("DIN", panel.getControlPointNodeName(0));
Assert.assertEquals("DOUT", panel.getControlPointNodeName(3));
}
/**
* The two ports are each other turned half a turn about the middle of the board, which is what
* lets panels be butted together and chained. Nothing in the drawing shows this is wrong; the
* output simply ends up somewhere a wire cannot reach.
*/
@Test
public void thePortsAreEachOtherTurnedAboutTheCentre() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Point2D in = panel.getControlPoint(i);
Point2D out = panel.getControlPoint(3 + i);
Assert.assertEquals("pad " + i + " x", 2 * board.getCenterX() - in.getX(), out.getX(),
0.01d);
Assert.assertEquals("pad " + i + " y", 2 * board.getCenterY() - in.getY(), out.getY(),
0.01d);
}
}
@Test
public void portPadsSitAtHeaderPitchAndStayOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
double padHalf = AbstractMakerBoard.PAD_SIZE.convertToPixels() / 2.0;
MakerBoardTestSupport.assertRow(panel, 0, 2);
MakerBoardTestSupport.assertRow(panel, 3, 5);
for (int i = 0; i < panel.getControlPointCount(); i++) {
Point2D pad = panel.getControlPoint(i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX() - padHalf, pad.getY() - padHalf, padHalf * 2, padHalf * 2));
}
}
/**
* Both ports stand in the gap between the first and second rows of pixels rather than on a bare
* strip, because this board has no bare strip -- the pixel grid reaches within half a pitch of
* every edge. A port drawn a row out would land under the packages.
*/
@Test
public void portsLieInTheGapBetweenTwoPixelRows() {
WS2812BPanel panel = new WS2812BPanel();
double firstRow = ledCentre(panel, 0).getY();
double secondRow = ledCentre(panel, ORDER).getY();
Assert.assertEquals("input port", (firstRow + secondRow) / 2.0,
panel.getControlPoint(0).getY(), 0.01d);
}
/**
* The holes are drilled in the gaps of the pixel grid, which on the 8x8 leaves 3.89 mm of bare
* board for a 2.8 mm hole. There is no room to be wrong by much, and a hole over a package is
* only visible in a render.
*/
@Test
public void mountingHolesClearThePixelsAndStayOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
double holeR = WS2812BPanel.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
double packageHalf = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
// the holes are private to the part, so they are reached the way the drawing reaches them
for (int i = 0; i < ORDER * ORDER; i++) {
Point2D led = ledCentre(panel, i);
Rectangle2D pack = new Rectangle2D.Double(led.getX() - packageHalf,
led.getY() - packageHalf, packageHalf * 2, packageHalf * 2);
for (double[] hole : HOLES_MM) {
double hx = board.getCenterX() + px(hole[0]);
double hy = board.getCenterY() + px(hole[1]);
Assert.assertTrue("hole breaches the board edge",
board.contains(hx - holeR, hy - holeR, holeR * 2, holeR * 2));
Assert.assertFalse("hole sits under a package",
pack.intersects(hx - holeR, hy - holeR, holeR * 2, holeR * 2));
}
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BPanel());
}
@Test
public void bomValueNamesTheMatrix() {
Assert.assertEquals("8x8, 64 LEDs", new WS2812BPanel().getValueForDisplay());
}
}
@@ -0,0 +1,197 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the NeoPixel stick. Everything on this board competes for 10.22 mm of height:
* the mounting holes take the top of it, the packages take what is left, and the pads run the
* whole of it at each end. None of the three can be moved without crowding another, and nothing
* but a render shows it, so the clearances are asserted here.
*/
public class WS2812BStickTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static Rectangle2D ledPackage(WS2812BStick stick, int index) {
Point2D led = stick.getLedCentre(index);
double half = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, half * 2, half * 2);
}
private static Rectangle2D padRect(WS2812BStick stick, int index) {
Point2D pad = stick.getControlPoint(index);
double w = WS2812BStick.PAD_WIDTH.convertToPixels();
double h = WS2812BStick.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
private static Rectangle2D holeRect(WS2812BStick stick, boolean right) {
Point2D hole = stick.getHoleCentre(right);
double r = WS2812BStick.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(hole.getX() - r, hole.getY() - r, r * 2, r * 2);
}
@Test
public void boardIsTheMeasuredSize() {
MakerBoardTestSupport.assertBoardSize(new WS2812BStick(), WS2812BStick.BOARD_WIDTH,
WS2812BStick.BOARD_HEIGHT);
}
/**
* Four pads at each end, and no two sharing a name: both grounds on an end are the same net on
* the board, which is what made this easy to get wrong.
*/
@Test
public void eachEndCarriesFourDistinctlyNamedPads() {
WS2812BStick stick = new WS2812BStick();
Assert.assertEquals(8, stick.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < stick.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + stick.getControlPointNodeName(i),
names.add(stick.getControlPointNodeName(i)));
}
Assert.assertEquals("DIN", stick.getControlPointNodeName(1));
Assert.assertEquals("DOUT", stick.getControlPointNodeName(5));
}
/**
* The pads are surface copper flush with the cut ends rather than plated holes set in from
* them, so each column's outer edge lands exactly on the board edge: half a pad out and it
* overhangs, half a pad in and there is a bare strip the real board does not have.
*/
@Test
public void padsAreFlushWithTheCutEnds() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
for (int i = 0; i < 4; i++) {
Assert.assertEquals("input pad " + i, board.getX(), padRect(stick, i).getX(), 0.01d);
Assert.assertEquals("output pad " + i, board.getMaxX(), padRect(stick, 4 + i).getMaxX(),
0.01d);
}
for (int i = 0; i < stick.getControlPointCount(); i++) {
Rectangle2D pad = padRect(stick, i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX(), pad.getY(), pad.getWidth(), pad.getHeight()));
}
}
/** A 1.5 mm pad on a 0.1 inch pitch leaves about a millimetre; any taller and they merge. */
@Test
public void padsInAColumnDoNotTouch() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 3; i++) {
Assert.assertFalse("input pads " + i + " and " + (i + 1) + " overlap",
padRect(stick, i).intersects(padRect(stick, i + 1)));
Assert.assertFalse("output pads " + i + " and " + (i + 1) + " overlap",
padRect(stick, 4 + i).intersects(padRect(stick, 5 + i)));
}
}
@Test
public void mountingHolesAreASpanApartAstrideTheCentre() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
Point2D left = stick.getHoleCentre(false);
Point2D right = stick.getHoleCentre(true);
Assert.assertEquals("span", WS2812BStick.MOUNTING_HOLE_SPACING.convertToPixels(),
right.getX() - left.getX(), 0.01d);
Assert.assertEquals("not centred", board.getCenterX(), (left.getX() + right.getX()) / 2.0,
0.01d);
Assert.assertEquals("depth from the top edge",
WS2812BStick.MOUNTING_HOLE_TOP_OFFSET.convertToPixels(), left.getY() - board.getY(),
0.01d);
}
/**
* The holes and the packages are the pair that cannot both have the middle of the board. The
* packages were moved down to make room, and this is what says they went far enough.
*/
@Test
public void packagesClearTheMountingHoles() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
Assert.assertFalse("package " + i + " sits over the left hole",
pack.intersects(holeRect(stick, false)));
Assert.assertFalse("package " + i + " sits over the right hole",
pack.intersects(holeRect(stick, true)));
}
}
@Test
public void packagesAndHolesStayOnTheBoard() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
for (boolean right : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(stick, right);
Assert.assertTrue("a mounting hole breaches the board edge",
board.contains(hole.getX(), hole.getY(), hole.getWidth(), hole.getHeight()));
}
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
Assert.assertTrue("package " + i + " hangs off the board",
board.contains(pack.getX(), pack.getY(), pack.getWidth(), pack.getHeight()));
}
}
/** The pads are drawn after the packages, so an overlap would be hidden under copper. */
@Test
public void packagesClearThePads() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
for (int pad = 0; pad < stick.getControlPointCount(); pad++) {
Assert.assertFalse("package " + i + " runs under pad " + pad,
pack.intersects(padRect(stick, pad)));
}
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BStick());
}
}
@@ -23,8 +23,11 @@ package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.WS2812BStrip.Density;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@@ -55,7 +58,21 @@ public class WS2812BStripTest {
}
private static double padHalfWidth() {
return new Size(0.11d, SizeUnit.in).convertToPixels() / 2.0d;
return WS2812BStrip.PAD_WIDTH.convertToPixels() / 2.0d;
}
/** Where a pad's copper lies, which is what has to stay on the tape and off the packages. */
private static Rectangle2D padRect(WS2812BStrip strip, int index) {
Point2D pad = strip.getControlPoint(index);
double w = WS2812BStrip.PAD_WIDTH.convertToPixels();
double h = WS2812BStrip.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
/** How far in from a cut the first package's centre sits, as the component derives it. */
private static double leadIn() {
return WS2812BStrip.PAD_WIDTH.convertToPixels()
+ WS2812BStrip.PAD_CLEARANCE.convertToPixels() + ledHalfWidth();
}
private static double ledHalfWidth() {
@@ -92,20 +109,67 @@ public class WS2812BStripTest {
Assert.assertEquals("eight more LEDs adds eight pitches", 8 * pitch, sixteen - eight, 0.5d);
}
/** Four pads whatever the count: the LEDs are drawn, not wired. */
/** Six pads whatever the count: the LEDs are drawn, not wired. */
@Test
public void controlPointCountIsFixedAtFour() {
public void controlPointCountIsFixedAtSix() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 60, 144}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Assert.assertEquals("control points at " + density + " x " + count, 4,
Assert.assertEquals("control points at " + density + " x " + count, 6,
strip.getControlPointCount());
}
}
}
/**
* Both cuts carry all three lines, which is the whole point of tape: a length of it is chained
* to the next by soldering its output end to the next one's input. An end short of its supply
* pads cannot be chained at all, and nothing in the drawing says so.
*/
@Test
public void bothCutsCarryAllThreeLines() {
WS2812BStrip strip = new WS2812BStrip();
Set<String> names = new HashSet<String>();
for (int i = 0; i < strip.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + strip.getControlPointNodeName(i),
names.add(strip.getControlPointNodeName(i)));
}
Assert.assertEquals("DIN", strip.getControlPointNodeName(1));
Assert.assertEquals("DOUT", strip.getControlPointNodeName(4));
// the rails are continuous copper down the tape, so each sits in the same row at both ends
for (int i = 0; i < 3; i++) {
Assert.assertEquals("row " + i + " is not level across the tape",
strip.getControlPoint(i).getY(), strip.getControlPoint(3 + i).getY(), 0.01d);
}
}
/**
* Tape is cut through the middle of a pad pair, so what is left at a cut is half a pad on the
* very edge rather than a pad set in from it.
*/
@Test
public void padsAreFlushWithTheCuts() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 40}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Assert.assertEquals(density + " x " + count + " input pad " + i, tape.getX(),
padRect(strip, i).getX(), 0.01d);
Assert.assertEquals(density + " x " + count + " output pad " + i, tape.getMaxX(),
padRect(strip, 3 + i).getMaxX(), 0.01d);
}
}
}
}
@Test
public void ledCountIsClamped() {
WS2812BStrip strip = new WS2812BStrip();
@@ -127,16 +191,14 @@ public class WS2812BStripTest {
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
for (int i = 0; i < strip.getControlPointCount(); i++) {
Point2D pad = strip.getControlPoint(i);
Rectangle2D pad = padRect(strip, i);
String where = density + " x " + count + " pad " + i;
Assert.assertTrue(where + " runs off the left cut",
pad.getX() - padHalfWidth() >= tape.getX() - 0.5d);
Assert.assertTrue(where + " runs off the left cut", pad.getX() >= tape.getX() - 0.5d);
Assert.assertTrue(where + " runs off the right cut",
pad.getX() + padHalfWidth() <= tape.getMaxX() + 0.5d);
Assert.assertTrue(where + " runs off the top edge",
pad.getY() >= tape.getY() - 0.5d);
pad.getMaxX() <= tape.getMaxX() + 0.5d);
Assert.assertTrue(where + " runs off the top edge", pad.getY() >= tape.getY() - 0.5d);
Assert.assertTrue(where + " runs off the bottom edge",
pad.getY() <= tape.getMaxY() + 0.5d);
pad.getMaxY() <= tape.getMaxY() + 0.5d);
}
}
}
@@ -155,11 +217,11 @@ public class WS2812BStripTest {
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
double pitch = new Size(density.getPitchMm(), SizeUnit.mm).convertToPixels();
double leadIn = mmToPx(2.5d) + padHalfWidth() + ledHalfWidth() + mmToPx(0.5d);
double lastLed = tape.getX() + leadIn + (count - 1) * pitch;
double padLeadingEdge = strip.getControlPoint(3).getX() - padHalfWidth();
Assert.assertTrue(density + " x " + count + ": output pad overlaps the last LED",
padLeadingEdge >= lastLed + ledHalfWidth() - 0.5d);
double lastLed = tape.getX() + leadIn() + (count - 1) * pitch;
for (int i = 0; i < 3; i++) {
Assert.assertTrue(density + " x " + count + ": output pad " + i + " overlaps the last LED",
padRect(strip, 3 + i).getX() >= lastLed + ledHalfWidth() - 0.5d);
}
}
}
}
@@ -172,12 +234,10 @@ public class WS2812BStripTest {
strip.setDensity(density);
strip.setLedCount(8);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
double leadIn = mmToPx(2.5d) + padHalfWidth() + ledHalfWidth() + mmToPx(0.5d);
double firstLed = tape.getX() + leadIn;
double firstLed = tape.getX() + leadIn();
for (int i = 0; i < 3; i++) {
double padTrailingEdge = strip.getControlPoint(i).getX() + padHalfWidth();
Assert.assertTrue(density + ": input pad " + i + " overlaps the first LED",
padTrailingEdge <= firstLed - ledHalfWidth() + 0.5d);
padRect(strip, i).getMaxX() <= firstLed - ledHalfWidth() + 0.5d);
}
}
}
@@ -83,6 +83,7 @@ public class MakerComponentsTest {
WS2812BRing.class,
WS2812BStrip.class,
WS2812BJewel.class,
WS2812BPanel.class,
Nokia5110LCD.class,
// Electro-Mechanical
+113 -7
View File
@@ -70,8 +70,9 @@ happened to come back true, and only after a fix.
| `LEDBarGraph` | 8 / 10 / 12 segment | **New in this slice (§6.6), not a draft**, and the only class here that is not an `AbstractMakerBoard` — it is a DIP-outline part, so it follows `DIL_IC` on `AbstractLabeledComponent` with a sibling transformer. The package follows from the segment count rather than being fixed: anode and cathode per segment gives 16 / 20 / 24 pins, and the body lengthens one pin pitch per segment (20.32 / 25.4 / 30.48 mm) at a constant 10.3 mm width, with rows 7.62 mm apart. Unlike a DIP IC the body spans the rows instead of sitting between them, because the pins leave the underside rather than the sides, so the plastic covers them entirely — they are drawn first and painted over, never skipped, or they would drop out of the conductive areas. Each segment is centred on the pin pair that drives it, taken from the two control points rather than by dividing the body, which is also what makes it orientation-proof. Colour is a rule rather than a property — one red at the top, two yellow, green for the rest — so the green band grows with the count; the cost is that a single-colour bar can no longer be drawn. Pins are round and drawn over the package: hidden on the real part, but a pin that cannot be seen cannot be positioned against a board. Covered by `LEDBarGraphTest`, whose `bodyCoversEveryPin` replaces an earlier assertion that encoded the copied DIP arrangement and therefore passed while the component was wrong, and whose `segmentsAreCentredOnTheirPins` catches a placement that was inside the body and still off its pins. |
| `WS2812BStrip` | 30 / 60 / 144 LED/m, 1-144 LEDs | **New in this slice (§6.1), not a draft** — so unlike its neighbours this row describes a part rather than a rehabilitation. Everything follows from the density, which is how tape is sold: pitch is 1000 mm over the count (33.33 / 16.67 / 6.94 mm), length is linear in the LED count, and four control points hold at every density and count because the LEDs are drawn rather than wired. Two figures are not measured: the tape width (10 mm for the 30 and 60, 12 mm for the 144) is the plan's stated norm, and the lead-in that keeps the end pads clear of the first and last package is derived from the pad and package footprints rather than taken from a real tape. Covered by `WS2812BStripTest`, whose pad-clearance assertions exist because that geometry was got wrong three times before it was measured. |
| `WS2812BJewel` | none (a single part) | **New in this slice (§6.7), not a draft.** A sibling of `WS2812BRing` rather than another `RingSize` constant, per D2: it shares the part family and the pad footprint but not the footprint family. It is a solid 23 mm disc with no inner rim, six LEDs on a 16 mm circle around a seventh in the middle, five pads on a 9 mm circle *inside* the LED ring rather than inset from the rim, and two 3 mm mounting holes 19 mm apart that no ring carries — five variant-specific structures, which is a different part and not a variant. Every figure is maintainer-supplied. The pad arrangement is the part that could silently have been wrong: five pads share six gaps between the ring LEDs, so one gap carries none, and it works out to the gap pointing due left, which is where a mounting hole sits. `WS2812BJewelTest` pins that down, because every count and radius assertion passes whichever gap is left empty. Its packages — and the rings', which this changed too — are drawn turned to follow their own radius the way they are mounted; the centre package has no radius and stays square to the board. |
| `WS2812BPanel` | none (a single part) | **New in this slice (§6.7), not a draft.** The only part in the package whose every figure comes from the manufacturer's own CAD rather than from a listing or a measurement: Adafruit publishes the EagleCAD board file for the 8x8 NeoMatrix, which gives the 71.12 mm square, the 8.89 mm pitch, the twelve 2.8 mm holes and the two three-pin ports to the micron. The ports are each other turned half a turn about the centre, which is what lets panels be chained, and both sit in the gap between the first and second rows of pixels because the grid reaches within half a pitch of every edge and there is no bare strip for them. The chain is a progressive raster rather than the serpentine most cheap panels use, which matters here only because the LEDs are drawn lit off the shared gradient and the sweep follows the chain. Silk is left off as on the other addressable parts, and for the same reason the Stick's is: the board file puts the port names on layer 22, the back of the board. Carries no variant property: see §6.7 for why the `Grid` enum it was built with came out again. |
| `Nokia5110LCD` | none (a single part) | **New in this slice (§6.5), not a draft.** Every figure is maintainer-supplied and the whole outline is measured: a 43.8 x 45.8 mm board, a 40 x 35 mm bezel 4.5 mm down from the top edge, a 37 x 27 mm glass 11 mm down, and 3 mm mounting holes on a 34.5 x 41 mm pattern. It is the first display board whose header is on the **bottom** edge, so the board grows upwards from control point zero rather than down from it, and every feature is measured from an edge no control point touches. The glass is placed from the top edge rather than centred in the bezel, because it is not centred in it -- 6.5 mm of frame above, 1.5 mm below. It is also the first in the package to carry silkscreen: the pin names lie flat along the row under the header rather than standing on end like the Arduino boards', and the part name sits in the top left corner beside the mounting hole, where the module prints it (§6.5). One figure is unsourced: the 3.5 mm clearance from the bottom edge to the pin row. |
| `WS2812BStick` | none | Hardcoded to 8 LEDs, with every LED dimension in raw pixels (28 × 28 package, 68 px insets), so the LEDs do not scale with the board. See the node-name bug below. All since corrected: a `Size`-based 51.1 × 10.22 mm board, real 5 mm 5050 packages off the shared `RGB_LED_SIZE`, pads reordered and renamed for uniqueness, and the LED row respread between the pad columns and drawn lit from the shared colour wheel (§11.6). |
| `WS2812BStick` | none | Hardcoded to 8 LEDs, with every LED dimension in raw pixels (28 × 28 package, 68 px insets), so the LEDs do not scale with the board. See the node-name bug below. All since corrected: a `Size`-based 51.1 × 10.22 mm board, real 5 mm 5050 packages off the shared `RGB_LED_SIZE`, pads reordered and renamed for uniqueness, and the LED row respread between the pad columns and drawn lit from the shared colour wheel (§11.6). Its footprint was then corrected a second time against maintainer-supplied figures, and the three corrections go together: two 2 mm mounting holes 1 inch apart astride the centre and 2 mm down from the top edge; surface pads of 3 x 1.5 mm flush with each cut end rather than plated holes set in from it; and the LED row moved off the board's own middle to sit centred in what the holes leave below them. On a board 10.22 mm tall those three compete for the same height, which is why `WS2812BStickTest` -- the class had no test until then -- asserts the clearances rather than the dimensions. |
### 3.1 Two defects that are bugs, not style
@@ -232,6 +233,39 @@ such a component requires shifting **every** control point: `setControlPoint(p,
point and does not re-run `updateControlPoints()`, so the body follows point 0 while the remaining
pins stay behind.
### 3.3 One pad, drawn one way
`AbstractMakerBoard` carried two through-hole pad painters that differed in appearance rather than
in purpose. `drawPcbSolderPads` drew a round gold pad and was used by every microcontroller board;
`drawSolderPads` drew a silver rounded rectangle, took three per-part size hooks
(`getSolderPadWidth`, `getSolderPadLength`, `getSolderPadHoleSize`), and was used by every
addressable part and the two drafted modules. The result was that the NeoPixel family's pads looked
like nothing else in the library, and the Stick did not draw pads at all — it drew a fitted pin
header, on a part that ships bare and is wired by soldering to it.
**Resolved: there is now one pad painter.** `drawSolderPads` and its three size hooks are gone, the
six callers moved to `drawPcbSolderPads`, and the per-class `PAD_WIDTH` / `PAD_LENGTH` / `PAD_HOLE`
constants on the Ring, the Jewel and the Panel went with them — the shared `PAD_SIZE` and
`HOLE_SIZE` now decide, which is the rule already settled for board appearance: tune the shared
constants globally rather than adding per-board hooks. The Stick's header became pads at the same
time, which also simplified its LED field, since the field now has to clear a pad radius rather
than the wider block the header painter drew around each pin.
`WS2812BStripTest.outputPadClearsTheLastLed` failed on the change and was right to: the strip's
lead-in is derived from the pad footprint, so a narrower pad shortens the tape, and the test had
the old 0.11" pad written into it as a literal. The test now reads `PAD_SIZE`, so it tracks the
component instead of restating it, and the clearance it checks comes out at exactly the
`PAD_CLEARANCE` the lead-in is built from.
**A second pad shape followed, because not every pad is a plated hole.** The stick's are surface
copper flush with its cut ends, so `drawSurfacePads` was added beside `drawPcbSolderPads`: a plain
rectangle in the same gold, no drill, and -- unlike a round pad -- turned with the board, since a
3 x 1.5 mm pad left axis-aligned on a rotated stick is visibly wrong. Two painters are the right
number here where three were not: they differ in what the part physically has, not in how it is
coloured. The strip followed the stick onto `drawSurfacePads` for the same reason, with the same
3 x 1.5 mm footprint -- taken from the stick rather than measured on tape, and flagged in the
class as the figure to correct first.
## 4. Shared mechanics
The board roadmap's §4 applies unchanged and is not repeated: component discovery is a classpath
@@ -347,6 +381,27 @@ Silk was left off, following the Ring and the Stick. Worth revisiting: unlike th
*does* print `+5V`, `DI` and `GND` between its pads, so this is the one case in the slice where
dropping silkscreen is a simplification rather than a fidelity gain.
**Both cuts now carry all three lines.** The original had `+5V`, `DIN` and `GND` at one end and a
lone `DOUT` at the other, on the reasoning that a strip is chained by its own pads -- which is true
and is exactly why the far end needs its supply pads too. The rails are continuous copper down the
length of the tape, so each sits in the same row at both ends; what changes from one end to the
other is only the sense of the data pad. Six control points, named `+5V_1` / `DIN` / `GND_1` and
`+5V_2` / `DOUT` / `GND_2` so the netlist keeps them apart, and `bothCutsCarryAllThreeLines`
asserts the rows are level across the tape, which is what a butted pair depends on.
The pads also became surface copper flush with each cut rather than plated holes set in from it,
following the stick (§3.3): tape is cut through the middle of a pad pair, so what a cut leaves is
half a pad on the very edge. `PAD_END_INSET` went with the change, since how far a pad centre sits
inboard is now half its width rather than a figure of its own.
One thing butting two lengths together shows that nothing else does: **the LEDs do not keep their
pitch across the joint.** The lead-in is derived from the pad footprint, so it is 6 mm whatever the
density, where a real cut falls halfway between two packages and would make it half a pitch --
8.33 mm at 60/m. Deriving it is deliberate and recorded above, because at 144/m half a pitch is
3.47 mm and a 3 mm pad would sit under the first package. The honest reading is that the 3 mm pad
is a 60/m figure being used at every density; a per-density pad would fix both at once, and wants
a measurement rather than a derivation.
The plan this was built from, kept for the record:
The most-used addressable form factor, absent entirely, and the one addition that also serves the
@@ -837,8 +892,58 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
ring sizes as well as the Jewel; the Jewel's centre package has no radius and stays square, and the
linear strip and stick are unaffected. The package is square, so the turn only shows modulo a
quarter turn, and no geometry assertion catches it -- it is visible only in a render.
- **NeoPixel panel 8x8** as its own class, reusing `drawAddressableLed` and the ring's pad handling.
Worth doing only after §6.1, whose helper it depends on. **~0.5 day.**
- **NeoPixel panel 8x8** as its own class — **done**, as `WS2812BPanel`, and it cost closer to the
estimate than the Jewel did because the geometry did not have to be argued over: Adafruit
publishes the EagleCAD board file for the 8x8 NeoMatrix, so every figure was read off the part
rather than sourced from a listing or derived.
That is worth recording as a method, not just a result. The product page gives only
`71.17 x 71.17 x 3.28 mm` and says there are "two 3-pin connection ports"; no vendor publishes the
pitch, the hole pattern, the port positions or the chain order, and all four are things this
drawing needs. The board file gives them exactly:
| Figure | From the board file |
|---|---|
| Board | 71.12 x 71.12 mm (the listing's 71.17 is a rounding of 2.8") |
| Pitch | 8.89 mm (0.35") |
| Pixel grid | centred, a half pitch of margin outside the outer pixels |
| Ports | 3 through-holes at 0.1" pitch, 1.905 mm in from a side edge, 8.89 mm down from an end |
| Port order | DIN, +5V, GND outward-in; the output port is the input turned half a turn about the centre |
| Holes | twelve, 2.8 mm, six in from the side edges and six in the gaps between pixel columns |
| Chain | progressive raster, not serpentine — LED1-8 left to right along the top row, LED9 back at the left |
Three relationships hold on this board and are recorded in the `Grid` enum's Javadoc as
measurements rather than derived in code, because none of them is guaranteed on someone else's
panel: the board is exactly the matrix order times the pitch, the grid is centred, and the port
row lands in the gap between the first and second rows of pixels.
`theBoardIsAWholeNumberOfPitchesAcross` asserts the first, so a second panel that does not share
it fails rather than being quietly drawn wrong.
The chain order is the one figure that changes the drawing without changing any dimension. The
LEDs are drawn lit off the shared gradient, so the colour runs along the data chain; wired as the
serpentine most cheap panels use, the sweep would fold back on itself every row. `WS2812BPanelTest`
pins the raster down, along with the rotational symmetry of the two ports and the 2.8 mm holes
sitting in 3.89 mm gaps — the three things that pass every dimension check and still render wrong.
Silk was left off, following the Ring, Stick, Strip and Jewel, and this time with proof rather
than an assumption: the port names `DIN`, `5V`, `GND`, `DOUT` are on layer 22, the **back** of the
board, exactly as the Stick's are. What the front does carry is `Adafruit NeoPixel 8X8`,
`64 RGB LEDs`, `24 bit Color`, `Only ONE Pin` and `bLiNkY bLiNkY bLiNkY bLiNkY bLiNkY`, in 1.8-2.3
mm letters laid in the gaps between pixel rows. That is real silkscreen in the place the
manufacturer prints it, so the rule would allow it; it is left off pending a decision, because
printing a vendor's marketing text across a part is a different question from printing its pin
names.
**The `Grid` enum was built and then removed.** It had one member and no prospect of a second:
Adafruit makes no rigid 4x4 or 16x16 NeoMatrix — the 1487 is the only rigid panel in the line,
and the flexible 16x16 is a different part with a different mounting — while the generic WS2812B
panels sold as 4x4 and 16x16 have no published board file and listings that disagree with each
other on the outline (that family's 8x8 is variously 65 and 80 mm). A variant property with one
value is a control the user cannot use, so the figures are plain constants on the class and the
panel is a single part like the Jewel and the Nokia. D2 is unchanged by this: it says a new
variant goes in an enum rather than a sibling class, not that a single part needs an enum to
hold its own dimensions. Should a second panel ever be sourced, re-introducing the enum is the
same work either way.
### 6.8 On-screen identification — the glass prints its own spec — **done**
@@ -1021,13 +1126,14 @@ part name, then the variant list).
6. **§7** tests alongside each of the above, not after.
7. **§8** regression samples and the `update.xml` block, last.
Eleven palette entries so far: the seven existing, plus the strip, the bar graph, the Jewel and the
Nokia, plus the panel if appetite holds. The `update.xml` 6.7.0 block now carries all eleven,
Twelve palette entries: the seven existing, plus the strip, the bar graph, the Jewel, the Nokia and
now the panel. The `update.xml` 6.7.0 block carries all twelve,
alphabetically as the 6.5.0 block lists its boards. Two of the entries it already had were stale
rather than missing, and both were stale because a class had outgrown them: the matrix was still
described as an 8x8 and the TFT as a touch screen with an SD slot, which is one variant of four
and a part that was removed from the drawing. A twelfth entry is needed only if the panel is
built.
and a part that was removed from the drawing. The §6.8 screen readout gets no entry of its own:
it is how four components that are new in this very block look, not an improvement over
anything a user has seen.
## 10. Later slices, and why they are later