mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
displays
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+236
@@ -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());
|
||||
}
|
||||
}
|
||||
+197
@@ -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());
|
||||
}
|
||||
}
|
||||
+80
-20
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user