This commit is contained in:
Bane Stojkovic
2026-10-05 13:33:44 +02:00
parent 2f59f35f46
commit 25331632c1
10 changed files with 1002 additions and 22 deletions
@@ -521,6 +521,10 @@
<changeType>NEW_FEATURE</changeType>
<description>LED Matrix (MAX7219) - single 8x8, compact 8x8, 4-in-1 32x8</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Breakout (WS2812B) - single LED</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Jewel (WS2812B) - 7 LEDs</description>
@@ -619,14 +619,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
drawingObserver.stopTrackingContinuityArea();
}
/**
* Helper to draw rectangular tinned solder pads with drill holes at given control points.
*/
/**
* Footprint of a solder pad. The defaults are the sizes this method has always drawn, expressed
* 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.
*/
/**
* 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.
@@ -0,0 +1,240 @@
/*
*
* 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.Point2D;
import java.awt.geom.Rectangle2D;
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;
@ComponentDescriptor(name = "NeoPixel Breakout", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Single-LED Addressable RGB WS2812B NeoPixel Breakout",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BBreakout extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
// The board lies on its side: a three-pad column up each of the two short edges, and the two
// mounting holes on the centre line between them, one towards the top edge and one the bottom.
public static Size BOARD_WIDTH = new Size(0.5d, SizeUnit.in);
public static Size BOARD_HEIGHT = new Size(0.4d, SizeUnit.in);
// The real board is wired through a three-way connector at each end. Those are drawn as plain
// pads instead: at this size a connector body would cover most of the board and hide the LED,
// and what a layout needs from it is where the three wires land.
//
// As on the stick the pads are flush with their own edge rather than inset from it, so how far
// in their centres sit is half their reach and not a figure of its own.
public static Size PAD_WIDTH = new Size(1.5d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(0.3d, SizeUnit.in);
// Drawn lit off the same colour wheel as the rest of the family, so a breakout on a layout reads
// as the same part as one pixel of a stick.
public static Color LED_COLOR = buildLedGradient(1)[0];
// Each edge carries ground, power and one data line. The two grounds are the same net on the
// board, as are the two supplies, and are numbered only to keep the node names distinct.
private static final String[] PIN_NAMES = {
"GND_1", "VIN_1", "IN",
"GND_2", "VIN_2", "OUT"
};
public WS2812BBreakout() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// the two pad columns stand flush against their own edges, 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 < 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);
}
/**
* 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_WIDTH.convertToPixels() / 2.0;
}
/** Top edge of the board, derived so that the three-pad column is centred across its height. */
private double getBoardY(double y) {
double padSpan = 2 * PIN_SPACING.convertToPixels();
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
/** The one package, in the middle of the board with the holes and pads placed around it. */
Point2D getLedCentre() {
Point2D p0 = controlPoints[0];
return new Point2D.Double(getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0,
getBoardY(p0.getY()) + BOARD_HEIGHT.convertToPixels() / 2.0);
}
/**
* One of the two mounting holes. They sit on the centre line between the pad columns, a fixed
* span apart astride the LED, and they are what the board is tightest against: 0.27 mm of board
* outside each rim and 0.31 mm between the rim and the package.
*/
Point2D getHoleCentre(boolean bottom) {
Point2D centre = getLedCentre();
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(centre.getX(), centre.getY() + (bottom ? offset : -offset));
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 4, 4);
}
@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);
}
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 holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean bottom : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(bottom);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
// The pad columns clear the package by 2.35 mm, but that room is between a pad and the
// package rather than beside a pad, so there is nowhere a pin name would go: this face
// carries no silkscreen, as the stick's does not.
Point2D led = getLedCentre();
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(),
RGB_LED_SIZE.convertToPixels(), LED_COLOR);
}
g2d.setTransform(oldTx);
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// landscape in the same 5:4 as the board, so the icon reads as the part and not as a sibling
// of the portrait modules further down the category
double rectW = width - 4;
double rectH = rectW * 0.8;
double rectX = 2;
double rectY = (height - rectH) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
double padW = 3;
double padH = 2;
double padStep = 5;
g2d.setColor(PAD_COLOR);
for (int i = 0; i < 3; i++) {
double padY = height / 2.0 - padStep + i * padStep - padH / 2.0;
g2d.fill(new Rectangle2D.Double(rectX, padY, padW, padH));
g2d.fill(new Rectangle2D.Double(rectX + rectW - padW, padY, padW, padH));
}
double ledSize = Math.min(rectW - 6, 12);
MakerBoardPainter.drawAddressableLed(g2d, width / 2.0, height / 2.0, ledSize,
Color.decode("#FF3333"));
}
}
@@ -0,0 +1,147 @@
/*
*
* 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.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.displays.LEDMatrix.Modules;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the MAX7219 matrix. The three variants differ in outline and in which edge
* their headers leave by, so each one places the same ten pins differently.
*/
public class LEDMatrixTest {
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 mm(double px) {
return px / PX_PER_MM;
}
private static LEDMatrix of(Modules modules) {
LEDMatrix matrix = new LEDMatrix();
matrix.setModules(modules);
return matrix;
}
@Test
public void boardSizeFollowsTheModules() {
for (Modules modules : Modules.values()) {
Rectangle2D bounds = of(modules).getBodyShape().getBounds2D();
Assert.assertEquals(modules + " width", modules.getBoardWidthMm(), mm(bounds.getWidth()),
0.01d);
Assert.assertEquals(modules + " length", modules.getBoardHeightMm(), mm(bounds.getHeight()),
0.01d);
}
}
/** The modules are flush with each other and with the board, so the outline has to bound them. */
@Test
public void theModulesFitAcrossTheBoard() {
for (Modules modules : Modules.values()) {
double matrixSize = mm(LEDMatrix.MATRIX_SIZE.convertToPixels());
Assert.assertTrue(modules + " is narrower than its modules",
modules.getBoardWidthMm() >= modules.getCount() * matrixSize);
}
}
@Test
public void controlPointsAreTheTwoHeadersInOrder() {
String[] expected = new String[] {"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN", "VCC_OUT",
"GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"};
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Assert.assertEquals(modules.toString(), expected.length, matrix.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals(modules + " pin " + i, expected[i], matrix.getControlPointNodeName(i));
Assert.assertTrue(modules + " duplicate node name " + expected[i], names.add(expected[i]));
}
}
}
/**
* Both headers are five-pin rows on 0.1" pitch wherever they leave the board. The tall single
* runs them across the width; the two tileable variants stand them on end at the short edges.
*/
@Test
public void eachHeaderIsAFivePinRow() {
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
MakerBoardTestSupport.assertRow(matrix, 0, 4);
MakerBoardTestSupport.assertRow(matrix, 5, 9);
}
}
@Test
public void everyPinSitsOnTheBoard() {
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Rectangle2D board = matrix.getBodyShape().getBounds2D();
for (int i = 0; i < matrix.getControlPointCount(); i++) {
Assert.assertTrue(modules + " pin " + i + " is off the board",
board.contains(matrix.getControlPoint(i)));
}
}
}
@Test
public void modulesRoundTripThroughTheSetter() {
Assert.assertEquals(Modules.Single_8x8, new LEDMatrix().getModules());
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Assert.assertEquals(modules, matrix.getModules());
Assert.assertEquals(modules.toString(), matrix.getValueForDisplay());
}
}
/** A project saved before the variant property existed deserialises with no modules set. */
@Test
public void aMissingModulesValueFallsBackToTheSingle() {
LEDMatrix matrix = of(null);
Assert.assertEquals(Modules.Single_8x8, matrix.getModules());
}
@Test
public void drawsCleanlyInEveryState() {
for (Modules modules : Modules.values()) {
MakerBoardTestSupport.assertDrawsCleanly(of(modules));
}
}
}
@@ -167,4 +167,9 @@ public class Nokia5110LCDTest {
Assert.assertEquals(Display.VALUE, printed.getScreen());
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new Nokia5110LCD());
}
}
@@ -0,0 +1,201 @@
/*
*
* 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.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.displays.SevenSegmentDisplay.Common;
import org.diylc.components.displays.SevenSegmentDisplay.DisplayType;
import org.diylc.components.displays.SevenSegmentDisplay.Punctuation;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the seven-segment displays. Three of the four variants are DIP packages that
* straddle their pins with a row either side, and the fourth is a module carrying a single header
* down one edge, so the pin layout is what differs most between them.
*/
public class SevenSegmentDisplayTest {
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 mm(double px) {
return px / PX_PER_MM;
}
private static SevenSegmentDisplay of(DisplayType displayType) {
SevenSegmentDisplay display = new SevenSegmentDisplay();
display.setDisplayType(displayType);
return display;
}
@Test
public void bodySizeFollowsThePackage() {
for (DisplayType displayType : DisplayType.values()) {
Rectangle2D bounds = of(displayType).getBodyShape().getBounds2D();
Assert.assertEquals(displayType + " width", displayType.getBodyWidthMm(),
mm(bounds.getWidth()), 0.01d);
Assert.assertEquals(displayType + " length", displayType.getBodyLengthMm(),
mm(bounds.getHeight()), 0.01d);
}
}
@Test
public void pinCountFollowsThePackage() {
Assert.assertEquals(10, of(DisplayType.SingleDigit_10Pin).getControlPointCount());
Assert.assertEquals(12, of(DisplayType.FourDigit_0_36_12Pin).getControlPointCount());
Assert.assertEquals(12, of(DisplayType.FourDigit_0_56_12Pin).getControlPointCount());
Assert.assertEquals(4, of(DisplayType.TM1637_Module_4Pin).getControlPointCount());
}
/**
* Pin arrays are in DIP order: pin 1 bottom left, along the bottom row, then back along the top.
*/
@Test
public void pinNamesAreInDipOrder() {
Assert.assertArrayEquals(new String[] {"E", "D", "COM1", "C", "DP", "B", "A", "COM2", "F", "G"},
SevenSegmentDisplay.PIN_NAMES_1DIGIT);
Assert.assertArrayEquals(
new String[] {"E", "D", "DP", "C", "G", "D4", "B", "D3", "D2", "F", "A", "D1"},
SevenSegmentDisplay.PIN_NAMES_4DIGIT);
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
Set<String> names = new HashSet<String>();
for (int i = 0; i < display.getControlPointCount(); i++) {
String name = display.getControlPointNodeName(i);
Assert.assertFalse(displayType + " pin " + i + " is unnamed", name.isEmpty());
Assert.assertTrue(displayType + " duplicate node name " + name, names.add(name));
}
}
}
/** The 3641AS and the 5641AS differ in size but not in pin function, so one array serves both. */
@Test
public void bothFourDigitPackagesSharePinNames() {
SevenSegmentDisplay small = of(DisplayType.FourDigit_0_36_12Pin);
SevenSegmentDisplay large = of(DisplayType.FourDigit_0_56_12Pin);
for (int i = 0; i < small.getControlPointCount(); i++) {
Assert.assertEquals("pin " + i, small.getControlPointNodeName(i),
large.getControlPointNodeName(i));
}
Assert.assertNotEquals(small.getBodyShape().getBounds2D(), large.getBodyShape().getBounds2D());
}
@Test
public void dipPackagesCarryTwoRowsAndTheModuleOne() {
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
int pinCount = display.getControlPointCount();
if (displayType.isDualRow()) {
int perRow = pinCount / 2;
MakerBoardTestSupport.assertRow(display, 0, perRow - 1);
MakerBoardTestSupport.assertRow(display, perRow, pinCount - 1);
Assert.assertEquals(displayType + " row spacing", displayType.getRowSpacingMm(),
mm(Math.abs(display.getControlPoint(0).getY()
- display.getControlPoint(pinCount - 1).getY())),
0.01d);
} else {
MakerBoardTestSupport.assertRow(display, 0, pinCount - 1);
}
}
}
/** A package only works if its pin rows sit within its body. */
@Test
public void everyPinSitsOnTheBody() {
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
Rectangle2D body = display.getBodyShape().getBounds2D();
for (int i = 0; i < display.getControlPointCount(); i++) {
Assert.assertTrue(displayType + " pin " + i + " is off the body",
body.contains(display.getControlPoint(i)));
}
}
}
@Test
public void propertiesRoundTripThroughTheirSetters() {
SevenSegmentDisplay display = new SevenSegmentDisplay();
Assert.assertEquals(DisplayType.SingleDigit_10Pin, display.getDisplayType());
Assert.assertEquals(Common.Cathode, display.getCommon());
Assert.assertEquals(Punctuation.DecimalPoints, display.getPunctuation());
for (DisplayType displayType : DisplayType.values()) {
Assert.assertEquals(displayType, of(displayType).getDisplayType());
}
for (Common common : Common.values()) {
display.setCommon(common);
Assert.assertEquals(common, display.getCommon());
}
for (Punctuation punctuation : Punctuation.values()) {
display.setPunctuation(punctuation);
Assert.assertEquals(punctuation, display.getPunctuation());
}
}
/**
* The module drives the digits itself and brings out no common pins, so polarity is not part of
* what you order and is left out of its BOM value; the bare packages carry it.
*/
@Test
public void bomValueNamesThePackageAndWhatVariesWithIt() {
SevenSegmentDisplay module = of(DisplayType.TM1637_Module_4Pin);
module.setCommon(Common.Anode);
module.setPunctuation(Punctuation.Colon);
Assert.assertEquals(DisplayType.TM1637_Module_4Pin + ", " + Punctuation.Colon,
module.getValueForDisplay());
SevenSegmentDisplay bare = of(DisplayType.SingleDigit_10Pin);
bare.setCommon(Common.Anode);
bare.setPunctuation(Punctuation.None);
Assert.assertEquals(
DisplayType.SingleDigit_10Pin + ", " + Common.Anode + ", " + Punctuation.None,
bare.getValueForDisplay());
}
@Test
public void drawsCleanlyInEveryState() {
for (DisplayType displayType : DisplayType.values()) {
for (Punctuation punctuation : Punctuation.values()) {
SevenSegmentDisplay display = of(displayType);
display.setPunctuation(punctuation);
MakerBoardTestSupport.assertDrawsCleanly(display);
}
}
}
}
@@ -0,0 +1,218 @@
/*
*
* 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 single-LED breakout. Three things compete for a board half an inch by four
* tenths: a three-pad column up each short edge, the two mounting holes on the centre line between
* them, and the package in the middle. The holes are what everything else is tight against -- 0.3
* inch apart across a 0.4 inch height leaves 0.27 mm of board outside each rim and 0.31 mm between
* a rim and the package -- so what is asserted here is that none of the three has taken another's
* room.
*/
public class WS2812BBreakoutTest {
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 mm(double px) {
return px / PX_PER_MM;
}
private static Rectangle2D padRect(WS2812BBreakout breakout, int index) {
Point2D pad = breakout.getControlPoint(index);
double w = WS2812BBreakout.PAD_WIDTH.convertToPixels();
double h = WS2812BBreakout.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
private static Rectangle2D holeRect(WS2812BBreakout breakout, boolean bottom) {
Point2D hole = breakout.getHoleCentre(bottom);
double r = WS2812BBreakout.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(hole.getX() - r, hole.getY() - r, r * 2, r * 2);
}
private static Rectangle2D ledPackage(WS2812BBreakout breakout) {
Point2D led = breakout.getLedCentre();
double half = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, half * 2, half * 2);
}
@Test
public void boardIsHalfAnInchByFourTenths() {
MakerBoardTestSupport.assertBoardSize(new WS2812BBreakout(), WS2812BBreakout.BOARD_WIDTH,
WS2812BBreakout.BOARD_HEIGHT);
}
/** Ground, power and a data line up each long edge, and no two pads sharing a name. */
@Test
public void eachEdgeCarriesThreeDistinctlyNamedPads() {
WS2812BBreakout breakout = new WS2812BBreakout();
Assert.assertEquals(6, breakout.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + breakout.getControlPointNodeName(i),
names.add(breakout.getControlPointNodeName(i)));
}
Assert.assertEquals("IN", breakout.getControlPointNodeName(2));
Assert.assertEquals("OUT", breakout.getControlPointNodeName(5));
}
/**
* The two grounds are one net on the real board, as are the two supplies, so their names differ
* only by a disambiguator that comes off again wherever a pad is named to the user.
*/
@Test
public void theNumberedPadsReadAsOneName() {
WS2812BBreakout breakout = new WS2812BBreakout();
for (int i : new int[] {0, 3}) {
Assert.assertEquals("GND",
AbstractMakerBoard.getDisplayPinLabel(breakout.getControlPointNodeName(i)));
}
for (int i : new int[] {1, 4}) {
Assert.assertEquals("VIN",
AbstractMakerBoard.getDisplayPinLabel(breakout.getControlPointNodeName(i)));
}
}
@Test
public void padColumnsRunUpTheShortEdgesAtHeaderPitch() {
WS2812BBreakout breakout = new WS2812BBreakout();
MakerBoardTestSupport.assertRow(breakout, 0, 2);
MakerBoardTestSupport.assertRow(breakout, 3, 5);
Rectangle2D board = breakout.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Assert.assertEquals("left pad " + i, board.getX(), padRect(breakout, i).getX(), 0.01d);
Assert.assertEquals("right pad " + i, board.getMaxX(), padRect(breakout, 3 + i).getMaxX(),
0.01d);
}
}
@Test
public void holesSitOnTheCentreLineASpanApartAstrideTheLed() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
Point2D top = breakout.getHoleCentre(false);
Point2D bottom = breakout.getHoleCentre(true);
Assert.assertEquals("span", WS2812BBreakout.MOUNTING_HOLE_SPACING.convertToPixels(),
bottom.getY() - top.getY(), 0.01d);
Assert.assertEquals("not centred across the board", board.getCenterX(), top.getX(), 0.01d);
Assert.assertEquals("not centred along the board", board.getCenterY(),
(top.getY() + bottom.getY()) / 2.0, 0.01d);
}
/**
* The margin the 0.3 inch span leaves outside each rim. Asserted as the figure it works out to
* rather than as mere containment, because a quarter of a millimetre is the whole of it: any
* revision to the board height or the hole spacing eats it entirely.
*/
@Test
public void aQuarterMillimetreOfBoardSurvivesOutsideEachHole() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
Assert.assertEquals("above the top hole", 0.27d,
mm(holeRect(breakout, false).getY() - board.getY()), 0.01d);
Assert.assertEquals("below the bottom hole", 0.27d,
mm(board.getMaxY() - holeRect(breakout, true).getMaxY()), 0.01d);
}
/**
* The package has the middle, and the holes and pads are placed to leave it there. The holes are
* the pair worth watching: 0.31 mm of board separates a rim from the package, so this fails on
* any revision that brings the two together rather than letting them overlap unnoticed.
*/
@Test
public void thePackageClearsTheHolesAndThePads() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D pack = ledPackage(breakout);
for (boolean bottom : new boolean[] {false, true}) {
Assert.assertFalse("the package sits over a mounting hole",
pack.intersects(holeRect(breakout, bottom)));
}
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertFalse("the package runs under pad " + i,
pack.intersects(padRect(breakout, i)));
}
}
/** The pads keep well clear of the holes at these figures; this is what says they still do. */
@Test
public void theHolesClearThePads() {
WS2812BBreakout breakout = new WS2812BBreakout();
for (boolean bottom : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(breakout, bottom);
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertFalse("a mounting hole runs under pad " + i,
hole.intersects(padRect(breakout, i)));
}
}
}
@Test
public void everythingStaysOnTheBoard() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
for (boolean bottom : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(breakout, bottom);
Assert.assertTrue("a mounting hole breaches the board edge",
board.contains(hole.getX(), hole.getY(), hole.getWidth(), hole.getHeight()));
}
Rectangle2D pack = ledPackage(breakout);
Assert.assertTrue("the package hangs off the board",
board.contains(pack.getX(), pack.getY(), pack.getWidth(), pack.getHeight()));
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Rectangle2D pad = padRect(breakout, i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX(), pad.getY(), pad.getWidth(), pad.getHeight()));
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BBreakout());
}
}
@@ -0,0 +1,132 @@
/*
*
* 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.displays.WS2812BRing.RingSize;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the NeoPixel ring. The pad arrangement itself is guarded by {@link RingSize}'s
* constructor, which throws unless each ring's LED gaps sum to its LED count, so what is left to
* assert here is that the three sizes carry the pads and diameters they were measured with and
* that the body is drawn concentric with them.
*/
public class WS2812BRingTest {
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 mm(double px) {
return px / PX_PER_MM;
}
private static WS2812BRing of(RingSize ringSize) {
WS2812BRing ring = new WS2812BRing();
ring.setRingSize(ringSize);
return ring;
}
@Test
public void bodyIsTheOuterDiameterOfEachRing() {
for (RingSize ringSize : RingSize.values()) {
Rectangle2D bounds = of(ringSize).getBodyShape().getBounds2D();
Assert.assertEquals(ringSize + " width", ringSize.getOuterDiameterMm(), mm(bounds.getWidth()),
0.01d);
Assert.assertEquals(ringSize + " height", ringSize.getOuterDiameterMm(),
mm(bounds.getHeight()), 0.01d);
}
}
/** Four pads on the 12-LED ring, six on the two larger ones, and no two sharing a name. */
@Test
public void padCountFollowsTheRingSize() {
Assert.assertEquals(4, of(RingSize._12_LED).getControlPointCount());
Assert.assertEquals(6, of(RingSize._16_LED).getControlPointCount());
Assert.assertEquals(6, of(RingSize._24_LED).getControlPointCount());
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Set<String> names = new HashSet<String>();
for (int i = 0; i < ring.getControlPointCount(); i++) {
String name = ring.getControlPointNodeName(i);
Assert.assertNotNull(ringSize + " pad " + i, name);
Assert.assertFalse(ringSize + " pad " + i + " is unnamed", name.isEmpty());
Assert.assertTrue(ringSize + " duplicate node name " + name, names.add(name));
}
}
}
/**
* Every pad sits one inset in from the rim, on a circle of its own rather than on the one the
* LEDs occupy. This also says the body is drawn concentric with the pads: the outline is placed
* by working backwards from the first pad, so a disagreement there would leave the ring drawn
* away from its own control points and show up as an off-centre radius here.
*/
@Test
public void everyPadSitsOnItsOwnCircleInsideTheRim() {
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Rectangle2D bounds = ring.getBodyShape().getBounds2D();
Point2D centre = new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
double expected = ringSize.getOuterDiameterMm() / 2.0
- mm(WS2812BRing.PAD_EDGE_INSET.convertToPixels());
for (int i = 0; i < ring.getControlPointCount(); i++) {
Assert.assertEquals(ringSize + " pad " + i + " radius", expected,
mm(centre.distance(ring.getControlPoint(i))), 0.01d);
}
}
}
@Test
public void ringSizeRoundTripsThroughTheSetter() {
Assert.assertEquals(RingSize._16_LED, new WS2812BRing().getRingSize());
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Assert.assertEquals(ringSize, ring.getRingSize());
Assert.assertEquals(ringSize.toString(), ring.getValueForDisplay());
}
}
@Test
public void drawsCleanlyInEveryState() {
for (RingSize ringSize : RingSize.values()) {
MakerBoardTestSupport.assertDrawsCleanly(of(ringSize));
}
}
}
@@ -84,6 +84,7 @@ public class MakerComponentsTest {
WS2812BStrip.class,
WS2812BJewel.class,
WS2812BPanel.class,
WS2812BBreakout.class,
Nokia5110LCD.class,
// Electro-Mechanical
+54 -14
View File
@@ -979,6 +979,40 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
hold its own dimensions. Should a second panel ever be sourced, re-introducing the enum is the
same work either way.
- **NeoPixel Breakout** as its own class — **done**, as `WS2812BBreakout`, on the maintainer's
figures: a **0.5 inch wide by 0.4 inch high** board, one 5050 package in the middle, two 2 mm
mounting holes **0.3 inch apart** on the centre line, and three connections at each end —
`GND`, `VIN`, `IN` on one and `GND`, `VIN`, `OUT` on the other. The real board brings those out
through a three-way connector at each end, and they are drawn as plain pads instead, which is the
same call §3.2 recorded for fine-pitch connectors generally: at this size a connector body would
cover most of the board and hide the LED, and what a layout needs from it is where the three
wires land.
So the board lies on its side — a three-pad column up each short edge, the holes spaced across
the height between them. **The figures were corrected once during the work and the first set did
not fit**, which is worth recording because the failure was not a rounding: the board was first
given as 0.5 inch long and 0.4 inch tall with the holes 0.4 inch apart, and 0.4 inch of spacing
does not fit a 0.4 inch height — the two centres land exactly on the edges. Read the other way,
with the spacing along the 0.5 inch axis, the holes then wanted the same place as the pad columns.
The 0.3 inch spacing removes the conflict entirely and puts the part back the way round the brief
described it. The lesson is only that a hole pattern and an outline have to be checked against
each other before either is drawn, since a spacing equal to the dimension it runs along reads
perfectly well in prose.
What is left is tight in two places and comfortable elsewhere. Each hole keeps **0.27 mm of board
outside its rim** and **0.31 mm between its rim and the package** — about 2 px each at 1:1, and
the tightest clearances anywhere in the package, against the ring's 0.5745 mm from pad to rim and
the full millimetre the stick's holes keep from its top edge.
`aQuarterMillimetreOfBoardSurvivesOutsideEachHole` asserts the first as the figure rather than as
mere containment, and `thePackageClearsTheHolesAndThePads` guards the second: any revision to the
height or the spacing consumes both. The pad columns are the comfortable ones, 2.35 mm clear of
the package and 4.88 mm from the nearest hole. The face carries no silkscreen — the room there is
between a pad and the package rather than beside a pad, so a pin name has nowhere to sit, as on
the stick.
Like the Jewel, the Nokia and the panel it is a single part with no variant enum, so its figures
are plain constants on the class.
### 6.8 On-screen identification — the glass prints its own spec — **done**
**Done.** The four displays that have a lit area — `OLEDDisplay`, `TFTDisplay`,
@@ -1158,8 +1192,8 @@ part name, then the variant list).
6. **§7** tests alongside each of the above, not after.
7. **§8** the `update.xml` block, last.
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,
Thirteen palette entries: the seven existing, plus the strip, the bar graph, the Jewel, the Nokia,
the panel and the breakout. The `update.xml` 6.7.0 block carries all thirteen,
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
@@ -1410,19 +1444,25 @@ gate the displays.
`_` so the silkscreen still reads `G`.
This also closes §3.2 item 8. The pads no longer share the LED arc: they sit on their own radius,
`outerR` less a 1.4 mm `PAD_EDGE_INSET`, drawn 1.6 × 1.3 mm with a 0.7 mm hole. A ring pad is far
smaller than a 0.1" header pad, so `AbstractMakerBoard` grew three
`getSolderPad{Width,Length,HoleSize}` hooks. That is a deliberate exception to the usual
preference for tuning the shared appearance constants globally, taken because only the Ring wants
these values: the defaults reproduce the previous hardcoded 22 / 16 / 7 px exactly, confirmed by
byte-comparing every maker render before and after and finding **only `WS2812BRing` changed**.
`outerR` less a 1.4 mm `PAD_EDGE_INSET`. They are **round**, drawn by the shared
`drawPcbSolderPads` off `AbstractMakerBoard`'s own `PAD_SIZE` and `HOLE_SIZE` — 1.651 mm
(0.065", exactly 13 px) across with a 0.7 mm drill — so no per-board pad footprint exists and
the standing preference for tuning the shared appearance constants globally is left intact.
Because the pad is an axis-aligned rectangle, what has to clear the rim is its corner diagonal
(1.031 mm), not its half-length — leaving 0.37 mm of board outside the furthest corner, about
3 px at 1:1. Both the full-size renders and a pixel scan appeared to show pads breaching the rim;
both were misreading the board's own anti-aliased edge, which registers at every angle rather
than only at the pads. Magnifying the tightest pad on each variant 12× showed unbroken board
between every pad and the rim.
What has to clear the rim is therefore the pad radius, and since both it and the inset are
constants the clearance is the same on every ring: **0.5745 mm of board outside each pad**, about
4.5 px at 1:1. It does not vary with ring size, which is worth knowing before either figure is
revised — one change moves all three variants at once.
*This paragraph was corrected after the fact.* It previously described rectangular 1.6 × 1.3 mm
pads reached through three `getSolderPad{Width,Length,HoleSize}` hooks on `AbstractMakerBoard`,
with a 0.37 mm corner-diagonal clearance arrived at by magnifying renders. None of that is in the
tree: the hooks do not exist and the pads are round. §3.3 records why — `drawSolderPads` and its
three size hooks were removed and their six callers moved onto `drawPcbSolderPads` — so it is
this paragraph that was left behind rather than the decision that is in doubt. The same removal
left two orphaned Javadoc blocks above `buildLedGradient`, since deleted. The figures above are read out of the
code rather than recomputed by hand; no render was re-checked for this correction, so the visual
question the old text claimed to settle is open again, if only trivially at 4.5 px of margin.
10. **`SevenSegmentDisplay`'s pin arrays** — **decided: the maintainer confirms they are correct.**
Three questions closed together. The arrays are right as they stand; the two bare four-digit
packages **share a pinout**; and each variant is now pinned to a named part — **5161AS** for the