This commit is contained in:
Bane Stojkovic
2026-10-02 15:26:21 +02:00
parent f997ed57fb
commit a2a39f7e3b
4 changed files with 513 additions and 78 deletions
@@ -64,12 +64,10 @@ public class CharacterLCD extends AbstractMakerBoard {
// the fields beside them, not by the text. The bezel and the lit area are measured off the
// module and are sizes in their own right -- deriving them as margins off the board made the
// window grow with the board instead of staying the size of the part.
_16x2("16x2", 16, 2, 80.0, 35.0, 72.2, 24.1, 64.5, 14.5),
_20x4("20x4", 20, 4, 98.0, 60.0, 96.8, 39.3, 77.0, 25.2);
_16x2("16x2", 80.0, 35.0, 72.2, 24.1, 64.5, 14.5),
_20x4("20x4", 98.0, 60.0, 96.8, 39.3, 77.0, 25.2);
private final String label;
private final int cols;
private final int rows;
private final double widthMm;
private final double heightMm;
private final double bezelWidthMm;
@@ -77,11 +75,9 @@ public class CharacterLCD extends AbstractMakerBoard {
private final double displayWidthMm;
private final double displayHeightMm;
LCDSize(String label, int cols, int rows, double widthMm, double heightMm, double bezelWidthMm,
LCDSize(String label, double widthMm, double heightMm, double bezelWidthMm,
double bezelHeightMm, double displayWidthMm, double displayHeightMm) {
this.label = label;
this.cols = cols;
this.rows = rows;
this.widthMm = widthMm;
this.heightMm = heightMm;
this.bezelWidthMm = bezelWidthMm;
@@ -91,8 +87,6 @@ public class CharacterLCD extends AbstractMakerBoard {
}
@Override public String toString() { return label; }
public int getCols() { return cols; }
public int getRows() { return rows; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
public double getBezelWidthMm() { return bezelWidthMm; }
@@ -27,6 +27,8 @@ import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
@@ -44,13 +46,15 @@ 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 = "TFT Touch Screen (ILI9341)", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "2.8\" 240x320 Color TFT LCD Display with SPI Interface, Touch, and SD Slot",
@ComponentDescriptor(name = "TFT Display", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Color TFT LCD display module with an SPI interface",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
@@ -58,34 +62,88 @@ public class TFTDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum Controller {
// Each board is dimensioned from its module outline, and the header sits centred on the short
// top edge at the offset recorded here. Active areas are the nominal diagonal taken at the
// panel's aspect ratio -- 2.8" at 4:3 gives 43.2 x 57.6, 1.8" at 4:5 gives 28.56 x 35.7, and
// the round 1.28" gives a 32.5 mm lit circle -- which makes them derivations rather than
// measurements, noted so they are not built upon. A glass length of zero means the module has
// no separate dark panel wider than its own lit area, so only the screen is drawn, and a hole
// size of zero means the board has no mounting holes at all.
// The round board is described by the same two outline figures as the others: its width is the
// disc diameter and its length runs from the tab's outer edge to the bottom of the disc, so the
// tab's projection is the difference between them and is not carried separately.
ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)", 50.0d, 86.0d, 43.2d, 57.6d, 69.1d, 3.0d,
3.0d, 2.5d, 0d,
new String[] {"VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)",
"T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"}),
ST7735_1_8("1.8\" ST7735 128x160", 34.0d, 45.8d, 28.56d, 35.7d, 0d, 1.5d, 3.0d, 3.0d, 0d,
new String[] {"GND", "VCC", "SCK", "SDA", "RES", "DC", "CS", "BL"}),
GC9A01_1_28("1.28\" GC9A01 240x240 Round", 38.0d, 45.5d, 32.5d, 32.5d, 0d, 1.76d, 0d, 0d,
22.9d,
new String[] {"VCC", "GND", "SCL", "SDA", "DC", "CS", "RST"});
private final String label;
private final double boardWidthMm;
private final double boardLengthMm;
private final double screenWidthMm;
private final double screenLengthMm;
private final double glassLengthMm;
private final double headerOffsetMm;
private final double holeInsetMm;
private final double holeSizeMm;
private final double tabWidthMm;
private final String[] pinNames;
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, double glassLengthMm, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames) {
this.label = label;
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.screenWidthMm = screenWidthMm;
this.screenLengthMm = screenLengthMm;
this.glassLengthMm = glassLengthMm;
this.headerOffsetMm = headerOffsetMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
this.tabWidthMm = tabWidthMm;
this.pinNames = pinNames;
}
@Override public String toString() { return label; }
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardLengthMm() { return boardLengthMm; }
public double getScreenWidthMm() { return screenWidthMm; }
public double getScreenLengthMm() { return screenLengthMm; }
public double getGlassLengthMm() { return glassLengthMm; }
public double getHeaderOffsetMm() { return headerOffsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public String[] getPinNames() { return pinNames; }
/** Inset of every mounting hole from each of the two edges nearest it. */
public double getHoleInsetMm() { return holeInsetMm; }
public boolean hasMountingHoles() { return holeSizeMm > 0; }
public boolean hasGlass() { return glassLengthMm > 0; }
/** Width of the tab the header sits on; meaningful only for a round board. */
public double getTabWidthMm() { return tabWidthMm; }
/** How far the tab projects past the disc: whatever the outline is longer than it is wide. */
public double getTabProjectionMm() { return boardLengthMm - boardWidthMm; }
public boolean isRound() { return tabWidthMm > 0; }
}
public static Color TFT_RED = Color.decode("#C0392B");
public static Color SCREEN_BG = Color.decode("#111111");
public static Color GLASS_COLOR = Color.decode("#1A1A1A");
// The board is taller than it is wide, with the header along the short top edge. Rotation is the
// Orientation property's job, so the drawing keeps the orientation the module ships in.
public static Size BOARD_WIDTH = new Size(50.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(86.0d, SizeUnit.mm);
// the glass runs the full width of the board and sits centred between the two rows of mounting
// holes, leaving bare PCB for the header above it and a narrow lip below
public static Size GLASS_LENGTH = new Size(69.1d, SizeUnit.mm);
// 2.8" diagonal at 4:3, centred within the glass
public static Size SCREEN_WIDTH = new Size(43.2d, SizeUnit.mm);
public static Size SCREEN_LENGTH = new Size(57.6d, SizeUnit.mm);
// clearance from the top edge to the pin row; the outline dimensions are published but this one
// is not, so it is taken from the pitch of the 2.54mm header
public static Size HEADER_OFFSET = new Size(3.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_INSET = new Size(3.0d, SizeUnit.mm);
// the pair nearest the header sits further in to clear the pin row; the other two keep the 3mm
// inset on all sides
public static Size MOUNTING_HOLE_HEADER_INSET = new Size(6.92d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.5d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
"VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)",
"T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"
};
private Controller controller = Controller.ILI9341_2_8;
public TFTDisplay() {
super();
@@ -93,22 +151,45 @@ public class TFTDisplay extends AbstractMakerBoard {
updateControlPoints();
}
@EditableProperty(name = "Controller")
public Controller getController() {
return controller == null ? Controller.ILI9341_2_8 : controller;
}
public void setController(Controller controller) {
this.controller = controller;
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getController().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
String[] pinNames = getController().getPinNames();
if (index >= 0 && index < pinNames.length) {
return pinNames[index];
}
return "Pin " + (index + 1);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** Left edge of the board, derived so that the pin row sits centred on the top edge. */
private double getBoardX(double x) {
Controller controller = getController();
double spacing = PIN_SPACING.convertToPixels();
return x - (BOARD_WIDTH.convertToPixels() - (PIN_NAMES.length - 1) * spacing) / 2.0;
double span = (controller.getPinNames().length - 1) * spacing;
return x - (px(controller.getBoardWidthMm()) - span) / 2.0;
}
private double getBoardY(double y) {
return y - HEADER_OFFSET.convertToPixels();
return y - px(getController().getHeaderOffsetMm());
}
@Override
@@ -116,8 +197,9 @@ public class TFTDisplay extends AbstractMakerBoard {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < PIN_NAMES.length; i++) {
int pinCount = getController().getPinNames().length;
double[][] relativeOffsets = new double[pinCount][2];
for (int i = 0; i < pinCount; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
@@ -128,14 +210,33 @@ public class TFTDisplay extends AbstractMakerBoard {
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
Controller controller = getController();
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
if (controller.isRound()) {
// A disc with a rectangular tab on top. The tab has to be carried down to the disc's centre
// line before the two are unioned: taken only as far as the top of the disc it would meet it
// at a single tangent point and leave a notch either side of it. Below the line where the
// disc is as wide as the tab, the disc governs the outline and only the projection shows.
double discR = boardW / 2.0;
double discCy = boardY + px(controller.getTabProjectionMm()) + discR;
double tabW = px(controller.getTabWidthMm());
Area body = new Area(new Ellipse2D.Double(boardX, discCy - discR, boardW, boardW));
body.add(new Area(new Rectangle2D.Double(boardX + (boardW - tabW) / 2.0, boardY, tabW,
discCy - boardY)));
return body;
}
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), radius, radius);
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
@@ -149,8 +250,9 @@ public class TFTDisplay extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
Controller controller = getController();
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
double boardX = getBoardX(x);
double boardY = getBoardY(y);
@@ -168,34 +270,59 @@ public class TFTDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
double holeInset = MOUNTING_HOLE_INSET.convertToPixels();
double headerInset = MOUNTING_HOLE_HEADER_INSET.convertToPixels();
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + headerInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + headerInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
// The panel is centred between the two rows of mounting holes rather than measured from an
// edge, which is what keeps it clear of both pairs. A board without holes has nothing to be
// centred between, so it falls back to the space below the pin row.
double bandTop = boardY + px(controller.getHeaderOffsetMm());
double bandBottom = boardY + boardH;
// centred between the two rows of mounting holes rather than measured from an edge, which is
// what keeps it clear of both pairs
double glassH = GLASS_LENGTH.convertToPixels();
double glassY = boardY + (headerInset + boardH - holeInset) / 2.0 - glassH / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new Rectangle2D.Double(boardX, glassY, boardW, glassH));
if (controller.hasMountingHoles()) {
double holeInset = px(controller.getHoleInsetMm());
double holeSize = px(controller.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
bandTop = boardY + holeInset;
bandBottom = boardY + boardH - holeInset;
}
double screenW = SCREEN_WIDTH.convertToPixels();
double screenH = SCREEN_LENGTH.convertToPixels();
double screenX = boardX + (boardW - screenW) / 2.0;
double screenY = glassY + (glassH - screenH) / 2.0;
double screenW = px(controller.getScreenWidthMm());
double screenH = px(controller.getScreenLengthMm());
Shape screen;
if (controller.isRound()) {
// the lit circle is centred on the disc, which sits below the tab rather than on the
// centre line of the bounding box
double discR = boardW / 2.0;
double discCx = boardX + discR;
double discCy = boardY + px(controller.getTabProjectionMm()) + discR;
screen = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
screenH);
} else {
double bandCentre = (bandTop + bandBottom) / 2.0;
double screenY;
if (controller.hasGlass()) {
double glassH = px(controller.getGlassLengthMm());
double glassY = bandCentre - glassH / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new Rectangle2D.Double(boardX, glassY, boardW, glassH));
screenY = glassY + (glassH - screenH) / 2.0;
} else {
screenY = bandCentre - screenH / 2.0;
}
screen = new Rectangle2D.Double(boardX + (boardW - screenW) / 2.0, screenY, screenW,
screenH);
}
g2d.setColor(SCREEN_BG);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.fill(screen);
g2d.setColor(Color.DARK_GRAY);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.draw(screen);
}
g2d.setTransform(oldTx);
@@ -0,0 +1,279 @@
/*
*
* 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.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.displays.TFTDisplay.Controller;
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 controller variants. The pin count differs between them, so the checks
* that matter are the ones that would catch an array and an outline disagreeing: that the header
* still fits across the board it belongs to, and that the screen and the mounting holes do not
* land on top of each other once a different outline is in play.
*/
public class TFTDisplayTest {
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 px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
private static TFTDisplay of(Controller controller) {
TFTDisplay display = new TFTDisplay();
display.setController(controller);
return display;
}
@Test
public void boardSizeFollowsTheController() {
for (Controller controller : Controller.values()) {
Rectangle2D bounds = of(controller).getBodyShape().getBounds2D();
Assert.assertEquals(controller + " width", controller.getBoardWidthMm(),
mm(bounds.getWidth()), 0.01d);
Assert.assertEquals(controller + " length", controller.getBoardLengthMm(),
mm(bounds.getHeight()), 0.01d);
}
}
@Test
public void controlPointCountFollowsThePinArray() {
for (Controller controller : Controller.values()) {
Assert.assertEquals(controller.toString(), controller.getPinNames().length,
of(controller).getControlPointCount());
}
Assert.assertEquals(14, Controller.ILI9341_2_8.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_1_8.getPinNames().length);
Assert.assertEquals(7, Controller.GC9A01_1_28.getPinNames().length);
}
@Test
public void pinNamesComeFromTheController() {
TFTDisplay display = of(Controller.ST7735_1_8);
String[] expected = new String[] {"GND", "VCC", "SCK", "SDA", "RES", "DC", "CS", "BL"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
/** Switching variants must resize the array, not leave the longer one's points behind. */
@Test
public void switchingControllerResizesTheControlPoints() {
TFTDisplay display = new TFTDisplay();
Assert.assertEquals(14, display.getControlPointCount());
display.setController(Controller.ST7735_1_8);
Assert.assertEquals(8, display.getControlPointCount());
display.setController(Controller.ILI9341_2_8);
Assert.assertEquals(14, display.getControlPointCount());
}
@Test
public void pinsSitAtHeaderPitchAlongOneRow() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
for (int i = 1; i < display.getControlPointCount(); i++) {
Point2D previous = display.getControlPoint(i - 1);
Point2D current = display.getControlPoint(i);
Assert.assertEquals(controller + " pitch at pin " + i, 2.54d,
mm(current.getX() - previous.getX()), 0.01d);
Assert.assertEquals(controller + " pin " + i + " left the row", previous.getY(),
current.getY(), 0.01d);
}
}
}
/** A pin array longer than its board would hang the header off both ends. */
@Test
public void headerFitsAcrossTheBoard() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Rectangle2D board = display.getBodyShape().getBounds2D();
for (int i = 0; i < display.getControlPointCount(); i++) {
Point2D pin = display.getControlPoint(i);
Assert.assertTrue(controller + " pin " + i + " hangs off the board",
pin.getX() >= board.getX() && pin.getX() <= board.getMaxX());
}
}
}
@Test
public void screenFitsWithinTheBoard() {
for (Controller controller : Controller.values()) {
Assert.assertTrue(controller + " screen is wider than its board",
controller.getScreenWidthMm() < controller.getBoardWidthMm());
Assert.assertTrue(controller + " screen is longer than its board",
controller.getScreenLengthMm() < controller.getBoardLengthMm());
}
}
/**
* The screen is centred between the hole rows, so a board whose holes move has to be rechecked:
* on the 1.8" the two pairs are only 3 mm in and the panel very nearly reaches them.
*/
@Test
public void screenClearsTheMountingHoles() {
for (Controller controller : Controller.values()) {
if (!controller.hasMountingHoles()) {
continue;
}
double holeInset = px(controller.getHoleInsetMm());
double holeRadius = px(controller.getHoleSizeMm()) / 2.0;
double boardH = px(controller.getBoardLengthMm());
// every hole shares one inset, so the midpoint of the two rows is the middle of the board
double bandCentre = boardH / 2.0;
double panelH = controller.hasGlass() ? px(controller.getGlassLengthMm())
: px(controller.getScreenLengthMm());
double panelTop = bandCentre - panelH / 2.0;
double panelBottom = bandCentre + panelH / 2.0;
Assert.assertTrue(controller + " panel overlaps the header-side holes",
panelTop >= holeInset + holeRadius);
Assert.assertTrue(controller + " panel overlaps the far holes",
panelBottom <= boardH - holeInset - holeRadius);
}
}
@Test
public void mountingHolesSitInsideTheBoard() {
for (Controller controller : Controller.values()) {
if (!controller.hasMountingHoles()) {
continue;
}
double holeRadius = controller.getHoleSizeMm() / 2.0;
Assert.assertTrue(controller + " holes breach an edge",
controller.getHoleInsetMm() >= holeRadius);
Assert.assertTrue(controller + " far holes breach the bottom edge",
controller.getHoleInsetMm() + holeRadius <= controller.getBoardLengthMm());
}
}
/**
* Every hole sits the same distance in from the two edges nearest it. The 2.8" board carried a
* deeper inset on the header-side pair for a while, which is the regression this guards.
*/
@Test
public void mountingHolesAreUniformlyInsetFromEveryEdge() {
Assert.assertEquals(3.0d, Controller.ILI9341_2_8.getHoleInsetMm(), 0.01d);
Assert.assertEquals(3.0d, Controller.ST7735_1_8.getHoleInsetMm(), 0.01d);
}
/** Without this the BOM collapses every variant of the part into a single row. */
@Test
public void bomValueCarriesTheController() {
for (Controller controller : Controller.values()) {
Assert.assertEquals(controller.toString(), of(controller).getValueForDisplay());
}
Assert.assertNotEquals(of(Controller.ILI9341_2_8).getValueForDisplay(),
of(Controller.ST7735_1_8).getValueForDisplay());
}
/** Only the 2.8" module has a dark panel wider than its lit area; the others draw the screen. */
@Test
public void onlyTheLargeBoardCarriesAGlassPanel() {
Assert.assertTrue(Controller.ILI9341_2_8.hasGlass());
Assert.assertFalse(Controller.ST7735_1_8.hasGlass());
Assert.assertFalse(Controller.GC9A01_1_28.hasGlass());
}
@Test
public void onlyTheRoundBoardHasATab() {
Assert.assertFalse(Controller.ILI9341_2_8.isRound());
Assert.assertFalse(Controller.ST7735_1_8.isRound());
Assert.assertTrue(Controller.GC9A01_1_28.isRound());
Assert.assertEquals(22.9d, Controller.GC9A01_1_28.getTabWidthMm(), 0.01d);
}
/** The projection is the outline's length beyond its width, not a figure of its own. */
@Test
public void tabProjectionFollowsFromTheOutline() {
Assert.assertEquals(7.5d, Controller.GC9A01_1_28.getTabProjectionMm(), 0.01d);
}
@Test
public void roundBoardHasNoMountingHoles() {
Assert.assertFalse(Controller.GC9A01_1_28.hasMountingHoles());
}
@Test
public void roundPinNamesComeFromTheController() {
TFTDisplay display = of(Controller.GC9A01_1_28);
String[] expected = new String[] {"VCC", "GND", "SCL", "SDA", "DC", "CS", "RST"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
/**
* The outline is a disc with a narrower tab on it, so the corners of its bounding box are off the
* board while the middle of the top edge is on it. Taking the tab only as far as the top of the
* disc would have left a notch either side of the tangent point, which these corners would not
* catch but the lower check would: low down, the disc is wider than the tab.
*/
@Test
public void roundBodyIsADiscWithATab() {
TFTDisplay display = of(Controller.GC9A01_1_28);
Shape body = display.getBodyShape();
Rectangle2D bounds = body.getBounds2D();
Assert.assertEquals("disc diameter across", 38.0d, mm(bounds.getWidth()), 0.01d);
Assert.assertEquals("tab tip to disc bottom", 45.5d, mm(bounds.getHeight()), 0.01d);
double x = bounds.getX();
double y = bounds.getY();
Assert.assertTrue("the middle of the tab is off the board",
body.contains(x + px(19.0d), y + px(1.0d)));
Assert.assertFalse("the top left corner is on the board",
body.contains(x + px(1.0d), y + px(1.0d)));
Assert.assertFalse("the top right corner is on the board",
body.contains(x + px(37.0d), y + px(1.0d)));
// at the disc's centre line the board is wider than the tab, which is what proves the union
// took the disc's outline and not the rectangle's
Assert.assertTrue("the disc is no wider than its tab",
body.contains(x + px(2.0d), y + px(26.5d)));
Assert.assertTrue("the centre of the disc is off the board",
body.contains(x + px(19.0d), y + px(26.5d)));
}
}
+46 -11
View File
@@ -66,7 +66,7 @@ happened to come back true, and only after a fix.
| `CharacterLCD` | 16x2 / 20x4, I2C backpack / 16-pin parallel | Correct HD44780 and PCF8574 pin data. The bodies are now maintainer-given at 80 × 35 and 98 × 60 mm — the 16x2 height had been 36, so "correct bodies" was never true of it. The screen geometry was raw pixels — an eyeballed `bezelMarginX = (16x2) ? 45 : 40` and a window derived as a margin off the board — and is now measured: a 72.2 × 24.1 mm bezel around a 64.5 × 14.5 mm lit area on the 16x2, 77 × 25.5 around 70.4 × 20.4 on the 20x4, carried on `LCDSize` and centred rather than subtracted. Headers are placed from measurements too (§3.2 item 7). No pixel literal survives in `draw()`; only the mounting holes remain unsourced (§11.8). The `Font` it once allocated inside `draw()` went with the demo text in item 7; the one that remains is in `drawIcon`, which runs per toolbox icon rather than per repaint. |
| `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. |
| `TFTDisplay` | ILI9341 2.8" / ST7735 1.8" / GC9A01 1.28" round | 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. The gap it always had -- no variant enum -- is now half closed: §6.2's `Controller` enum has landed with the ILI9341 and the ST7735, leaving the ST7789 (blocked on a contradictory outline) and the round GC9A01. |
| `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. |
| `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). |
@@ -266,9 +266,11 @@ one net, so the pads are now named `GND_1`…`GND_4` and the silkscreen prints "
`getSilkPinLabel`. **The control-point count stays at 8.**
Both of the items these left open have since been settled from the vendor drawing and are recorded
in §11.5 and §11.6. The TFT carries **four** mounting holes, all 3 mm in from the side edges, with
the pair nearest the header 6.92 mm down from that edge to clear the pin row and the lower pair
3 mm up from the bottom. They are drawn after the glass so that all four stay visible. The Stick's
in §11.5 and §11.6. The TFT carries **four** mounting holes, every one 3 mm in from each of the two
edges nearest it. The header-side pair was carried at a deeper 6.92 mm inset for a while so that it
would clear the pin row; it sits well outside that row horizontally and never needed to, and the
inset is now uniform with the field that distinguished the two pairs removed. The holes are drawn
before the glass, which is placed to clear them, and `TFTDisplayTest` asserts that clearance. The Stick's
front face now carries no silkscreen at all, which is what the real board does — its pad names are
printed on the back — and with the labels gone the LED row takes its true layout, eight 5 mm
packages centred, with the inset derived rather than tuned.
@@ -351,6 +353,38 @@ length-scales-with-count assertion, draw smoke at both densities. **~1 day inclu
The biggest structural gap, and it lands on top of the §5 item 3 footprint fix, so do it in the same
pass rather than twice.
**Progress.** The `Controller` enum has landed with three of the four. Beside the existing
`ILI9341_2_8` there is now a
new `ST7735_1_8` (34 x 45.8 mm, eight pins `GND VCC SCK SDA RES DC CS BL`, header 1.5 mm in from the
top edge, four 3 mm holes 3 mm in from each edge). Board size, pin array, header offset, hole pattern
and screen now all come off the enum; `updateControlPoints` sizes the array from the variant's pin
list, and `getVariantLabel` feeds the controller into the BOM value so the variants no longer collapse
into one row. The descriptor was renamed from "TFT Touch Screen (ILI9341)" to "TFT Display", which the
class can still afford because it has not shipped (D6). `TFTDisplayTest` covers it in 11 tests, and
the default variant's render is byte-identical to the pre-refactor one.
Item 5 above is **stale and dropped**: `draw()` has no SD slot or touch hardware to branch on. The
slot was removed in §5 item 3 because the reader is on the back, and touch was only ever pins. The
only per-variant difference left is the pin array, which item 2 covers -- which also makes the
1.5-2 day estimate too high. The round outline is the one genuinely new piece of drawing remaining.
**`ST7789_1_54` is blocked on a contradiction rather than a missing figure.** The supplied outline of
27.78 x 39.22 mm cannot be a PCB: a 1.54" 240x240 panel is square, so its active area is
39.116 / sqrt(2) = 27.66 mm, which would leave 0.06 mm of bezel across the entire width. Either that
figure describes a bare glass module rather than a breakout, or the width is a transcription slip --
37.78 mm would give 5.06 mm of bezel each side and the same proportions as the 1.8".
**`GC9A01_1_28` has landed as well:** a 38 mm disc with a 22.9 mm tab projecting 7.5 mm, the header
1.76 mm in from the tab edge, no mounting holes, and seven pins `RST CS DC SDA SCL GND VCC` -- the
clock pin confirmed as `SCL` rather than the `SLC` typo some of these boards carry on their silk.
Two things about that outline are worth keeping. The body is the disc unioned with the tab, and the
tab has to be carried down to the disc's centre line before the union: taken only as far as the top
of the disc, the two shapes meet at a single tangent point and leave a notch either side of it. And
the projection is not stored, being whatever the outline is longer than it is wide, so the round
board is described by the same two figures as every other variant. The lit area is a 32.5 mm circle
centred on the disc rather than on the bounding box, since the tab offsets one from the other.
1. `Controller` enum: `ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)")`,
`ST7735_1_8("1.8\" ST7735 128x160")`, `ST7789_1_54("1.54\" ST7789 240x240")`,
`GC9A01_1_28("1.28\" GC9A01 240x240 Round")`. Default stays the ILI9341 so the icon is unchanged.
@@ -767,13 +801,14 @@ gate the displays.
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
two hole rows rather than measured from an edge, which is what reconciles a 69.1 mm glass with
holes only 3 mm from the bottom: its midpoint is 44.96 mm from the top, so it spans 10.41 mm to
79.51 mm and clears both pairs. Both are derived in `draw()` from the hole constants, so moving
a hole moves the glass with it.
5. **The TFT's mounting holes** — **decided, and since corrected.** Four holes, every one 3 mm in
from each of the two edges nearest it. The header-side pair was carried at 6.92 mm for a while
on the reasoning that it had to clear the pin row; it does not, that pair sitting outside the row
horizontally, and the maintainer corrected it to a uniform inset. The glass is still centred
between the two hole rows rather than measured from an edge, which with a uniform inset puts its
midpoint at the middle of the board: 43 mm from the top, so a 69.1 mm glass spans 8.45 mm to
77.55 mm and clears both pairs by 4.2 mm. Both are derived in `draw()` from the hole constants,
so moving a hole moves the glass with it.
6. **Where the Stick's pad labels go** — **decided: nowhere.** The front face carries no
silkscreen, matching the board, which prints its pad names on the back; node-name tooltips and
the netlist already carry the names. With the labels gone the LED row takes the real layout —