mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-07 12:24:58 +10:00
strip
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
*
|
||||
* 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.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.EditableProperty;
|
||||
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 Strip (WS2812B)", category = "Displays & Outputs",
|
||||
author = "Branislav Stojkovic",
|
||||
description = "Addressable RGB WS2812B LED Strip (30, 60 or 144 LEDs per metre)",
|
||||
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
|
||||
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
|
||||
enableCache = true)
|
||||
public class WS2812BStrip extends AbstractMakerBoard {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Tape is sold by LEDs per metre, and everything else follows from that: the pitch is the metre
|
||||
* divided by the count, so it is derived rather than stored. Width does not follow, and is the
|
||||
* one figure here that is a stated norm rather than a measurement -- the 30 and 60 tapes are
|
||||
* usually 10 mm and the 144 usually 12 mm, because at that pitch the package leaves no room for
|
||||
* the conductor beside it.
|
||||
*/
|
||||
public enum Density {
|
||||
_30("30 LED/m", 30, 10.0d),
|
||||
_60("60 LED/m", 60, 10.0d),
|
||||
_144("144 LED/m", 144, 12.0d);
|
||||
|
||||
private final String label;
|
||||
private final int ledsPerMetre;
|
||||
private final double widthMm;
|
||||
|
||||
Density(String label, int ledsPerMetre, double widthMm) {
|
||||
this.label = label;
|
||||
this.ledsPerMetre = ledsPerMetre;
|
||||
this.widthMm = widthMm;
|
||||
}
|
||||
|
||||
@Override public String toString() { return label; }
|
||||
public int getLedsPerMetre() { return ledsPerMetre; }
|
||||
public double getWidthMm() { return widthMm; }
|
||||
|
||||
/** Centre-to-centre LED spacing: one metre shared out between that many LEDs. */
|
||||
public double getPitchMm() { return 1000.0d / ledsPerMetre; }
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
* 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"};
|
||||
|
||||
private Density density = Density._60;
|
||||
private int ledCount = DEFAULT_LED_COUNT;
|
||||
|
||||
// Rebuilt only when the count changes, because draw() runs on every repaint.
|
||||
private transient Color[] ledColors;
|
||||
|
||||
public WS2812BStrip() {
|
||||
super();
|
||||
this.bodyColor = TAPE_WHITE;
|
||||
updateControlPoints();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "Density")
|
||||
public Density getDensity() {
|
||||
if (density == null) {
|
||||
density = Density._60;
|
||||
}
|
||||
return density;
|
||||
}
|
||||
|
||||
public void setDensity(Density density) {
|
||||
this.density = density;
|
||||
updateControlPoints();
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
@EditableProperty(name = "LEDs")
|
||||
public int getLedCount() {
|
||||
if (ledCount < MIN_LED_COUNT) {
|
||||
ledCount = DEFAULT_LED_COUNT;
|
||||
}
|
||||
return ledCount;
|
||||
}
|
||||
|
||||
public void setLedCount(int ledCount) {
|
||||
this.ledCount = Math.max(MIN_LED_COUNT, Math.min(MAX_LED_COUNT, ledCount));
|
||||
this.ledColors = null;
|
||||
updateControlPoints();
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
// Both decide what you buy and how much of it, so the BOM carries them together.
|
||||
@Override
|
||||
protected String getVariantLabel() {
|
||||
return getDensity() + ", " + getLedCount() + " LEDs";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlPointNodeName(int index) {
|
||||
if (index >= 0 && index < PIN_NAMES.length) {
|
||||
return PIN_NAMES[index];
|
||||
}
|
||||
return Integer.toString(index + 1);
|
||||
}
|
||||
|
||||
private double getPitch() {
|
||||
return new Size(getDensity().getPitchMm(), SizeUnit.mm).convertToPixels();
|
||||
}
|
||||
|
||||
private double getTapeWidth() {
|
||||
return new Size(getDensity().getWidthMm(), SizeUnit.mm).convertToPixels();
|
||||
}
|
||||
|
||||
/**
|
||||
* One pitch of tape per LED, plus a lead-in at each cut end for the pads. The length is linear in
|
||||
* the count rather than proportional to it, because those two ends do not scale.
|
||||
*/
|
||||
private double getTapeLength() {
|
||||
return (getLedCount() - 1) * getPitch() + 2 * getLeadIn();
|
||||
}
|
||||
|
||||
private Color[] getLedColors() {
|
||||
int count = getLedCount();
|
||||
if (ledColors == null || ledColors.length != count) {
|
||||
ledColors = buildLedGradient(count);
|
||||
}
|
||||
return ledColors;
|
||||
}
|
||||
|
||||
/** The span the three input pads occupy across the tape, at the library's 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.
|
||||
*/
|
||||
private double getLeadIn() {
|
||||
return PAD_END_INSET.convertToPixels() + getSolderPadWidth().convertToPixels() / 2.0
|
||||
+ RGB_LED_SIZE.convertToPixels() / 2.0 + PAD_CLEARANCE.convertToPixels();
|
||||
}
|
||||
|
||||
/** Left end of the tape; control point 0 is the first pad, one inset in from the cut. */
|
||||
private double getBoardX(double x) {
|
||||
return x - PAD_END_INSET.convertToPixels();
|
||||
}
|
||||
|
||||
/** Top edge. The input pads are centred across the width, so the first sits above centre. */
|
||||
private double getBoardY(double y) {
|
||||
return y - (getTapeWidth() - getPadRunLength()) / 2.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
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;
|
||||
|
||||
rotatePoints(firstPoint, relativeOffsets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getBodyShape() {
|
||||
Point2D p0 = controlPoints[0];
|
||||
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getTapeLength(),
|
||||
getTapeWidth(), 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);
|
||||
}
|
||||
|
||||
double boardX = getBoardX(x);
|
||||
double boardY = getBoardY(y);
|
||||
double tapeH = getTapeWidth();
|
||||
|
||||
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) {
|
||||
// The LED field starts after the input pads and ends before the output pad, so the first and
|
||||
// last packages cannot sit on top of them -- at 144/m the pitch is only 6.94 mm, so a 5 mm
|
||||
// package placed half a pitch from the cut would overlap the pad it is meant to clear.
|
||||
// Between those ends the LEDs keep the real pitch, which is what the density means.
|
||||
double pitch = getPitch();
|
||||
double ledSize = RGB_LED_SIZE.convertToPixels();
|
||||
double firstCenter = boardX + getLeadIn();
|
||||
Color[] colors = getLedColors();
|
||||
int count = getLedCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
MakerBoardPainter.drawAddressableLed(g2d, firstCenter + i * pitch, boardY + tapeH / 2.0,
|
||||
ledSize, colors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
g2d.setTransform(oldTx);
|
||||
|
||||
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
|
||||
|
||||
g2d.setComposite(oldComposite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawIcon(Graphics2D g2d, int width, int height) {
|
||||
g2d.setColor(TAPE_WHITE);
|
||||
g2d.fill(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
|
||||
g2d.setColor(Color.GRAY);
|
||||
g2d.draw(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
|
||||
|
||||
Color[] colors = new Color[] {Color.decode("#FFEE00"), Color.decode("#FF1122"),
|
||||
Color.decode("#2255FF"), Color.decode("#22DD44")};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
g2d.setColor(colors[i]);
|
||||
g2d.fillRect(3 + i * (width - 6) / 4, (int) (height / 2.0 - 3), 5, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
*
|
||||
* 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 org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.components.displays.WS2812BStrip.Density;
|
||||
import org.diylc.core.measures.Size;
|
||||
import org.diylc.core.measures.SizeUnit;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Geometry tests for the strip, written after three separate attempts at the output pad's position
|
||||
* each passed every numeric check that existed while the drawing was still wrong. The assertions
|
||||
* here are the ones that would have failed: that no pad hangs off the tape, and that no pad sits
|
||||
* under an LED package.
|
||||
*/
|
||||
public class WS2812BStripTest {
|
||||
|
||||
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 double padHalfWidth() {
|
||||
return new Size(0.11d, SizeUnit.in).convertToPixels() / 2.0d;
|
||||
}
|
||||
|
||||
private static double ledHalfWidth() {
|
||||
return new Size(5.0d, SizeUnit.mm).convertToPixels() / 2.0d;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pitchFollowsFromDensity() {
|
||||
Assert.assertEquals(1000.0d / 30.0d, Density._30.getPitchMm(), 0.01d);
|
||||
Assert.assertEquals(1000.0d / 60.0d, Density._60.getPitchMm(), 0.01d);
|
||||
Assert.assertEquals(1000.0d / 144.0d, Density._144.getPitchMm(), 0.01d);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapeWidthFollowsFromDensity() {
|
||||
for (Density density : Density.values()) {
|
||||
WS2812BStrip strip = new WS2812BStrip();
|
||||
strip.setDensity(density);
|
||||
Assert.assertEquals("tape width for " + density, density.getWidthMm(),
|
||||
mm(strip.getBodyShape().getBounds2D().getHeight()), 0.01d);
|
||||
}
|
||||
}
|
||||
|
||||
/** The count drives the length, and the two lead-ins do not scale, so it is linear not doubled. */
|
||||
@Test
|
||||
public void lengthIsLinearInLedCount() {
|
||||
WS2812BStrip strip = new WS2812BStrip();
|
||||
strip.setDensity(Density._60);
|
||||
strip.setLedCount(8);
|
||||
double eight = strip.getBodyShape().getBounds2D().getWidth();
|
||||
strip.setLedCount(16);
|
||||
double sixteen = strip.getBodyShape().getBounds2D().getWidth();
|
||||
double pitch = new Size(Density._60.getPitchMm(), SizeUnit.mm).convertToPixels();
|
||||
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. */
|
||||
@Test
|
||||
public void controlPointCountIsFixedAtFour() {
|
||||
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,
|
||||
strip.getControlPointCount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ledCountIsClamped() {
|
||||
WS2812BStrip strip = new WS2812BStrip();
|
||||
strip.setLedCount(100000);
|
||||
Assert.assertEquals(WS2812BStrip.MAX_LED_COUNT, strip.getLedCount());
|
||||
strip.setLedCount(0);
|
||||
Assert.assertEquals(WS2812BStrip.MIN_LED_COUNT, strip.getLedCount());
|
||||
strip.setLedCount(-5);
|
||||
Assert.assertEquals(WS2812BStrip.MIN_LED_COUNT, strip.getLedCount());
|
||||
}
|
||||
|
||||
/** Every pad must lie within the tape. The output pad sat exactly on the far edge once. */
|
||||
@Test
|
||||
public void everyPadSitsOnTheTape() {
|
||||
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 < strip.getControlPointCount(); i++) {
|
||||
Point2D pad = strip.getControlPoint(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 right cut",
|
||||
pad.getX() + padHalfWidth() <= 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The output pad must clear the last LED package. This is the one that kept regressing: at 144/m
|
||||
* the pitch is 6.94 mm against a 5 mm package, so there is very little room to be wrong in.
|
||||
*/
|
||||
@Test
|
||||
public void outputPadClearsTheLastLed() {
|
||||
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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** And the input pads must clear the first one, which is the same failure mirrored. */
|
||||
@Test
|
||||
public void inputPadsClearTheFirstLed() {
|
||||
for (Density density : Density.values()) {
|
||||
WS2812BStrip strip = new WS2812BStrip();
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double mmToPx(double value) {
|
||||
return new Size(value, SizeUnit.mm).convertToPixels();
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ public class MakerComponentsTest {
|
||||
TFTDisplay.class,
|
||||
WS2812BStick.class,
|
||||
WS2812BRing.class,
|
||||
WS2812BStrip.class,
|
||||
|
||||
// Electro-Mechanical
|
||||
BatteryHolder18650.class
|
||||
|
||||
@@ -40,8 +40,9 @@ robotics are deliberately deferred; §10 records why.
|
||||
|
||||
## 3. Current coverage
|
||||
|
||||
Seven classes, all `category = "Displays & Outputs"`, all `bomPolicy = SHOW_ONLY_TYPE_NAME`, all
|
||||
`enableCache = true`, all extending `AbstractMakerBoard`.
|
||||
Eight classes now, all `category = "Displays & Outputs"`, all `bomPolicy = SHOW_ONLY_TYPE_NAME`,
|
||||
all `enableCache = true`, all extending `AbstractMakerBoard`. Seven are the drafts this slice set
|
||||
out to release; `WS2812BStrip` is the first of §6's additions to land.
|
||||
|
||||
| Class | Variants today | Assessment |
|
||||
|---|---|---|
|
||||
@@ -51,6 +52,7 @@ Seven classes, all `category = "Displays & Outputs"`, all `bomPolicy = SHOW_ONLY
|
||||
| `OLEDDisplay` | I2C 4-pin / SPI 7-pin | Correct pin names, and the body is now the measured 26.7 × 19.3 mm panel rather than a 27 mm square. The glass had been drawn near-square against what is really a 2:1 letterbox; the lit area is now 21.744 × 10.864 mm, which is exactly 2:1, so that complaint is closed. What remains is the original gap: no size variants — the 0.91" and 1.3" boards are §6.3. |
|
||||
| `LEDMatrix` | Single 8x8 / Compact 8x8 / 4-in-1 32x8 | Plausible single MAX7219 module with correct cascade headers, but a hardcoded `-44 mm` output-header offset and pixel-placed matrix and chip. Both are gone: the modules are the real 32 × 32 mm part drawn flush with the board, and the header rows sit one clearance in from their own edges with the spacing derived from the board rather than fitted. Whichever headers the modules cover are drawn before them and painted over, as on the real part, so the display face stays clean while the pins stay wireable (§3.2 item 9). §6.4 added the other two: the compact 8x8, which fits its driver under the module so the board is only the module (32 × 32 mm) and several can be butted together, and the 4-in-1 (128 × 32 mm, four flush modules), both standing their headers vertical on the short edges with every driver hidden. Carrying boards other than an 8x8 is what retired the `8x8` in the old class name. |
|
||||
| `TFTDisplay` | none | Was the weakest file, on two counts that are now both gone: a footprint bug, and a descriptor reading 240x320 against on-screen silk reading `320x240`. The footprint was rebuilt from the module drawing (§5 item 3) and no `320x240` string survives anywhere in the class. What is left is the gap it always had and nothing worse: no variant enum, which is §6.2's job. |
|
||||
| `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. |
|
||||
| `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). |
|
||||
|
||||
### 3.1 Two defects that are bugs, not style
|
||||
@@ -271,7 +273,40 @@ red SPI TFT and the I2C backpack, which have no canonical vendor.
|
||||
|
||||
The archetypes are covered; the gaps are inside them. Each item below is an independent commit.
|
||||
|
||||
### 6.1 NeoPixel Strip — new class `WS2812BStrip`
|
||||
### 6.1 NeoPixel Strip — new class `WS2812BStrip` — **done**
|
||||
|
||||
Everything derives from the density, which is the one figure tape is actually sold by: the pitch is
|
||||
1000 mm divided by the count (33.33, 16.67, 6.94 mm), the 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.
|
||||
The one figure not derived is the tape width, which is the plan's stated norm — 10 mm for the 30 and
|
||||
60, 12 mm for the 144 — and remains unmeasured.
|
||||
|
||||
`LedCount` is the bounded integer §11.4 assumed. Note that the item's stated reason for a bound was
|
||||
wrong: it worried about "an enormous control-point array", but the array is fixed at four whatever
|
||||
the count. The bound exists because the *shape* grows — 144 LEDs of 30/m tape is nearly five metres.
|
||||
|
||||
**The output pad's position took four attempts, three of them wrong, and the way it went is the
|
||||
point of recording it.** Each attempt reasoned from a formula and shipped; each time every numeric
|
||||
check passed while the drawing was wrong. One edit was a net no-op that restored the original
|
||||
expression while adding a comment claiming a symmetry the code did not implement. What settled it
|
||||
was measuring instead of deriving: a probe showed the pad centre sitting exactly on the tape edge,
|
||||
which exposed the cause in one line — offsets are measured from control point 0, which is itself one
|
||||
inset inboard of the near cut, so reaching one inset in from the *far* cut costs two insets, not
|
||||
one. The original expression had been right about the pad all along; the real defect was the
|
||||
lead-in, and three edits went into fixing the wrong half of the geometry.
|
||||
|
||||
`WS2812BStripTest` encodes exactly those failures — `everyPadSitsOnTheTape` and
|
||||
`outputPadClearsTheLastLed` would have caught every one — so the mistake cannot recur quietly. The
|
||||
class was also added to `releasedMakerComponentClasses`: until then it was discoverable in the app
|
||||
but asserted on by nothing, which is why the suite stayed green while the part was broken. That list
|
||||
is hand-maintained and not exhaustive, so a new maker component is invisible to every test until
|
||||
someone remembers to add it.
|
||||
|
||||
Silk was left off, following the Ring and the Stick. Worth revisiting: unlike those two, real tape
|
||||
*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.
|
||||
|
||||
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
|
||||
existing guitar and amp audience.
|
||||
@@ -570,9 +605,16 @@ gate the displays.
|
||||
transformer, or extend `AbstractMakerBoard` for consistency with its category neighbours. The
|
||||
recommendation is `DIL_IC`, because the part is a DIP package and rotation for DIP parts already
|
||||
has a transformer.
|
||||
4. **Whether `LedCount` on the strip should be a free integer or an enum** (§6.1). A free integer
|
||||
matches how someone thinks about a tape they will cut; an enum keeps the property editor
|
||||
predictable and bounds the control-point maths. The plan above assumes a bounded integer.
|
||||
4. **Whether `LedCount` on the strip should be a free integer or an enum** (§6.1) — **decided: a
|
||||
bounded integer**, as the plan assumed. An enum would invent a constraint the part does not
|
||||
have: tape is cut wherever the builder wants, so any fixed set of lengths would be arbitrary.
|
||||
|
||||
One of the arguments recorded for the enum does not survive contact with the implementation. It
|
||||
said an enum "bounds the control-point maths" — but the strip carries four control points at any
|
||||
count, because the LEDs are drawn rather than wired, so there is no maths for it to bound. The
|
||||
bound that does exist is on the drawn shape, which grows without limit: 144 LEDs of 30 LED/m
|
||||
tape is already most of five metres. `MIN_LED_COUNT`, `MAX_LED_COUNT` and `DEFAULT_LED_COUNT`
|
||||
carry it, and the getter repairs a count that deserializes as zero.
|
||||
5. **The TFT's mounting holes** — **decided, from the vendor drawing.** Four holes: all of them
|
||||
3 mm in from the side edges, the pair nearest the header 6.92 mm down from that edge so they
|
||||
clear the pin row, the lower pair 3 mm up from the bottom edge. The glass is centred between the
|
||||
|
||||
Reference in New Issue
Block a user